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(the0aat the end is the newline that is appended when we press Enter). -
This results in the updated
contentvalue being double the input size (plus 2 bytes for the newline). - The
contentproperty of thepastestruct 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
192bytes 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
contentproperty 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_useris also a global object.struct paste paste; struct user current_user; - Both objects reside in the
.bsssection of memory, which makes sense as they are uninitialized.
-
Their addresses are
0x320bytes apart, meaning they are stored sequentially in memory. -
By overflowing the
contentproperty of thepastestruct, we can overwrite theflagsproperty in thecurrent_userstruct, 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_useraddress:0x55555567ce20(BASE_ADDRESS + 0x128e20). -
flagsproperty address:0x55555567ce28(after 4 bytes for theuserIdplus 4 bytes for stack alignment).
-
-
Since I was logged in anonymously with limited permissions, the
flagsproperty of mycurrent_userstruct had the value0x000000000000001e(0..00011110in binary), indicating create, update, view and list permissions. -
The
permission_rootvalue is defined as1<<8(100000000in binary). To gain root permissions, theflagsproperty 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
pastestruct was found at address0x55555567cb00, with itscontentproperty located at0x55555567cd04. The distance from thecontentproperty to the target byte we need to modify is0x55555567ce29 - 0x55555567cd04 = 293bytes. Since we are using the hex modifier, which doubles the size of the input, our input needs to be half of that. -
With a
146bytes input we can fill the buffer forcontentand start writing on theuserstruct up until theflagproperty. Then with a 147th character we can override theflagsvalue. - 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
pastestruct to be updated and overflowing into thecurrent_userstruct. - The
flagsproperty was successfuly modified to0x0000000041303133. This value includes thepermission_rootbit, granting root permissions.
- Finally I was able to execute option
7and revealed the flag.