Penetration Testing

SQL Injection Testing Guide: A Practical sqlmap Workflow

Photo: Wendelin Jacober (CC0 1.0)
In this article7 sections

What it is and the problem it solves

SQL injection testing confirms that input you control actually changes the structure of a SQL query the application sends to its database, and then proves what that change gives you. It answers two questions scanners answer badly: is this parameter genuinely vulnerable, and what can an attacker reach through it?

The problem it solves is triage. Static analysis and crawlers produce long lists of possible injection points, most of them unreachable or already handled by the query builder. A confirmed injection with a reproducible payload, a named parameter, and demonstrated impact gets fixed first, and it hands the developer the exact query to correct.

This guide uses sqlmap, the long-running open-source detection and exploitation tool, with manual confirmation first and automation second. The goal is proof, not damage: identify the parameter, demonstrate the impact, document it, stop.

Version note: this reflects the sqlmap 1.10 stable release line. Confirm your build with sqlmap --version and skim the project changelog before relying on a newer switch, because defaults and output formatting shift between releases.

Prerequisites

  • Written authorization. A scope document or rules of engagement naming in-scope hosts, the test window, permitted techniques, prohibited actions such as shell access and file writes, and who can approve changes mid-test.
  • A practice target you own. DVWA, OWASP Juice Shop, or bWAPP in an isolated lab. For real work, a staging copy with production-like configuration and data shapes.
  • A current sqlmap build. Python 3 plus the tool from the official repository, or a distribution such as Kali Linux that packages it.
  • Basic SQL literacy. You need to read a WHERE clause and understand UNION well enough to judge whether a result is a real oracle or an application coincidence.
  • A request-capturing proxy for targets that use JSON bodies, custom headers, or multi-step authentication you must replay exactly.
  • Rate limits agreed with the target owner, and a monitoring window, so their security team does not treat your traffic as an intrusion.

Scope and authorization: every technique below applies only to systems you own or have explicit, current written permission to test. Permission must name the hosts, the window, and the techniques allowed, and must come from someone authorized to grant it. Testing systems you do not own is a criminal offense in most jurisdictions, intent notwithstanding. If a target’s scope status is unclear, stop and get written confirmation before sending a single probe. A saved email is evidence; a verbal maybe is not.

Setup

Clone the tool and check what you are running:

git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git
cd sqlmap
python3 sqlmap.py --version

Stand up an intentionally vulnerable lab target, bound to the loopback interface only:

docker run --rm -it -p 127.0.0.1:8080:80 vulnerables/web-dvwa

Browse to http://127.0.0.1:8080, log in with the default credentials the image documents, click Create / Reset Database, and set the security level to low. Open the SQL Injection page, submit a normal value, and copy the session cookie from your browser or proxy so sqlmap can work as an authenticated user.

Lock in two habits early. Point --output-dir at an engagement folder so session files, logs, and dumps stay together and can be deleted at project close. And remember that sqlmap caches results in a per-target session file, so re-testing after a fix needs --flush-session or the tool replays the old answer.

When working from a proxy log, --scope filters parsed targets by regular expression; pair it with -l so a stray host cannot drag the test out of scope.

Core workflow

  1. Map candidate inputs. List every value that reaches a query: query-string parameters, POST fields, cookies, headers used in filters, and sort or pagination arguments. Sort and filter parameters are the ones testers forget most often.
  2. Confirm by hand first. Send a baseline, a single quote, then a boolean pair. A syntax error on the quote, identical output for a true condition, and different output for a false one is persuasive on its own.
  3. Replay the real request. Capture it from your proxy, save it as req.txt, and run sqlmap with -r req.txt. This preserves cookies, tokens, and headers and is far more reliable than rebuilding the request inline.
  4. Tune detection rather than forcing it. Use --level and --risk to widen coverage, --technique to restrict which classes are attempted, and --dbms once you know the back end. Lower --threads or add --delay on fragile targets.
  5. Enumerate least-first. Databases, then tables, then columns, then one or two rows as proof. Do not dump tables you do not need.
  6. Record evidence. Keep the exact request, the payload, the raw output, and a timestamp, and note whether the endpoint needs authentication and which database account the application uses.
  7. Re-test after remediation. Run the same command again with --flush-session. A clean run against the fixed build is the evidence that the fix holds.

Worked example. Assume the DVWA container is running, security is set to low, and you hold a valid session cookie. The SQL Injection page takes an id parameter. Confirm manually first by pasting these values into that parameter in your proxy’s repeater, which handles URL encoding for you:

1                # baseline, returns a row
1'               # syntax probe, expect a database error
1' AND '1'='1    # true condition, expect baseline row
1' AND '1'='2    # false condition, expect no row

Then confirm and enumerate with the tool:

python3 sqlmap.py -u 'http://127.0.0.1:8080/vulnerabilities/sqli/?id=1&Submit=Submit' \
  --cookie='PHPSESSID=abc123; security=low' -p id \
  --batch --level=2 --risk=1 --output-dir=~/engagements/lab/sqli

python3 sqlmap.py -r dvwa-sqli.req -p id --batch --dbs
python3 sqlmap.py -r dvwa-sqli.req -p id -D dvwa -T users --columns
python3 sqlmap.py -r dvwa-sqli.req -p id -D dvwa -T users -C user,password --dump

sqlmap reports the injection class, the back-end DBMS, and the payload behind each check. Now fix the lab query: replace string concatenation with a bound parameter, confirm the page still works, then re-run the first command with --flush-session.

The defender’s view of the same workflow

Every step above has a mirror image in engineering and detection.

  • Parameterize the query, not the input. Prepared statements with bound parameters are the fix. Escaping quotes and blacklisting keywords is not; encoding variants defeat both.
  • Identifiers cannot be bound. Table names, column names, and sort directions must be validated against an allow-list, because parameters only carry values.
  • Enforce least privilege on the database account. An application user that cannot read system tables or write files sharply limits what an injection achieves.
  • Stop leaking query detail. Generic error pages, no stack traces, no SQLSTATE strings echoed to the browser.
  • Detect the pattern, not just the payload. Alert on bursts of syntax errors from one session, repeated boolean-shaped parameters, UNION or SLEEP strings in logs, and clusters of 500 responses. A web application firewall is a speed bump, not a control to rely on.
  • Verify the fix rather than assume it. Replay the original payload after the change, and add a regression test asserting that endpoint uses the query builder.

Reading the results

The confirmation block near the top of the output is the part that matters: target, parameter, injection type, and back-end DBMS. Each injection type is a different strength of evidence.

  • Error-based: a database error reaches the response. Usually the most straightforward confirmation.
  • UNION query: the tool merged its own SELECT into the original statement. The strongest proof that arbitrary data can be read.
  • Boolean-based blind: true and false conditions produce different response content or length. Solid when the difference is stable.
  • Time-based blind: the response is delayed. Weaker, because network jitter, load balancers, and caching can mimic the signal.
  • Stacked queries: multiple statements execute in one call where the driver permits it. High impact, high risk of side effects.

Before writing a finding, reproduce the oracle by hand. For time-based results, set --time-sec to an obviously unnatural value and confirm the delay tracks it. Distrust results where the response is a block page from an edge device, and check whether the application returns a generic error for every malformed request, which can look like a syntax error and is not one.

Record what the result does not tell you: whether the endpoint is reachable without authentication, and what privileges the application’s database account holds.

Limitations

sqlmap finds what a single request can reach. That leaves real gaps.

  • Second-order injection. A value stored safely on one endpoint and concatenated into a query later by a batch job, report, or admin export will not appear in a scan of the endpoint that accepted it. Find these by reading code and tracing where data is stored and reused.
  • Non-classic injection points. JSON and XML bodies, GraphQL variables, HTTP headers, mobile APIs, and stored procedures need deliberate coverage that a crawler will miss.
  • Destructive options. --risk=3 enables payloads that carry a real risk of altering or deleting data, --dump against a large table can hammer a database, and shell or file-write options cross from reading to writing. None of that belongs in a test without explicit permission.
  • Test-data pollution. Some techniques insert rows or leave residue. On a system of record, that is a change to production data, not a harmless probe.
  • Edge filtering produces false negatives. A clean scan can simply mean every payload was blocked. Absence of a finding is not proof of absence, and the report should say so.

The limitation experienced testers raise first is privilege mismatch between lab and production. A finding proved where the application connects as database owner says nothing about the same bug in production, where the account may hold SELECT on three tables and nothing else. The vulnerability is real in both places, but the impact is not, and reporting the lab’s blast radius is how a valid finding gets argued out of the remediation queue. The reverse matters too: a locked-down production account does not help if a second, higher-privileged connection exists elsewhere in the codebase.

Tool output is a starting point for a code review, not a replacement for one. The most valuable line in the finished report is usually the query you found in the source, not the payload that proved it.

Nathan Cole

Vulnerability management research, Dominion Cyber

Nathan Cole writes about vulnerability management and emerging threat research — why CVSS score alone is a poor prioritisation input, how patch operations actually get run, the mechanics of adversary-in-the-middle phishing, and the security model of AI assistants and browser extensions.

Get the weekly security brief

One email a week: what is worth patching, what is worth watching, and what is worth reading. No spam, unsubscribe any time.