Skip to main content

Command Palette

Search for a command to run...

Intigriti's Nov XSS Challenge Writeup

Updated
β€’5 min read
Intigriti's Nov XSS Challenge Writeup
G
Hey there πŸ‘‹ , I'm Godson, a dude who loves doing source-code reviews and web security πŸ‘¨β€πŸ’». I'm passionate about ensuring that web applications are secure πŸ”’ uncovering vulnerabilities 🐞 and keeping applications safe πŸ”.
πŸ’»
Challenge URL: https://challenge-1122.intigriti.io/

This is an XSS challenge. Guess what? This is also a note-taking application like my (shameless plug) previous month's challenge. The goal of the challenge is to take over the admin's account, which has the flag in it.


Intro

  • This challenge is organized in a bit different weird way.

  • The notes application runs on api.challenge-1122.intigriti.io, and cdn.challenge-1122.intigriti.io is used to store the notes and profile pictures of users.

  • Interestingly, cdn.challenge-1122.intigriti.io uses Varnish to cache static files.

  • Also, one of the JavaScript files contains a reference to the staging.challange-1122.intigriti.io domain. Interestingly enough, this staging domain also runs the same application.

  • JSON Web Toked is used for the session. Did that ring a πŸ””? Yes, I found that the production and the staging domain use the same JWT secret to sign the token. This means that we can register godson@0xgodson.com on both the production and staging domains, which are two totally different accounts in different environments. However, we can use the JWT token we obtain on the staging domain to access the godson@0xgodson.com account in the production environment, as the same JWT secret is used to sign the token.

  • So, technically, we can register any username on staging and use the signed JWT on api.challenge-1122.intigriti.io to take over any account. (Note that the admin bot will create new admin accounts every time when it visits a submitted URL. β€” username is also unpredictable)

Notes

  • Notes are stored in the pattern of cdn.challenge-1122.intigiti.io/<username>-<uuid>.html. Here, UUID is the note’s unique UUID.

  • Also, the server sets a CSP header in HTTP response β€” Content-Security-Policy: script-src 'none'; object-src 'none'. Very strict CSP, as it blocks any script execution.

Profile Pic Caching

  • Remember that I mentioned Varnish is used? Varnish is primarily used for caching. After some enumeration, I observed that profile pictures are cached by Varnish. The following screenshot shows that profile pictures are cached and served from cache X-Cache: HIT when they requested more than one times:

  • But what does Varnish cache? Only image files? After playing around a bit, I observed that vanish caches the files if its extension is something like .png or .jpg, etc.

Caching the Uncached

  • Remember that the server sets a CSP header to prevent script execution? The CSP header is only set when it returns valid content. When an HTTP request is sent to a non-existing endpoint, the CSP header is not attached to it.

  • Interestingly, the application does not return a 404 status when an HTTP request is sent to a non-existing endpoint. It sets the HTTP status to 200 but includes an error message indicating the 404 page. Similarly, it does not attach the CSP header. The following screenshot shows an HTTP request that was sent a couple of times to the server so that Varnish can cache it. Varnish caches it because it ends with .png, thinking that it’s a static file. It can be observed that the status code is 200, does not include the CSP header, and user input (from the URL path) is reflected in the HTTP response. More than that, the content type is set to text/html πŸ˜ƒ.

  • Since the URL is reflected in the HTTP response, it can be abused for XSS with the help of Varnish. For example, the following HTTP request is issued twice to cache a simple alert. From the HTTP response header, it can be observed that Varnish has cached it because the request path ends with .png.

  • Visiting the URL in the browser gives us a sweet popup πŸŽ‰:

  • You could ask why we need to abuse Varnish for XSS when we already can directly exploit this like a reflected XSS. We need to do this way because the browser would auto URL encode characters like < or >, and when the server receives it, it will not URL decode it. So, even if we inject an XSS payload, the response will contain as if it is URL encoded, which the browser actually encodes.

  • But, with a Proxy tool like Burp Suite, we can send a payload by having it automatically URL encoded, ensuring that the server receives it as it is and not as URL encoded.

  • So, we are using Burp Suite to cache the payload and visiting the browser to get the cached response. It’s valid to think the server would still receive a URL-encoded payload when we try to access the malicious link. Yes, that’s right, but Varnish is our guy πŸ˜‰. It decodes it for us, finds the cache key, and returns the cached response (the XSS payload stored here) without forwarding the request to the server.

Final Exploit

  • I found two possible solutions.

    • Use the above XSS to register a service worker on cdn.challenge-1122.intigriti.io to cache all requested pages.

    • Use the XSS to redirect the Admin bot to api.challenge-1122.intigriti.io, and when this page loads, notes created by the admin bot will be loaded into the page. Subsequently, the service worker can be used to cache the notes page. Then, the server worker will send us the URL so we can get the post UUID and admin’s username randomly generated.

    • Another solution: With the XSS in cdn.challenge-1122.intigriti.io, we could call window.open("https://api.challenge-1122.intigriti.io") and when the api.challenge-1122.intigriti.io loads, it will load all posts created by the user (admin bot is the user here β€” It creates a post with the flag in it) by framing the cdn.challenge-1122.intigriti.io/<username>-<uuid>.html.

      • Here, api.challenge is a child and cdn.challenge is the parent window. So, it is possible to read the frames src from cdn.challenge on api.challenge if the frame-src is the same origin as cdn.challenge, as shown in the following screenshot:

  • After leaking the iframe link (note URL β€” includes the username and the post UUID), we can find the username of the admin, we can create an account on staging domain with that username to get signed JWT for that username. Then it can be used on api.challange-1122.intigriti.io to the takeover admin account in the production. Once I done this, I found that flag was not in the post, but in admin’s profile picture. So, after taking over the account, I was able to see the profile picture:
🚩
INTIGRITI{workinghardorhardlyworking?}

More from this blog