tl;dr
- Mutation XSS using namespace confusion
- Parsing inconsistency in JSDOM
Challenge Points: 666
No. of solves: 11
Solved by: Luc1f3r,ma1f0y,lu513n
Challenge Description
Wake up, samurai. We have a city to burn!
Analysis
The challenge has two endpoints./note
:- for create and view a note/visit
:- reporting the note
First lets look at the /note
endpoint.
1 | app.get('/note', (req, res) => { |
Here the note we submitted is passed through a custom sanitizer. This custom sanitizer looks for tags and attributes which can be used for triggering XSS.
Now let’s look at the /visit
endpoint .
1 | app.post('/visit', async (req, res) => { |
The visit() function in this endpoint calls the bot and it visits the given url.
1 |
|
Exploitation
For getting the flag there are two steps.
1st Step - XSS through Mutation
First we have to bypass the custom sanitizer to get mutation XSS. The blocked tags and attributes are
1 | const BLOCKED_TAG = /(script|iframe|a|img|svg|audio|video)$/i |
Here the sanitizer uses JSDOM for sanitizing the note.
JSDOM is a library which parses and interacts with assembled HTML just like a browser. The benefit is that it isn’t actually a browser. Instead, it implements web standards like browsers do.
^^JSDOM
For the XSS we would have to mutate ordinary HTML tags into dangerous ones.
If you give the following HTML in browser
1 | <form> hello |
Then it will parse into DOM as
In the browser, form element cannot be nested in itself. If it is nested as given above it will remove inner form tag from the DOM.
The important part is that, nested form tags are possible in JSDOM.
There is a similar research on nested forms in DOMpurify.
Using this parsing inconsistency in JSDOM for nested tags, we can exploit the website. This can be done by giving nested forms into the custom sanitizer and it will take that as normal tags but in the browser it will trigger an XSS.
The exploit is<form><math><mtext></form><form><mglyph><style></math><script>alert(10)</script>
Then the sanitizer serializes this into …<form><math><mtext><form><mglyph><style></math><script>alert(10)</script></style></mglyph></form></mtext></math></form>
But the browser takes this as<form><math><mtext><mglyph><style></style></mglyph></mtext></math><script>alert(10)</script></form>
And the exploit will parse into the DOM as
It is because the form element comes as the direct child of another form, which is not possible, thus the inner form tag is removed from the DOM. Then the </math>
tag closes the tags before it and the script tag comes outside.
So now we have successfully got XSS. But how do we get the flag?
2nd Step - Getting the flag
The flag is in the visit() function
1 | async function visit(path) { |
Here the flag is only returned from the catch blocks.
One way to get the flag is using the inner catch block. It catches the exception ‘puppeteer.ProtocolError’ but there is also a finally block which will alter the response.
The other way is using the outer catch block which catches the exceptions even before entering the next try block. Hence our aim is to somehow enter the outer catch block.
This line, await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 2000 });
stands out because page.goto() function will wait for the ‘domcontent’ to be loaded and the maximum time for the domcontent for loading is given as ‘2000’ milli seconds
What happens if it doesn’t load the domcontent of the given url in the given time is that it will create an error!!
Using this we can enter the outer catch block. We can just give a url which will take more than 2000 milli seconds to load and combine it with the mXSS and get the flag.
The final exploit will be
<form><math><mtext></form><form><mglyph><style></math><script>window.location="https://app.requestly.io/delay/4000/<AnyWebsite>"</script>```