Apr. 24, 2019
This was my second year organizing ångstromCTF. Compared to last year, I wrote a lot more challenges and did a lot more work on the platform. Despite some site stability issues, we still ended up with over 1,300 scoring teams. Here are the challenges I wrote (this is going to be a long post):
- Aquarium, 50 points
- Pie Shop, 100 points
- Returns, 160 points
- Server, 180 points
- Weeb Hunting, 180 points
- TI-1337, 250 points
- Bugger, 200 points
- Control You, 20 points
- DOM Validator, 130 points
- NaaS, 140 points
- GiantURL, 190 points
This challenge is a relatively basic buffer overflow. You have a win function, so you use the unbounded input in
gets to overflow the buffer until the return address is overwritten with the address of the win function.
Plenty of tutorials online (and hopefully community created writeups for this challenge) will go into more detail about how to do this.
This is a partial overwrite challenge. You are unable to control the null byte at the end of your input and you have 4 bits that are random in the bottom 2 bytes, so you have to overwrite the lower 3 bytes of the address and just keep trying until you get lucky and return to the win function.
This is a format string challenge.
The first step is getting main to loop. The last printf has been changed to puts due to compiler optimizations or something, so the GOT of puts can be overwritten with the address of main and the function will loop. This can be done in a way similar to how this article describes it, although note that since this is 64-bit and the addresses have null bytes, the addresses must go after your format string.
Next you have to leak a libc addresses - this can be done by popping addresses off the stack (with
%p) until you get to
__libc_start_main_ret. From this and the libc provided, you can calculate the base address and thus the address of any function in libc.
After this, one last write is required to change strcmp to system, and then
/bin/sh can be entered as the item and you have shell.
In this challenge you were given a web server written in assembly. After disassembling the binary, you could see there were several syscalls which allowed the program to listen on port 19303 and fork a new process to serve each connection. You could also see there was a buffer overflow when reading in the path, since it just just kept reading until a space.
With this buffer overflow you could modify a syscall and ultimately get RCE.
This was a heap challenge, and I believe there were multiple ways to solve it. Below I’ll describe my solution.
You could get a double free by just using an item twice - the free’d pointer was not cleared, so the check to see if it was an empty slot failed. With this you could create a loop with the fastbins and allocate something that was also on the fastbin list. The
fd of this fastbin could be modified to point to a fake fastbin in
.bss and that could then be allocated and modified to overwrite a weapon pointer to an address on the global offset table, leaking a libc address when weapon names were printed.
The same attack with a double free could then be used to overwrite
__malloc_hook to the win function and get shell.
This challenge gave a highly restrictive Python
exec sandbox (no parentheses, no hashtags, no brackets, no imports, etc.). However, it did allow colons and the
@ symbol, so classes could be decorated and lambda functions could be made. Using this, you could open the flag file and read it:
x = 111, 112, 101, 110, 40, 39, 102, 108, 97, 103, 46, 116, 120, 116, 39, 41, 46, 114, 101, 97, 100, 40, 41 y = lambda z: x @print @eval @bytes @y class z: pass
The binary was packed with UPX (findable with
strings), but the packer said it could not unpack it. This was because the
UPX! header was replaced with null bytes, so it had to be added back in. There was also a ptrace antidebugging mechanism. Since it made syscalls directly, you couldn’t LD_PRELOAD a custom ptrace function. It also made two calls to make sure it could ptrace successfully once, but not twice. The easiest way to bypass this was to catch the syscall in GDB and modify the return value. The binary then performed some weird calculations (modified SHA512 with some random stuff) to get the flag. The values could be pulled from within GDB with a breakpoint set in the proper place.
For this challenge you just had to read source (keyboard shortcut: Control-U) and see what it was comparing your entered flag with.
This challenge had tons of unintended solutions - I’m sure people will make writeups for those. My intended solution was much simpler. Just change the URL from
https://dom.2019.chall.actf.co/posts//asdfasdfsadf.html and the relative source for DOMValidator.js no longer loads (404). This behavior is due to how express’s static file serving works (double slashes are collapsed).
This XSS is then used to steal the admin’s cookie, which has the flag.
This challenge required breaking Python’s random number generator to predict nonces.
My solve script (using randcrack):
from randcrack import RandCrack rc = RandCrack() import binascii import base64 import requests requests.get('https://naas.2019.chall.actf.co/status') noncehtml = "<script></script>"*156 nonces = requests.post('https://naas.2019.chall.actf.co/nonceify', data=noncehtml).json()["csp"].strip("script-src 'nonce-").strip(";").split("' 'nonce-") bits =  for nonce in nonces: h = binascii.hexlify(base64.b64decode(nonce)) for i in range(0, len(h), 8): bits.append(int(h[i:i+8], 16)) for i in range(0, len(bits), 4): bits[i], bits[i+1], bits[i+2], bits[i+3] = bits[i+3], bits[i+2], bits[i+1], bits[i] for b in bits: rc.submit(b) print(str(base64.b64encode(binascii.unhexlify(hex(rc.predict_getrandbits(128))[2:].zfill(32))), encoding="ascii")) print(str(base64.b64encode(binascii.unhexlify(hex(rc.predict_getrandbits(128))[2:].zfill(32))), encoding="ascii"))
This challenge gave a “URL lengthener” that also had a report link, where the admin would visit the lengthened URL and click on the link to follow the redirect.
You needed to change an admin’s password through a POST request to
/admin/changepass. At first it looked like this could be done just with CSRF, but that wouldn’t work because server set cookies to be
SameSite: Lax and the cookie was not sent with cross origin POST requests.
Instead, you had to use the ping attribute on the link you sent (since the href attribute wasn’t quoted you could break out of it with a space) and set it to
/admin/changepass?password=<some valid password>. Since the PHP used
$_REQUEST both GET and POST parameters were used to get the sent password.
After the admin clicked on the link the admin password would be changed and you could log in and get the flag.