SQL Injection

Make a database run your input as code. Log in without a password, read tables you were never shown, and learn why every fix except one is a patch over the real problem.

Easy · 5 challenges · ~120 min · 0/5 solved
Log in to start the challenges in this room.

What SQL injection actually is

An application asks a database questions in SQL. When it builds those questions by gluing your input into a string, the database has no way to tell where the developer's sentence ends and yours begins. Give it input that reads as SQL, and it runs as SQL.

That is the whole idea. Everything below is a consequence of it.

Take a login that looks up the user you typed:

query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"

Type admin and hunter2 and the database sees exactly what the developer intended:

SELECT * FROM users WHERE username = 'admin' AND password = 'hunter2'

Now type ' OR '1'='1 as the password instead. The database sees:

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'

The password check is still there. It just no longer matters, because OR '1'='1' is true for every row. You did not guess the password. You changed the question.

The bug is never "the password check was weak". The bug is that your input stopped being data and became part of the program.
Your turn

Injection

The classic. One login form, no account, and a query built by pasting your input into it.

easy · 50 pts

Log in to start this challenge.

The first one is exactly the example above: a login form, no account, and a query built by pasting your input into it. Get in as admin.

Finding it

Look for any place your input could reach a query: login forms, search boxes, an id in the URL, a sort or filter dropdown, a cookie, an HTTP header the app logs to a database.

The first probe is a single quote:

'

If the page returns a database error, a 500, or a suddenly empty result where it used to show data, you have almost certainly broken the query's syntax — which means you were inside it. Then confirm by fixing what you broke:

  • ' breaks it
  • '' puts it back
  • ' OR '1'='1 breaks it in a way that changes the answer

That last step matters. An error proves you can reach the parser. Changing the result proves you can control the logic.

Your turn

Beacon Auth Gateway

The same idea against a different app, so the pattern sticks rather than the payload.

easy · 50 pts

Log in to start this challenge.

Same bug, different application. Do it again without looking back at the first one — you are learning a shape, not a payload.

Payload shapes worth knowing

Authentication bypass. Make the WHERE clause true regardless:

' OR 1=1 --

The -- starts a SQL comment, throwing away the rest of the developer's query — including the closing quote you would otherwise have to balance. In MySQL, use -- with a trailing space, or #.

Reading other tables with UNION. If the app prints query results back to you, UNION SELECT appends a second result set to the first:

' UNION SELECT username, password FROM users --

Two rules govern this: the column count must match, and the types must be compatible. Find the count by incrementing until the error stops:

' ORDER BY 1 --
' ORDER BY 2 --
' ORDER BY 3 --     <-- errors: there are only 2 columns

Finding the table names. You rarely get told the schema. Ask for it:

-- SQLite
' UNION SELECT name, sql FROM sqlite_master WHERE type='table' --

-- MySQL / PostgreSQL
' UNION SELECT table_name, column_name FROM information_schema.columns --
Your turn

Cakectf Country Db

Reading data the app never meant to show you: UNION SELECT and finding the flag's table.

medium · 100 pts

Log in to start this challenge.

A lookup that prints its results back to you, which is exactly what UNION needs. Find the column count first, then go looking for the flag's table.

Blind injection. Sometimes nothing is printed — the page only differs between "worked" and "did not". Then you ask yes/no questions one bit at a time:

' AND (SELECT substr(flag, 1, 1) FROM flags) = 'o' --

True renders the normal page; false renders the error page. Slow, entirely mechanical, and the reason people script it.

What to watch out for

A filter is not a fix. Blocklists fail because SQL has more ways to say a thing than anyone can enumerate:

  • ' stripped? The injection point may be numeric, where you need no quotes at all.
  • OR blocked? || means OR in SQLite and Oracle.
  • Spaces blocked? /**/, %09, or newlines work as separators.
  • Keyword filtered once, non-recursively? SELSELECTECT survives the removal of the inner SELECT.
  • Case-sensitive filter? SeLeCt.

Escaping is fragile. Manually escaping quotes fails the moment the value is not quoted in the first place — the numeric id case — or when a multi-byte charset lets an attacker consume the escape character.

The injection point is often not the text box. Column names, table names, and ORDER BY direction cannot be parameterised, so developers frequently paste them in. ORDER BY injection is common precisely because the "obvious" fix does not apply there. Look at every sort dropdown.

Errors get hidden, not fixed. A generic 500 page does not mean the injection went away. It means you now have to work blind.

Your turn

Bsidesnoida Baby Web

Now the injection point is a URL parameter, and a filter sits in front trying to stop you.

easy · 50 pts

Log in to start this challenge.

A numeric id in the URL with a filter sitting in front of it. Both halves of this section matter here.

The actual fix

Use parameterised queries, always. The value travels to the database separately from the query text, so it can never be parsed as SQL:

cursor.execute(
    "SELECT * FROM users WHERE username = ? AND password = ?",
    (username, password),
)

That single change kills every payload above. It is not a filter and there is nothing to bypass, because the parser is never shown your input as code.

For the parts that genuinely cannot be parameterised — table names, column names, ORDER BY direction — use an allowlist and map user input onto a fixed set of known-good strings. Never pass it through.

Then, because defence in depth is not optional: give the application's database user the narrowest rights it can do its job with. A read-only account cannot be made to write a webshell no matter how good the injection is.


One last one, and it is the reason the previous section spends so long on injection points that are not text boxes.

Your turn

Naham Con Flaskmetal Alchemist

Capstone. The injection is not in the search text but in the sort order, where quotes cannot help you.

medium · 100 pts

Log in to start this challenge.

{# A reading room (no challenges) shows only its prose - no empty practice note, which would read as a misconfiguration rather than an intentional choice. #}