
Prometheus v3.15.0 - High - Remote - SSRF - http_sd __unix_socket__ unsanitized Dial leftover of UDS scrape
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · prometheus-unix-socket-dial
Class: SSRF Reach: Remote
Prometheus v3.15.0 (5241a27) - CNCF / Prometheus
v3.15.0 added Unix Domain Socket scrape (#12024 / #18091). The path is a target label __unix_socket__, not an operator allowlist. http_sd and file_sd copy that label through as-is. PopulateLabels never validates it. newUnixSocketScrapeClient then calls DialContext(ctx, "unix", socketPath) with no filepath.Clean, no allowlist, and no reject of ../ or Linux abstract @ names.
TCP scrape cannot do this. CheckTargetAddress still drops __address__ values that contain /, so unix:///var/run/docker.sock as an address is rejected. The new label is the hole.
Stock config does not set the label. Unauth HTTP to Prometheus cannot add jobs. This is SD-gated.
| ID | no CVE yet |
| Class | SSRF (unsanitized unix Dial from SD labels) |
| Reach | Remote (SD-gated: writer of http_sd / file_sd) |
| CWE | CWE-918, CWE-22 |
| CVSS | High: 8.6 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| Product | Prometheus |
| Affected | v3.15.0 (5241a27fe3c6983549fccc32f6e65917408c63cd), image prom/prometheus:v3.15.0 |
| Auth | SD-gated: writer of http_sd / file_sd labels (or a relabel that copies untrusted meta into __unix_socket__) |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Control of http_sd JSON (or writable file_sd) is enough. The SD URL is operator config. The leftover is that group labels may set __unix_socket__ and Prometheus will Dial it.
| Privilege | Method / sink | Lab-proven or inferred |
|---|---|---|
| Connect to a host unix socket the Prometheus UID can open | clientForTarget -> DialContext(ctx, "unix", socketPath) from __unix_socket__ | Lab-proven. Path /sockets/jail/../oracle.sock from http_sd; scrape health up |
Follow ../ in that path | no filepath.Clean / no .. reject | Lab-proven. Kernel resolved the traversal to /sockets/oracle.sock |
| Dial a Linux abstract socket | same Dial of @name | Lab-proven. __unix_socket__=@prom-lab-oracle; scrape health up |
| HTTP GET on that socket as the Prometheus UID | scrape is GET; __metrics_path__ is the request path | Lab-proven. GET /fs-traversal and /abstract; User-Agent Prometheus/; lab_witness ingested |
| Reach sockets TCP scrape cannot name | CheckTargetAddress rejects / in __address__ | Lab-proven. unix:///var/run/docker.sock as __address__ logged is not a valid hostname; not an active target; prometheus_target_sync_failed_total=1 |
| GET Docker Engine / kubelet HTTP APIs if those sockets are in scope | __unix_socket__=/var/run/docker.sock plus a Docker API path | Inferred. Same unsanitized Dial. This lab does not mount a real engine socket. JSON APIs usually fail exposition parse, so the High is the connect/GET, not TSDB exfil of Docker JSON |
| Mutate Docker (create container, write) | scrape verbs | Closed. Scrape is GET-only |
| Add scrape jobs over unauth Prometheus HTTP | /-/reload, admin API | Closed. Both flags default off; reload has no body |
| Use this on default example config | __unix_socket__ unset; example scrapes localhost:9090 over TCP | Closed. In-tree k8s/consul/docker SD emit __meta_* only; those prefixes are deleted after relabel |
Vendor security model already lets SD choose TCP scrape targets. The new primitive is host IPC that __address__ could not name.
I pinned prometheus/prometheus v3.15.0 after the 3.15 FEATURE line for UDS scrape. Default OSS on this pin has admin API, lifecycle HTTP, remote-write receiver, and OTLP receiver off. PopulateDiscoveredLabels copies every http_sd group label. __unix_socket__ is not __meta_*, so it survives the meta wipe. PopulateLabels checks __address__ only.
func (sp *scrapePool) clientForTarget(t *Target) *http.Client {
socketPath := t.labels.Get(UnixSocketLabel)
if socketPath == "" {
return sp.client
}
client, err := newUnixSocketScrapeClient(socketPath, ...)
func newUnixSocketScrapeClient(socketPath string, ...) (*http.Client, error) {
optFuncs = append([]config_util.HTTPClientOption{config_util.WithDialContextFunc(
func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
},
)}, optFuncs...)
CheckTargetAddress still rejects / in the address. URL() keeps scheme/host/path from __scheme__ / __address__ / __metrics_path__; the socket is dial-only.
Wrong turns already recorded: scoring this as in-model NONE because SD already chooses TCP targets (the new primitive is host IPC); treating unix:///var/run/docker.sock as __address__ as the hole (it is rejected); claiming default example config is this leftover. Theatre: a reverse shell. The oracle is scrape health up on sockets the SD JSON named.
python3 prometheus-unix-socket-dial-Abraxas-Labs.py
or
cd lab
./run.sh
prom/prometheus:v3.15.0 shares a netns with a Python sidecar. Sidecar serves http_sd at 127.0.0.1:8080/targets.json and two unix HTTP oracles: filesystem /sockets/oracle.sock and abstract @prom-lab-oracle. Prometheus is published at 127.0.0.1:19390. Admin API and lifecycle stay off.
SUCCESS prometheus-unix-socket-dial sd=http_sd fs-traversal=yes abstract=yes tcp-slash-rejected=yes how=sd-labels PROMETHEUS-UNIX-SOCKET-DIAL-WITNESS
IOC: scrapeUrl=http://localhost:1/fs-traversal and /abstract, both health up. TCP slash never became an active target.
Do not Dial __unix_socket__ from SD until it is a sanitized absolute filesystem path. Reject empty values, NUL, Linux abstract @ names, relative paths, and any .. after filepath.Clean. An allowlist of prefixes from scrape YAML is stronger. Dropping the label from PopulateDiscoveredLabels unless it came from operator config also closes the SD injection.
5241a27fe3c6983549fccc32f6e65917408c63cd)scrape/scrape.go · scrape/target.go · config/config.goGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.