Skip to content
imattas
Go back

ORDER ORDER

Edit page

Challenge Description

Can you try to get the flag from our website. I’ve prepared my queries everywhere! I think!

The developer believes they have parameterized all SQL queries, but missed one — the ORDER BY clause. Since ORDER BY takes a column name or expression (not a value), it cannot be parameterized using standard prepared statement placeholders. This is a common oversight even among security-aware developers.

Approach

Identifying the Vulnerability

When interacting with the web application, we notice a sorting feature — likely a table of items (e.g., metals, products, users) that can be sorted by clicking column headers or via a query parameter like ?order=name or ?sort=atomic_number.

Inspecting the requests reveals that the sort/order parameter is passed directly into the SQL query. While the developer correctly used parameterized queries for WHERE clauses and user input fields, the ORDER BY clause was left vulnerable because:

  1. ORDER BY cannot be parameterized in most SQL libraries — prepared statements only protect values, not identifiers (column names, table names, sort directions).
  2. The developer likely assumed all queries were safe since they used parameterized queries “everywhere.”

The Vulnerable Code Pattern

The backend likely looks something like this:

# Parameterized (safe) - WHERE clause
query = "SELECT * FROM items WHERE category = ?"
cursor.execute(query, (user_input,))

# NOT parameterized (vulnerable!) - ORDER BY clause
order = request.args.get('order', 'name')
query = f"SELECT * FROM items ORDER BY {order}"
cursor.execute(query)

Exploitation Strategy: Boolean-Based Blind SQL Injection via ORDER BY

Since ORDER BY doesn’t produce direct output, we use a blind SQL injection technique with CASE WHEN conditional expressions. The idea:

We use SQLite’s substr() function to extract the flag character-by-character:

CASE WHEN (SELECT substr(flag,1,1) FROM flag)='p' THEN name ELSE id END

Solution

Step 1: Confirm the Injection Point

Test with a valid column name vs. an invalid one:

Test with a CASE expression:

If the sort order changes, the injection is confirmed.

Step 2: Enumerate the Database

Discover table names (SQLite):

CASE WHEN (SELECT count(*) FROM sqlite_master WHERE type='table' AND name='flag')>0 THEN name ELSE id END

Step 3: Extract the Flag Character-by-Character

For each position i in the flag, iterate through possible characters and check:

CASE WHEN (SELECT substr(flag,{i},1) FROM flag)='{char}' THEN name ELSE id END

Observe the response sort order to determine if the character matches.

Instead of testing each character individually (up to ~95 printable ASCII characters), use binary search on the ASCII value:

CASE WHEN (SELECT unicode(substr(flag,{i},1)) FROM flag)>{mid} THEN name ELSE id END

This reduces each character extraction from ~95 requests to ~7 requests.

Solution Script

python3 solve.py

Flag

picoCTF{...}  (placeholder - actual flag varies per instance)

Edit page
Share this post on:

Previous Post
Old Sessions
Next Post
Password Profiler