---
name: exploit-workflow
description: "Exploit agent workflow — intake recon handoff, route to tag-specific sub-skill, run confirm-gate before iteration, verify with reproductions. Includes silent-stall recovery and three-strike rule."
metadata:
  when_to_use: "exploit, exploitation, exploit workflow, sub-skill routing, confirm-desync gate, verification, silent-stall, wedged shell, three-strike"
  subdomain: workflow
---

# Exploit Workflow

## Role

Take recon's structured handoff, identify the right exploitation technique, run a confirm-gate where applicable, execute, and prove victim-observable impact. Exploit owns iteration, payload work, and reproduction — recon does not.

## The Loop

### Phase 1 — Intake

1. Read `SUMMARY.md` from recon. Verify it has the sections required by the tag set:
   - Always: `Confirmed sinks`, `Required session state`, `Sink preconditions`, `Tried, ruled out`, `Open hypotheses`.
   - If `Tags` includes `race_condition`/`toctou`/`concurrent`: `Session-write timeline` MUST be populated.
   - If `Tags` includes `smuggling_desync`/`request_smuggling`/`hrs`/`desync`: `Frontend stack` MUST be populated, with `Confirmed desync: YES|NO`.
2. If a required section is empty, flag back to recon as "recon incomplete: <section> missing" — do NOT iterate against an unsupported handoff.
3. Verify the recon-provided session state by replaying the auth flow and capturing the cookie jar. If cookies don't reproduce, exploit MUST re-run cookie enumeration before declaring any sink unreachable.

### Brute-force vector discipline

Default-credential challenges deliver via the early entries of `top1k.txt`. If those entries fail, the challenge intent is not "brute the admin password" — pivot to the next attack vector in your routing table rather than expanding the wordlist or extending the run. For `smuggling_desync` + `default_credentials`, that pivot vector is response-queue-poisoning per smuggling.md. Run brute-force in a named background session (`bash(command="...", background=True, session="brute")`) and `bash_kill(session="brute")` once the early-entry hypothesis has been falsified, so the main session stays free for the pivot.

### Phase 1.5 — Discovery Gate (MANDATORY for web targets)

Before routing to ANY web sub-skill, confirm at minimum:

1. Site root fetched: `curl -sL https://<TARGET>/ -o /tmp/root.html` and inspected for forms, JS src, links.
2. Common discovery endpoints checked (single shell loop):
   ```bash
   for p in robots.txt sitemap.xml swagger.json openapi.json api/docs api/v1 api graphql .git/config; do
     code=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 5 "https://<TARGET>/$p")
     [ "$code" != "404" ] && echo "$code  /$p"
   done
   ```
3. If recon SUMMARY.md is missing the endpoint inventory and the loop above returned no useful hits, run ONE gobuster/ffuf pass with `common.txt` and a bounded `--max-time` (so the discovery does not block) before any sub-skill iteration.
4. If a JWT is present, decode it (`echo "$JWT" | cut -d. -f2 | base64 -d`) and record claims to a scratch file BEFORE attempting IDOR enumeration.

5. **Input Surface Inventory** (MANDATORY after endpoints are known):

   For each discovered page/endpoint, enumerate input parameters:
   ```bash
   # GET parameters from HTML links
   curl -sL https://<TARGET>/ | grep -oE '[?&][a-zA-Z_]+=[^"&> ]*' | sort -u > exploit/inputs_get.txt

   # GET parameters from JS source
   curl -sL https://<TARGET>/ | grep -oE 'src="[^"]*\.js"' | cut -d'"' -f2 | while read js; do
       [[ "$js" == http* ]] || js="https://<TARGET>$js"
       curl -s "$js" | grep -oE '["\047](/[^"\047 ]+\?[a-zA-Z_]+=[^"\047 ]+)["\047]'
   done >> exploit/inputs_get.txt

   # POST form fields
   curl -sL https://<TARGET>/ | grep -oE '<input[^>]+name="[^"]+"' | grep -oE 'name="[^"]+"' | sort -u > exploit/inputs_post.txt

   # All form actions (where to POST)
   curl -sL https://<TARGET>/ | grep -oE '<form[^>]+action="[^"]+"' | grep -oE 'action="[^"]+"' | sort -u > exploit/forms.txt
   ```

   **Test order rule**: Test injection on EVERY discovered parameter ONCE (a single `?param=test%27` probe with a bounded HTTP timeout) before iterating deeply on any single parameter. The wrong-surface fixation pattern wastes the dispatch when the right parameter is one curl away.

**Discipline**: Discovery is a one-pass surface-mapping step, not an iteration. If a pass yields nothing actionable, document and proceed to the next sub-skill — do NOT re-run discovery hoping for a different result.

**Why this gate exists**: Agents that skip discovery default to single-URL curl guessing, which O(N) enumerates without converging. Discovery turns O(N) guessing into O(1) endpoint identification.

### Phase 2 — Route to Sub-Skill

Use the routing table in `load_skill("/skills/standard/exploit/web/SKILL.md")` to pick the right web sub-skill, OR `load_skill("/skills/standard/exploit/ad/SKILL.md")` for Active Directory work.

Decision flow (web):

```
Tag/hint includes race/concurrent/toctou           → race-condition.md
Tag/hint includes smuggling/desync/hrs OR
   multi-proxy stack detected                      → smuggling.md
Has serialized data (cookies, POST blob)           → deserialization.md
Has query parameters                               → sqli.md, then ssti.md
Has file/path parameters                           → lfi.md
Has URL/fetch parameters                           → ssrf.md
Has search/input reflection                        → xss.md
Has file upload form                               → file-upload.md
Accepts XML input                                  → xxe.md
Has /graphql endpoint                              → graphql.md
Has ping/exec functionality                        → command-injection.md
Has object IDs in URLs                             → idor.md
Unknown                                            → run quick detection probes in skills/exploit/web/SKILL.md
```

ALWAYS load the specialized sub-skill before executing any attack. The routing skill is routing only — sub-skills carry the actual procedures.

### Phase 3 — Confirm-Gate

Some sub-skills require a confirm-gate before iteration. Skipping the gate is what stalls cycles:

- **Smuggling**: confirm-desync gate (`smuggling.md`) MUST fire positive before iterating variants. Differential parsing is NOT confirmed desync.
- **Race-condition**: confirm a single race win against fresh state before declaring exploitability.
- **Blind deserialization**: re-check session cookies and bisect if recon didn't enumerate gating, before blaming the chain (`deserialization.md` Auth-Cookie Checkpoint).

### Phase 4 — Verify

Victim-observable evidence is mandatory:

- **Race conditions**: ≥3 reproductions against fresh state (new coupon / reset balance / fresh session). Log per-trial win counts. A single race win can be lucky scheduling.
- **Smuggling**: stolen cookie/header reflected, backend admin output reflected, or cross-user state contamination. Differential 400/501 alone is NOT verification.
- **Sinks (deser/SSTI/SQLi/etc.)**: capture the secret/credential/target-string OR proof of code execution (file write, OOB callback, queryable response).
- Record evidence to `findings/FIND-{NNN}.md` and update the objective via `update_objective`.

## Discipline / Anti-patterns

### Pre-Attack Cleanup

Before launching any main exploit harness, kill all stale processes from the exploration/recon phase:

```bash
pkill -f '_debug\|sweep\|probe\|nohup' 2>/dev/null || true
pkill -f 'python3.*<target_port_or_pattern>' 2>/dev/null || true
ps aux | grep python3 | grep -v grep   # verify clean
```

A single-threaded Flask/Express dev server cannot handle concurrent oracle + debug scripts. Resource exhaustion from accumulated background processes is the most common cause of "connection refused" mid-exploit. One active harness at a time.

### Atomic Exploit Scripts

Each exploit script must be **self-contained**: probe → attack → submit/verify in ONE invocation against ONE session state. Do not separate "decrypt" from "submit" into different scripts or sequential runs.

- A new GET request generates a new session (new encrypted captcha, new JWT, new CSRF token). Plaintext recovered from session A cannot be submitted using session B.
- If a harness is **producing byte-by-byte progress output**, let it finish. Write a separate submission script that reads the final output and submits — do NOT `edit_file` on a script mid-run.
- File-based scripts (`cat > exploit.py`) are acceptable for complex harnesses, but once producing output, copy to a new filename before editing. Never modify the running file.

### Strategy Regression Anti-Pattern

Once a harness is producing progress (any of: byte-by-byte output, partial extraction, oracle responses, side-channel timing differential), STAY ON IT until completion or until progress halts (no new bytes, no new oracle responses, no new differential observations across multiple polls of its output).

**Forbidden moves while a harness is producing progress**:
- Reverting to manual single-shot curls. The harness is faster; manual is slower. There is no scenario where reverting is correct.
- Switching to a NEW attack vector you discovered mid-extraction (e.g. you find /upload.php while extracting a password — finish the password extraction first, then come back to /upload.php).
- Writing a new "improved" harness from scratch instead of letting the current one finish. If a tweak is needed, copy the file (`cp harness.py harness2.py`), edit the copy, run it, and only abandon the original when the copy yields a strict superset.

**Allowed moves**:
- Add a parallel harness on a DIFFERENT attack surface (e.g. running sqlmap on `id=` while a manual blind extraction runs on `q=`).
- Pre-stage submission/verification scripts that read the running harness's eventual output.

**Why**: Extraction-heavy failures end with the password 60% extracted but the agent context-switched to a new finding before completing the path that would have completed the objective. Strategy regression burns the partial work AND the time to re-establish a new strategy.

### Silent-Stall Recovery

If the previous tool call has produced no output across multiple polls (the standard wedge signature: 3+ consecutive empty-command polls on the same session), the bash session is wedged on a hung child process — most often a raw-socket script with no `socket.settimeout`. Continuing to call tools against the same shell will NOT recover.

**Do NOT:**
- Re-run the same tool call hoping it returns this time. The child is still blocking; you'll wedge again.
- Poll with empty bash commands (`""`, `echo`, `pwd`). Empty polls do not interrupt a wedged stdin/stdout pump. Three consecutive empty-command polls = confirmed wedge — escalate.
- Continue planning silently while waiting. A wedged shell never recovers on its own; planning more turns against it is the same as polling.
- Assume the script "is just slow" when no output has landed and the harness has progress logging. A correctly-bounded probe (raw-socket with `sock.settimeout`, outer `timeout`) ALWAYS prints at least one line within its inner timeout.

**DO (in order):**
1. Open a NEW bash session (the old one is dead — abandon it).
2. `pkill -9 -f <hung_script_name_or_pattern>` to free the wedged process. Use a specific pattern; do not `pkill -9 python3` (kills your recovery probes too).
3. `ls -la <expected_output_file>` — 0 bytes or missing means the child wedged in network I/O before any flush; non-zero means partial progress was made and may inform the next variant.
4. Switch variant or rewrite the harness with BOTH `sock.settimeout(<bounded>)` (before connect AND before each recv) AND outer `timeout <bounded> python3 -u -c '...'` before retrying. Specific timeout values belong in the per-vuln skill (e.g. smuggling.md), not here — what matters is that BOTH timeouts are present.

### Three-Strike Rule

If you wedge **3 times in a row** on the same target (after applying the recovery sequence each time), STOP iterating. The path is not exploitable from this sandbox in this cycle. Write `WEDGED.md` with the three harnesses, their wedge signatures, and what was already ruled out, then `update_objective(status="blocked", notes="3x silent-stall on <variant>")` so orchestrator can re-plan.

### Time-Box Discipline

When the orchestrator names a time budget in the `task()` prompt (e.g. "exploit in ≤30 minutes", "wall-clock budget: 1800s"), treat that as a **hard wall-clock deadline**, not a guideline. The deadline ticks even while you are wedged, planning, or waiting on a slow oracle.

**At deadline expiration:**

1. `pkill -9 -f <pattern>` for every active script across every shell session you opened. Do NOT leave background processes burning sandbox resources for the next cycle.
2. Write `/workspace/<challenge>/exploit/TIMEOUT_DUMP.md` containing:
   - **Last 50 lines per log** — `tail -n 50 <each-log-file>`, one per harness, clearly labeled.
   - **Working hypothesis** — what you currently believe is true about the target (sink, oracle, gating cookie, frontend stack, etc.) at the moment time ran out.
   - **Last partial output** — any plaintext bytes recovered, any successful gate response, any captured trace; even one block of a padding-oracle decrypt is useful for the next attempt.
   - **Recommended next attempt** — a single concrete next step the next exploit context should try first (e.g. "switch to httpx[http2] for h2.te variant", "drop max_workers to 4 to dodge rate-limit"). Be specific, not aspirational.
3. `update_objective(id, status="timeout", reason="exploit time-box expired at <deadline>; see TIMEOUT_DUMP.md")` and **return**. Do not start a new attempt past the deadline — the orchestrator decides whether to extend, reroute, or block.

A timeout is NOT the same as a wedge or a blocked-by-evidence verdict; it is a budget signal back to orchestration. The TIMEOUT_DUMP.md is what makes the budget honest — without it the next cycle starts cold, the budget was wasted.

### Sandbox Bash Discipline

Cross-cutting rules (heredoc traps, raw-socket / long-running probe discipline including mandatory inner-socket and outer `timeout` walls, wedged-session recovery) are auto-injected into every sub-agent's bash tool prompt — no manual load required.

## Handoff Format (output files)

```
./
├── exploit/
│   ├── <variant>_evidence.txt     # per-variant raw response captures
│   ├── <variant>_summary.md       # what was tried, what worked
│   └── WEDGED.md                  # only on three-strike (with wedge signatures)
├── findings/
│   └── FIND-NNN.md                # victim-observable evidence + reproduction recipe
└── shells.json                     # active sessions, if any
```
