MENAS'S SQLi Challenge
Nine SQL injection labs three easy, three difficult, three hard. Every challenge below is a real REST endpoint. Send requests with curl, Burp Suite, Postman, or code of your own, and read the flag straight out of the HTTP response once you've exploited the endpoint. Flags look like MENAS{...}.
A login form for the staff portal. There is exactly one account you're supposed to know about, and it isn't yours.
Objective
Log in as an account with the admin role without knowing a valid password.
Endpoint
POST/api/challenges/easy-01
Parameters
username, password
Example request
curl -X POST /api/challenges/easy-01 \
-H "Content-Type: application/json" \
-d '{"username": "test", "password": "test"}'Example response
{
"success": false,
"message": "Invalid username or password"
}Hints
Hint 1
What happens to the SQL query if your password field contains a quote character?
Hint 2
You don't need to know a real password if you can make the WHERE clause always true.
A storefront search box for a small catalog. Search returns matching products but the catalog has more rows than the storefront ever shows you.
Objective
Use the search box to reveal a hidden product outside the normal catalog.
Endpoint
GET/api/challenges/easy-02?q=
Parameters
q
Example request
curl "/api/challenges/easy-02?q=lamp"
Example response
{
"results": [
{ "id": 1, "name": "Desk Lamp", "price": 24.99 }
]
}Hints
Hint 1
The search term is dropped straight into a LIKE clause.
Hint 2
Closing the quote lets you add your own condition to the WHERE clause.
A public profile lookup by numeric ID. Only IDs 1 through 5 are advertised in the UI.
Objective
Retrieve a profile that isn't one of the five advertised IDs.
Endpoint
GET/api/challenges/easy-03?id=
Parameters
id
Example request
curl "/api/challenges/easy-03?id=1"
Example response
{
"results": [
{ "id": 1, "username": "alice", "bio": "Coffee enthusiast." }
]
}Hints
Hint 1
The id parameter is concatenated straight into the query, not bound as a parameter.
Hint 2
You're not limited to a single numeric value.
The same product search, this time backed by a second, unrelated table you were never given a link to. The frontend only ever shows product rows: id, name, price.
Objective
Use a UNION-based injection to pull data out of a table the product search was never meant to expose.
Endpoint
GET/api/challenges/difficult-01?q=
Parameters
q
Example request
curl "/api/challenges/difficult-01?q=lamp"
Example response
{
"results": [
{ "id": 1, "name": "Desk Lamp", "price": 24.99 }
]
}Hints
Hint 1
Work out the column count before anything else UNION requires a matching number of columns.
Hint 2
The product columns are (id, name, price). Can you SELECT three columns from somewhere else with matching types?
Hint 3
difficult01_secrets (id, note, value)
An account-existence checker. It never echoes any data back only { "exists": true } or { "exists": false }. Somewhere in that database is a row that was never meant to be found this way.
Objective
Use boolean-based blind injection to extract a hidden value one character at a time.
Endpoint
GET/api/challenges/difficult-02?id=
Parameters
id
Example request
curl "/api/challenges/difficult-02?id=1"
Example response
{
"exists": true
}Hints
Hint 1
You get exactly one bit of information per request: true or false.
Hint 2
You can inject a subquery condition alongside the id check with AND.
Customers check an order's status by supplying both an order ID and the email it was placed under. Both fields feed the same query.
Objective
Break out of one of the two parameters to view an order that isn't yours.
Endpoint
POST/api/challenges/difficult-03
Parameters
orderId, email
Example request
curl -X POST /api/challenges/difficult-03 \
-H "Content-Type: application/json" \
-d '{"orderId": "1001", "email": "customer@example.com"}'Example response
{
"success": false,
"message": "No matching order found"
}Hints
Hint 1
Both fields are concatenated into the same WHERE clause, joined with AND.
Hint 2
You only need to control one of the two fields to neutralize the other.
A username-exists check that always responds with the same generic message and the same JSON shape, regardless of whether the username is real. There is nothing to read in the response except how long it takes to arrive.
Objective
Use time-based blind injection to extract a hidden value using response timing alone.
Endpoint
GET/api/challenges/hard-01?user=
Parameters
user
Example request
curl "/api/challenges/hard-01?user=alice"
Example response
{
"message": "Request processed"
}Hints
Hint 1
There's no visible difference between a true and false condition in the response body.
Hint 2
SQLite has no SLEEP(); a heavyweight recursive query can be used to simulate a delay when a condition is true.
A two-step flow. Step one: register a display name (this write is done safely). Step two: view your public profile, which is rendered by looking your stored name back up in the database. The registration step is not the vulnerable one.
Objective
Register a display name that plants an injection payload, then trigger the profile lookup so the stored payload executes on step two.
Endpoint
POST/api/challenges/hard-02 (register), /api/challenges/hard-02/profile?id= (view)
Parameters
displayName
Example request
curl -X POST /api/challenges/hard-02 \
-H "Content-Type: application/json" \
-d '{"displayName": "alice"}'
# -> { "success": true, "id": 1 }
curl "/api/challenges/hard-02/profile?id=1"Example response
{
"results": [
{ "displayName": "alice" }
]
}Hints
Hint 1
The registration endpoint uses a parameterized query — it is not where the bug is.
Hint 2
The profile endpoint re-reads your stored name out of the database and drops it into a fresh query unsafely.
An employee directory filtered by department. A naive filter strips certain SQL keywords out of your input before running the query but it only strips them once, and it doesn't touch whitespace-free syntax.
Objective
Bypass the keyword filter to pull a row out of a table the directory search never queries directly.
Endpoint
GET/api/challenges/hard-03?dept=
Parameters
dept
Example request
curl "/api/challenges/hard-03?dept=engineering"
Example response
{
"results": [
{ "id": 1, "name": "Sam Rivera", "department": "engineering" }
]
}Hints
Hint 1
Send the word UNION and see what comes back out the other side.
Hint 2
A filter that does a single, non-recursive string replace can be defeated by nesting the banned word inside itself.