
Prometheus v3.15.0 - Alta - Remoto - SSRF - http_sd __unix_socket__ Dial não sanitizado remanescente do scrape UDS
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · prometheus-unix-socket-dial
Classe: SSRF Alcance: Remoto
Prometheus v3.15.0 (5241a27) - CNCF / Prometheus
A v3.15.0 adicionou o scrape de Unix Domain Socket (#12024 / #18091). O caminho é um label de destino __unix_socket__, não uma allowlist do operador. http_sd e file_sd copiam esse label como está. PopulateLabels nunca o valida. newUnixSocketScrapeClient então chama DialContext(ctx, "unix", socketPath) sem filepath.Clean, sem allowlist e sem rejeitar ../ ou nomes abstratos @ do Linux.
O scrape TCP não consegue fazer isso. CheckTargetAddress ainda descarta valores de __address__ que contenham /, então unix:///var/run/docker.sock como endereço é rejeitado. O novo label é o buraco.
A configuração padrão não define o label. HTTP sem autenticação para o Prometheus não consegue adicionar jobs. Isso é restrito por SD.
| ID | ainda sem CVE |
| Classe | SSRF (Dial unix não sanitizado a partir de labels de SD) |
| Alcance | Remoto (restrito por SD: quem escreve http_sd / file_sd) |
| CWE | CWE-918, CWE-22 |
| CVSS | Alto: 8.6 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| Produto | Prometheus |
| Afetado | v3.15.0 (5241a27fe3c6983549fccc32f6e65917408c63cd), imagem prom/prometheus:v3.15.0 |
| Autenticação | Restrito por SD: quem escreve os labels de http_sd / file_sd (ou um relabel que copia meta não confiável para __unix_socket__) |
| Licença | GNU Affero GPL v3.0 |
| Lab | somente 127.0.0.1 |
O controle do JSON de http_sd (ou de um file_sd gravável) é suficiente. A URL do SD é configuração do operador. O que sobra é que os labels do grupo podem definir __unix_socket__ e o Prometheus fará o Dial.
| Privilégio | Método / sink | Comprovado em lab ou inferido |
|---|---|---|
| Conectar a um unix socket do host que o UID do Prometheus consegue abrir | clientForTarget -> DialContext(ctx, "unix", socketPath) a partir de __unix_socket__ | Comprovado em lab. Caminho /sockets/jail/../oracle.sock a partir de http_sd; saúde do scrape up |
Seguir ../ nesse caminho | sem filepath.Clean / sem rejeição de .. | Comprovado em lab. O kernel resolveu o traversal para /sockets/oracle.sock |
| Discar para um socket abstrato do Linux | mesmo Dial de @name | Comprovado em lab. __unix_socket__=@prom-lab-oracle; saúde do scrape up |
| HTTP GET nesse socket como o UID do Prometheus | o scrape é GET; __metrics_path__ é o caminho da requisição | Comprovado em lab. GET /fs-traversal e /abstract; User-Agent Prometheus/; lab_witness ingerido |
| Alcançar sockets que o scrape TCP não consegue nomear | CheckTargetAddress rejeita / em __address__ | Comprovado em lab. unix:///var/run/docker.sock como __address__ registrou is not a valid hostname; não é um alvo ativo; prometheus_target_sync_failed_total=1 |
| Fazer GET nas APIs HTTP do Docker Engine / kubelet se esses sockets estiverem no escopo | __unix_socket__=/var/run/docker.sock mais um caminho da API do Docker | Inferido. Mesmo Dial não sanitizado. Este lab não monta um socket real do engine. APIs JSON geralmente falham no parse de exposição, então o Alto é o connect/GET, não a exfiltração via TSDB do JSON do Docker |
| Modificar o Docker (criar container, escrever) | verbos do scrape | Fechado. O scrape é somente GET |
| Adicionar jobs de scrape via HTTP sem autenticação do Prometheus | /-/reload, API de admin | Fechado. Ambas as flags vêm desativadas por padrão; o reload não tem corpo |
| Usar isso na configuração de exemplo padrão | __unix_socket__ não definido; o exemplo faz scrape de localhost:9090 via TCP | Fechado. Os SDs k8s/consul/docker do próprio projeto emitem apenas __meta_*; esses prefixos são apagados após o relabel |
O modelo de segurança do fornecedor já permite que o SD escolha alvos de scrape TCP. A nova primitiva é IPC do host que __address__ não conseguia nomear.
Fixe na versão prometheus/prometheus v3.15.0 após a linha de FEATURE 3.15 para scrape UDS. O OSS padrão nesse pin tem API de admin, lifecycle HTTP, receptor de remote-write e receptor OTLP desativados. PopulateDiscoveredLabels copia todos os labels de grupo do http_sd. __unix_socket__ não é __meta_*, então sobrevive à limpeza de meta. PopulateLabels verifica apenas __address__.
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 ainda rejeita / no endereço. URL() mantém scheme/host/path de __scheme__ / __address__ / __metrics_path__; o socket é apenas para o Dial.
Caminhos errados já registrados: classificar isso como NONE dentro do modelo porque o SD já escolhe alvos TCP (a nova primitiva é IPC do host); tratar unix:///var/run/docker.sock como __address__ como o buraco (ele é rejeitado); afirmar que a configuração de exemplo padrão é esse resquício. Teatro: um reverse shell. O oráculo é a saúde do scrape up em sockets que o JSON do SD nomeou.
python3 prometheus-unix-socket-dial-Abraxas-Labs.py
ou
cd lab
./run.sh
prom/prometheus:v3.15.0 compartilha um netns com um sidecar Python. O sidecar serve http_sd em 127.0.0.1:8080/targets.json e dois oráculos HTTP unix: filesystem /sockets/oracle.sock e abstrato @prom-lab-oracle. O Prometheus é publicado em 127.0.0.1:19390. A API de admin e o lifecycle permanecem desativados.
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 e /abstract, ambos com saúde up. A barra no TCP nunca se tornou um alvo ativo.
Não faça Dial em __unix_socket__ a partir do SD até que seja um caminho de filesystem absoluto e sanitizado. Rejeite valores vazios, NUL, nomes abstratos @ do Linux, caminhos relativos e qualquer .. após filepath.Clean. Uma allowlist de prefixos a partir do YAML de scrape é mais forte. Remover o label de PopulateDiscoveredLabels a menos que ele venha da configuração do operador também fecha a injeção via SD.