
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
Write-up: https://abraxaslabs.tech/research/prometheus-unix-socket-dial
Classe: SSRF Alcance: Remoto
Prometheus v3.15.0 (5241a27) - CNCF / Prometheus
A v3.15.0 adicionou scrape via 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 é a brecha.
A configuração padrão não define o label. HTTP sem autenticação ao Prometheus não consegue adicionar jobs. Isso é condicionado ao SD.
| ID | ainda sem CVE |
| Classe | SSRF (Dial unix não sanitizado a partir de labels SD) |
| Alcance | Remoto (condicionado ao 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 | Condicionado ao SD: quem escreve labels de http_sd / file_sd (ou um relabel que copie 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 resta é que os labels do grupo podem definir __unix_socket__ e o Prometheus fará o Dial.
| Privilégio | Método / sink | Comprovado em laboratório ou inferido |
|---|---|---|
| Conectar a um socket unix de host que o UID do Prometheus consegue abrir | clientForTarget -> DialContext(ctx, "unix", socketPath) a partir de __unix_socket__ | Comprovado em laboratório. 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 laboratório. O kernel resolveu a travessia para /sockets/oracle.sock |
| Discar um socket abstrato do Linux | mesmo Dial de @name | Comprovado em laboratório. __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 laboratório. 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 laboratório. unix:///var/run/docker.sock como __address__ registrou is not a valid hostname; não é um alvo ativo; prometheus_target_sync_failed_total=1 |
| 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 laboratório não monta um socket real do engine. APIs JSON geralmente falham na análise de exposição, então o risco Alto é o connect/GET, não a exfiltração via TSDB do JSON do Docker |
| Mutar o Docker (criar container, escrever) | verbos de scrape | Fechado. O scrape é somente GET |
| Adicionar jobs de scrape via HTTP do Prometheus sem autenticação | /-/reload, API de admin | Fechado. Ambas as flags 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 SD k8s/consul/docker in-tree emitem apenas __meta_*; esses prefixos são removidos 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 de host que __address__ não conseguia nomear.
Eu fixei prometheus/prometheus v3.15.0 após a linha FEATURE da 3.15 para scrape UDS. O OSS padrão nesse pin tem a API de admin, o lifecycle HTTP, o receptor de remote-write e o receptor OTLP desativados. PopulateDiscoveredLabels copia cada rótulo 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 address. URL() mantém scheme/host/path de __scheme__ / __address__ / __metrics_path__; o socket é apenas para dial.
Caminhos errados já registrados: pontuar isto como NONE no modelo porque o SD já escolhe alvos TCP (a nova primitiva é IPC de host); tratar unix:///var/run/docker.sock como __address__ como a falha (ele é rejeitado); afirmar que a configuração de exemplo padrão é este resíduo. Teatro: um reverse shell. O oráculo é a saúde de scrape up nos 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 uma 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 abstract @prom-lab-oracle. O Prometheus é publicado em 127.0.0.1:19390. A API de administração e o ciclo de vida 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 health up. O TCP slash nunca se tornou um alvo ativo.
Não faça Dial de __unix_socket__ a partir do SD até que seja um caminho de filesystem absoluto e sanitizado. Rejeite valores vazios, NUL, nomes abstratos Linux @, caminhos relativos e qualquer .. após filepath.Clean. Uma allowlist de prefixos do scrape YAML é 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.