Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.jaspervanzeir.be/llms.txt

Use this file to discover all available pages before exploring further.

Hint: SQL Injection
The hint for this lab explicitly mentioned SQL Injection. As always, my first step was to fire up Burp Suite to inspect the requests in the HTTP history. When I loaded the homepage, I noticed the application was a sort of Instagram clone for otters called “Ottergram.” I immediately spotted a user named admin. In CTF challenges, if there is an admin user, the flag is almost always their password. My goal was set: use SQL injection to retrieve the admin’s password. I clicked on the admin’s profile to see how the application fetched user data.

Enumeration & SQLi Confirmation

I found the GET /api/profile/admin request in Burp’s HTTP history and sent it to Repeater to test for vulnerabilities in the URL path.
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2fzarx Nw Meflc7xrcmb3xu%2fimage
To confirm if the endpoint was vulnerable to SQL injection, I used a classic boolean testing method. First, I appended a true statement to the URL:
GET /api/profile/admin' and 1=1 -- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2f Sd Qhp Km R Wt1i Cy0mq G4k%2fimage
This returned a 200 OK response. Next, I tested a false statement to see if the database logic would change the application’s response:
GET /api/profile/admin' and 1=2 -- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2fjxv Ri0v1j Viu3o Fxi Jbj%2fimage
When this returned an error (or empty result), it confirmed my suspicion: the input was being directly evaluated by the database, meaning SQL injection was possible.

Exploitation

Determining the Number of Columns

With SQLi confirmed, my next step was to figure out how many columns the original SQL query was returning. To do this, I used a UNION SELECT attack. The goal is to incrementally add numbers until the request succeeds without a column mismatch error.
Side note: Another way to do this is using ORDER BY (e.g., ORDER BY 1, ORDER BY 2), increasing the number until you hit an error, which means the previous number was the maximum column count. However, for the sake of this writeup and my own flow, I stuck with the UNION SELECT method.
I kept incrementing until I got a working, successful response with 7 columns:
GET /api/profile/admin' union select 1,2,3,4,5,6,7 -- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2f Hgleiwk HN De4cu T0rs Ha%2fimage

Identifying the DBMS and Version

Now that I knew there were 7 columns, I needed to identify which Database Management System (DBMS) was running and its version. The syntax for this differs slightly between systems. I initially assumed it was MySQL, so I replaced the 7th column with version():
GET /api/profile/admin' union select 1,2,3,4,5,6,version() -- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2fu R Yn3h Zgd7qrx IBZFK Ah%2fimage
This failed and didn’t give me the result I wanted. Since CTFs frequently use lightweight databases, my next guess was SQLite. I changed the payload to use sqlite_version():
GET /api/profile/admin' union select 1,2,3,4,5,6,sqlite_version() -- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2f7ksn KTF Tm Ia5anv5e Bwo%2fimage
Success! The application rendered the version number 3.44.2, confirming that the backend was running SQLite.

Extracting the Database Schema

The next step was to map out the database to find where user credentials were stored. I needed to list all the tables. Since I wasn’t entirely familiar with SQLite’s specific syntax for this, I used a Datacamp tutorial on SQLite commands as a reference. The tutorial noted that you can query the sqlite_master table using SELECT name FROM sqlite_master WHERE type='table';. Because I wanted to output all the table names at once in my injection point, I wrapped the column name in a group_concat() function:
GET /api/profile/admin' union select 1,2,3,4,5,6,group_concat(tbl_name) from sqlite_master where type = 'table'-- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2fic ST Ql K1ad Xt Ykfqct Kq%2fimage
The response revealed all the tables in the database, and the users table immediately stood out.

Extracting the Flag

To get the admin’s password, I first needed to know the exact column names within the users table. In SQLite, you can retrieve column information using pragma_table_info():
GET /api/profile/admin' union select 1,2,3,4,5,6,group_concat(name) FROM pragma_table_info('users')-- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2f0i V Ucz Hm02da Us Ou Bh5s%2fimage
This returned a list of columns, and both username and password were clearly visible. Knowing the table name (users), the column names (username, password), and the target user (admin), I crafted my final payload to extract the credentials, placing the username and password in the 6th and 7th column slots so they would render on the page:
GET /api/profile/admin' union select 1,2,3,4,5,username,password from users where username = 'admin'-- -
Https Files Gitbook Com V0 B Gitbook X Prod Appspot Com O Spaces%2fwf Uhrqei Mz Jnppiwss2r%2fuploads%2fa Xgs6wrt Ep Hjy0ycq9zo%2fimage
Once I sent this request, the application spit out the admin’s password. Exactly as I had hoped, it wasn’t hashed, it was the flag in plain text!

Tools Used

  • Burp Suite (Community Edition): Proxy, HTTP History, and Repeater for finding the injection point and iterating payloads.
  • Online Documentation: Datacamp tutorial for SQLite-specific syntax.

Summary

  • Key Steps: I identified an SQL injection point in the URL path, determined the column count via UNION SELECT, identified the database as SQLite, extracted the table schema from sqlite_master, and finally dumped the admin credentials from the users table.
  • What I Learned: I reinforced my knowledge of SQLite-specific payloads. Specifically, using sqlite_version(), querying sqlite_master for table names, and using pragma_table_info() to extract column names.
  • Crucial Mistakes/Takeaways: My initial assumption that the database was MySQL cost me a few minutes. Getting an error on version() was a great reminder not to assume the backend technology and to systematically test for different DBMS fingerprints (like transitioning to sqlite_version()) when standard payloads fail.