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 the actions_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, becomes 746573740a (the 0a 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 the paste 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 the paste struct, we can overwrite the flags property in the current_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 the userId plus 4 bytes for stack alignment).
  • Since I was logged in anonymously with limited permissions, the flags property of my current_user struct had the value 0x000000000000001e (0..00011110 in binary), indicating create, update, view and list permissions.

  • The permission_root value is defined as 1<<8 (100000000 in binary). To gain root permissions, the flags property must be updated to a value like 0x000000000000011e (Basically the 9th least significant bit must be 1). Our target byte for modification is located at address 0x55555567ce29.

  • The paste struct was found at address 0x55555567cb00, with its content property located at 0x55555567cd04. The distance from the content property to the target byte we need to modify is 0x55555567ce29 - 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 for content and start writing on the user struct up until the flag property. Then with a 147th character we can override the flags 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 the current_user struct.

  • The flags property was successfuly modified to 0x0000000041303133. This value includes the permission_root bit, granting root permissions.
  • Finally I was able to execute option 7 and revealed the flag.