SQLate
From Iris CTF 2025. You can download all challenge files from here and run the application locally.
Application Overview
-
The provided code represents a Pastebin-like application with several functionalities for creating, updating, viewing, and listing “pastes” stored in an SQLite database.
- The application also features a user system, with distinct permissions for performing actions such as creating, updating, or viewing pastes.
enum user_flags { permission_create = 1<<1, permission_update = 1<<2, permission_view = 1<<3, permission_list = 1<<4, permission_login = 1<<5, permission_register = 1<<6, permission_root = 1<<8, };
- By default, users can log in anonymously with limited permissions.
void login_anonymous() { current_user.userId = -1; current_user.flags = permission_create | permission_update | permission_view | permission_list; strcpy(current_user.username, "anonymous"); }
Root permissions give the flag
- The menu includes six options:
printf( "\n===== SQLate =====\n" "1) Create new Paste\n" "2) Update a Paste\n" "3) Show a Paste\n" "4) List all Pastes\n" "5) Login / Register\n" "6) Exit\n" "\n" "> "
- However, there is a hidden 7th option that checks if the user has
permission_root
. If true, it calls theactions_sys()
function to retrieve the flag.... case '7': { if (!check_permissions(permission_root)) continue; action_sys(); continue; } ... ... void action_sys() { system("/usr/bin/cat flag"); }
Update functionality
- When updating a paste, we can choose which field to update and apply a modifier:
void action_update() { sqlite3_stmt *stmt; printf( "Which field?\n" "1) Language\n" "2) Content\n" "\n" ">" ); int c = getc(stdin); getc(stdin); if (c != '1' && c != '2') return; const char* field = c == '1' ? "language" : "content"; if (c == '2') { printf( "Which modifier?\n" "1) None\n" "2) Hex\n" "3) Base64\n" "\n" ">" ); ...
-
Using the hex modifier for updating
content
, the input string is converted into its hexadecimal ASCII representation. -
For example, the input
test
, becomes746573740a
(the0a
at the end is the newline that is appended when we press Enter). -
This results in the updated
content
value being double the input size (plus 2 bytes for the newline). - The
content
property of thepaste
struct is 256 bytes long.struct paste { int rowId; char title[256]; char language[256]; char content[256]; };
-
A buffer overflow occurs if we supply a 128-byte input with the hex modifier, as the resulting 258-byte (2*128 + 2) output exceeds the buffer size.
- The app implements an overflow detection check, but it is flawed, since it only denies inputs with over
192
bytes in length.... else if (c == '2') { if (strlen(line_buffer) > 192) err(EXIT_FAILURE, "Attempted to overflow!"); } ... ...
- Therefore, there is definitely a buffer overflow vulnerability when updating the
content
property of a paste. But can we somehow exploit that?
Exploiting the vulnerability
-
Whenever we view a paste, the app queries the database, updates a global paste struct and passes it to the
print_paste(struct paste *paste)
function. - Our user struct object, named
current_user
is also a global object.struct paste paste; struct user current_user;
- Both objects reside in the
.bss
section of memory, which makes sense as they are uninitialized.

-
Their addresses are
0x320
bytes apart, meaning they are stored sequentially in memory. -
By overflowing the
content
property of thepaste
struct, we can overwrite theflags
property in thecurrent_user
struct, potentially granting root permissions:
struct paste {
int rowId;
char title[256];
char language[256];
char content[256];
};
struct user {
int userId;
uint64_t flags;
char username[256];
char password[256];
};
struct paste paste;
struct user current_user;
- I ran the app through gdb and determined the exact addresses of the structs:
-
current_user
address:0x55555567ce20
(BASE_ADDRESS + 0x128e20
). -
flags
property address:0x55555567ce28
(after 4 bytes for theuserId
plus 4 bytes for stack alignment).
-

-
Since I was logged in anonymously with limited permissions, the
flags
property of mycurrent_user
struct had the value0x000000000000001e
(0..00011110
in binary), indicating create, update, view and list permissions. -
The
permission_root
value is defined as1<<8
(100000000
in binary). To gain root permissions, theflags
property must be updated to a value like0x000000000000011e
(Basically the 9th least significant bit must be1
). Our target byte for modification is located at address0x55555567ce29
. -
The
paste
struct was found at address0x55555567cb00
, with itscontent
property located at0x55555567cd04
. The distance from thecontent
property to the target byte we need to modify is0x55555567ce29 - 0x55555567cd04 = 293
bytes. Since we are using the hex modifier, which doubles the size of the input, our input needs to be half of that. -
With a
146
bytes input we can fill the buffer forcontent
and start writing on theuser
struct up until theflag
property. Then with a 147th character we can override theflags
value. - I generated the following payload using
python3 -c "print(146 * 'A' + '1')"
:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1
-
I updated a paste using the hex modifier and the payload above. Then, by listing the pastes, I triggered the exploit, causing the global
paste
struct to be updated and overflowing into thecurrent_user
struct. - The
flags
property was successfuly modified to0x0000000041303133
. This value includes thepermission_root
bit, granting root permissions.

- Finally I was able to execute option
7
and revealed the flag.
