tl;dr
- Leak csrf token bypassing document.domain
- visiting
/profile/
will not change the nonce - Leak nonce using dangling markup in firefox
- Add XSS payload using the csrf to get the flag
Challenge Points: 322
No. of solves: 7
Solved by: ma1f0y,Lu513n
Challenge Description
Just a simple app to store notes.
Analysis
The challenge was a simple application, which will store our secret in localStorage.Our goal was to get the flag which is stored in the admin’s localStorage using xss.
Analyzing the source code of the app.
1 | const newCookie = rand() |
we can see that when we log in to the application it will create a cookie named id with a random value for us, and assign a random csrf token and random nonce to that corresponding cookie. And the site uses those csrf token and nonce in CSP to protect against CSRF and XSS
In profile.ejs which will be rendered when in /profile
we have direct html injection using username.
1 | <div class=main> |
And there is getSettings.js which is used to set the csrf token into the page.
Exploitation
We have to leak the CSRF token to change the admin’s username and then only we can have HTML injection. To do so we can load the getSettings.js on our site and make it set the csrf token on the input feild on our site. But there is some check on the js which needs to bypass.
1 | if (isInWindowContext() && document.domain === '<%= domain %>') |
The isInWindowContext()
will retrun true when the script is loaded in a window, so the only check valid is that document.domain should be the domain of the challenge site, we can easily bypass the check by defining the domain property on the document object from our site itself.
PoC:
1 | <html> |
after getting the csrf token we can change the name to whatever html we want, But due to csp we can’t get xss
1 | const csp = (id && db.cookies[id] && db.cookies[id].nonce) ? `script-src 'nonce-${db.cookies[id].nonce}'` : ''; |
When we observe the profile page:
After our injection point, we can see they are using single quotes in the type attribute of the script tag after the nonce attribute. So we can use dangling markup injection to leak the nonce, using a meta tag to redirect to our site and with an opening single quote that will close after the nonce part.
Actually, if we check
Chrome blocks HTTP URLs with “<” or “\n” in it.
So our dangling markup will not work in chrome based browser. Luckily the challenge’s admin bot was using firefox . So our exploit will work like a charm
Now we have the nonce but there is still a problem left, whenever the /profile
page is loaded it contains an image tag with csp.gif
as src, which is used to change the nonce each time after we load the page.
1 | app.get('/csp.gif', shouldBeLoggedIn, (req, res) => { |
So if we get the nonce value from the page, we can’t use it again , as the nonce will be changed when the csp.gif loads. So our aim is to somehow make the page doesn’t load the csp.gif .
Another close observation of the page will give you the answer, the csp.js is using the relative path. So instead of visiting the profile file page using /profile
we can visit /profile/
and that will make the csp.js request to /profile/csp.js
which is not a valid endpoint. Thus csp will not change and we can reuse the csp we stole using dangling markup.
Exploit script
1 | <script> |
Host the script in the webhook server and change the url to your webhook server, Submit that url to the admin bot and get the flag !!.
Flag
LINECTF{72fdb8db303404e8388062c7233f248e}