Skip to content

Replication Workflow

The core idea behind a safe pentest workflow: don’t attack the live target during development. Instead, capture the endpoint’s behavior (its contract), rebuild a local stub that mimics it, fuzz the replica freely, then replay only validated payloads against the real target.

[Live target] ──capture──▶ [Contract: requests + responses] ──reverse──▶ [Local replica]
fuzz / probe / exploit
[Validated payloads] ──replay──▶ [Live target]

The contract is the set of requests → responses that define an endpoint. Capture as much as you can before writing any code.

Artifact Tool Why
Raw request/response pairs Burp Suite, mitmproxy, curl -v Full headers, body, status — the gold standard
HAR (HTTP Archive) Playwright, Chrome DevTools Timed, ordered request/response log of a flow
API spec /openapi.json, /swagger, Postman Schema, params, types, auth requirements
Frontend call sites Grep the JS bundle / devtools How the client calls the API, param names, auth headers
Error responses Deliberately malformed requests Validation logic, stack traces, framework fingerprints
Terminal window
# 1. Interactive browser flow -> replayable script + HAR
playwright codegen https://target.example/app
npx playwright test --save-har=flow.har --save-har-glob="**/api/**"
# 2. Proxy all traffic and dump to file
mitmproxy -w traffic.flows
mitmdump -w traffic.flows
# 3. Single endpoint, raw
curl -sS -i -X POST https://target.example/api/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"item_id": 123, "qty": 1}'
# 4. Convert HAR to a request list (jq)
jq -r '.log.entries[] | [.request.method, .request.url, .response.status] | @tsv' flow.har

For each endpoint, answer these questions before building the replica:

  1. Method & routeGET /api/orders/{id}? POST /api/orders? Verb confusion tolerated?
  2. AuthN — What header/cookie/session proves identity? (Authorization: Bearer, X-API-Key, JWT, session cookie.)
  3. AuthZ — Does the response change per user/role? Where is object ownership enforced?
  4. Input contract — Required vs optional params, types, ranges, enums, defaults.
  5. Validation behavior — What happens on bad types, missing fields, oversized values, negative numbers, arrays where objects are expected?
  6. Response shape — Status codes, JSON structure, error format, pagination, rate-limit headers (X-RateLimit-*).
  7. Side effects — Does it write to DB, send email, call a third party, trigger a payment?
  8. Idempotency — Safe to repeat? Critical for fuzzing safety.
Terminal window
# Discover endpoints from a target host
katana -u https://target.example -jc -d 5
gau target.example | sort -u
ffuf -u https://target.example/FUZZ -w /usr/share/wordlists/dirb/common.txt
# Hidden/undocumented params
arjun -u https://target.example/api/orders -m POST

Choose a lightweight framework; the goal is behavioral fidelity, not a perfect reimplementation.

Stack Framework Use when
Python FastAPI / Flask Fast to write, easy validation + OpenAPI
Node Express Target is Node; reuse middleware patterns
Go net/http / chi Target is Go; concurrency-heavy fuzzing

What the replica must reproduce: routes + methods, authN/authZ shape (where 401/403 fire), input validation, response shapes, and — critically — the failure modes you saw in Phase 0.

replica.py
from fastapi import FastAPI, Header, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI()
DB = { # fake user-owned objects
100: {"owner": "alice", "data": "alice-order"},
101: {"owner": "bob", "data": "bob-order"},
}
class Order(BaseModel):
item_id: int = Field(..., ge=1)
qty: int = Field(1, ge=1, le=99)
def get_user(authorization: Optional[str] = Header(None)):
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(401, detail={"error": "unauthorized"})
return authorization.removeprefix("Bearer ")
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user: str = Depends(get_user)):
row = DB.get(order_id)
if row is None:
raise HTTPException(404, detail={"error": "not_found"})
# BOLA: intentionally no ownership check (mirrors the real flaw)
return {"id": order_id, "data": row["data"]}
@app.post("/api/orders")
def create_order(order: Order, user: str = Depends(get_user)):
return {"id": 102, "status": "created", **order.model_dump()}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
Terminal window
pip install fastapi uvicorn
python replica.py

Confirm the replica behaves like the real target before trusting your fuzzing results: replay captured requests and diff status + body, compare error cases, and check side-effect parity.

Terminal window
# Send the same request to both, normalize, diff
curl -s http://127.0.0.1:8000/api/orders/100 | jq -S . > local.json
curl -s https://target.example/api/orders/100 | jq -S . > real.json
diff <(jq -S . local.json) <(jq -S . real.json)

Now fuzz aggressively. Nothing here touches the real target.

Terminal window
# Route + parameter fuzzing
ffuf -u http://127.0.0.1:8000/FUZZ -w /usr/share/wordlists/dirb/common.txt
ffuf -u http://127.0.0.1:8000/api/orders?FUZZ=1 -w params.txt -fs 422 -mc all
# Object/ID enumeration (BOLA / IDOR)
seq 1 200 | xargs -P 10 -I{} \
curl -s -o /dev/null -w "%{http_code} {}\n" \
-H "Authorization: Bearer alice" \
http://127.0.0.1:8000/api/orders/{}
# Injection wordlists
ffuf -u http://127.0.0.1:8000/api/orders \
-X POST -H "Content-Type: application/json" \
-d '{"item_id": FUZZ, "qty": 1}' \
-w /usr/share/seclists/Fuzzing/SQLi/quick-SQLi.txt
# Concurrency blast (rate-limit / resource-consumption checks)
seq 1 500 | xargs -P 50 -I{} curl -s -o /dev/null -w "%{http_code}\n" \
http://127.0.0.1:8000/api/orders/100

An authN/authZ matrix is a great way to catch BOLA/BFLA: run route × method × role and expect 200 only where the role is allowed.

Terminal window
for role in anon alice bob admin; do
for method in GET POST PUT DELETE; do
code=$(curl -s -o /dev/null -w "%{http_code}" \
-X $method http://127.0.0.1:8000/api/orders/100 \
-H "Authorization: Bearer $role")
printf "%-8s %-6s -> %s\n" "$role" "$method" "$code"
done
done

For GraphQL, probe introspection and recover schemas when introspection is disabled:

Terminal window
curl -s http://127.0.0.1:8000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name } } }"}'
# schema recovery tools: clairvoyance, graphql-cop, InQL (Burp), gqlmap

Only after a payload demonstrably works on the replica:

  1. Re-check RoE — confirm the finding is in scope and allowed.
  2. Rate-limit yourself — one request at a time, minimal volume.
  3. Prefer non-destructive proofs — a 200 on someone else’s object ID, or a harmless read; don’t delete/modify real data.
  4. Log everything — timestamp, request, response, for your report.
  5. Document impact + remediation — map each finding to the OWASP API Top 10.