Hack The Box —BountyHunter Writeup
For those who are experienced with Python 3 scripting and XXE Injection, or at least willing to learn this stuff, this is a proper machine…


For those who are experienced with Python 3 scripting and XXE Injection, or at least willing to learn this stuff, this is a proper machine for you. Also, this machine forces you to perform a code review in order to detect anomalies in specific lines of code that potentially vulnerable to exploitation. Without further ado, let’s begin!
Reconnaissance & Enumeration
As usual, we’re using Nmap to scan the ports with the result below.
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-server-header: Apache/2.4.41 (Ubuntu) |_http-title: Bounty Hunters Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Nmap shows 2 ports open.
- Port 22: OpenSSH 8.2p1
- Port 80: Apache httpd 2.4.41
Before trying to check Port 80, let’s try to dig further information about it using Nikto.
- Nikto v2.1.6 — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — - + Target IP: 10.129.189.253 + Target Hostname: 10.129.189.253 + Target Port: 80 + Start Time: 2021–07–25 19:59:58 (GMT7) — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — - + Server: Apache/2.4.41 (Ubuntu) + The anti-clickjacking X-Frame-Options header is not present. + The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS + The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type + No CGI Directories found (use ‘-C all’ to force check all possible dirs) + Web Server returns a valid response with junk HTTP methods, this may cause false positives. + OSVDB-3093: /db.php: This might be interesting… has been seen in web logs from an unknown scanner. + 7916 requests: 0 error(s) and 5 item(s) reported on remote host + End Time: 2021–07–25 20:28:39 (GMT7) (1721 seconds) — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — - + 1 host(s) tested
We need to take a note at the most interesting information regarding to the Nikto result, the /db.php
directory. Let’s save it for future usage.
We then take a look at port 80.

It’s just a simple landing page, no login page detected. I checked all over the page and found something interesting. The portal page looks redirecting to somewhere.

A form! Let’s fill it with some arbitrary values and see what it shows.


The output shows the input value as it is, thought of something?
…
One thing that crossed my mind is to execute any injection payloads. But before doing any further, let’s perform a directory scanning using Gobuster.

From the result, we see several accessible pages, the index.php
(obviously) with 200 status code, the rest are any assets/resource pages such as /assets
, /css
, /js
, and /resources
with 301 status code which means redirected to another page. The /resources
page makes me the most curious of all, let’s take a look at it.

Here it is, the directory listing with an information disclosure vulnerability. The bountylog.js
seems suspicious.

As we see from the above result, the bountylog.js
is a script that handles POST data to tracker_diRbPr00f314.php
endpoint from submitted user input at log_submit.php
page before. The bountySubmit()
function decodes the XML script into base64 using btoa()
built-in JS function, and then call the returnSecret()
function asynchronously to POST the data with the data
key by Ajax request.
Let’s try intercept the request with BurpSuite!

The POST request in data
key sent something in base64 encoding, but apparently it converted to another form before converted to base64. We then use the Inspector menu to trace the decoding steps by selecting the decoded string (see Figure 9). From the Inspector result, the string is decoded from URL encoding first before being converted to base64. In other word, in order to send a valid request, we must first convert the XML request to base64 and then to URL encoding.

To make things clearer, we use Cyber Chef to show the decoding process as Figure 10 below. The final decoded form is in XML (Extensive Markup Language), which potentially exploitable by XXE (XML External Entity) Injection.

Initial Foothold
Speaking of XXE, let’s try a payload which reads the /etc/passwd
directory as shown in Figure 11 and 12.


Based on the above response, we can conclude that the system is vulnerable to XXE. Take a note of the user credential, development
, with id 1000 for future reference.
Using the previously obtained hidden directory from Nikto, /db.php
, we make this as a XXE payload. In my first attempt, i somehow failed to perform an injection because i treated the directory as a normal directory, while in fact, it is not. I was stuck here quite a while. The .php
extension should be treated using PHP filter as shown in Figure 13.


The Injection worked! The response surprisingly returned an extremely confidential thing. Same as before, we used the payload as the POST request parameter and got an intriguing response, which is a PHP code containing the database credential after being decoded.<?php
// TODO -> Implement login system with the database.
$dbserver = “localhost”;
$dbname = “bounty”;
$dbusername = “admin”;
$dbpassword = “m19RoAU0hP41A1sTsq6K”;
$testuser = “test”;
?>
We may use the username or password as the SSH login credential. I tried the $testuser
and $dbusername
as login username but they were both invalid. Prior to our previous discovery, the development
user was a valid credential when combined with our obtained $dbpassword
.

Finally! We got the user flag.
Privilege Escalation
To gain the root access, we first try to list all possible commands that have the right to use with sudo using sudo -l
.

The one and only command that sudo allows with no password is Python3.8 on /opt/skytrain_inc/ticketValidator.py
. Shall we need to open the code?

at /opt/skytrain_inc/ticketValidator.py
The code expected path to file from user’s input, if the filename ends with .md
extension, it opens the file and executes the next condition as written above. If the ticket is valid, the console prints “Valid ticket.
”, else it prints “Invalid ticket.
”. Before we exploit the code, I used the positive test case approach. Let’s make a valid .md
file with the condition met below.

The reason why i put 39+70 is from the if int(ticketCode) % 7 == 4
line and 2 lines after that, which forces the first number (in this case, 39) to be 4 after modulo by 7 and the sum of two numbers (39 and 70) to be greater than 100. It can be everything, but the numbers popped out in my mind at the first place. The result prints “Valid ticket
” as expected (see Figure 18).
Now here comes the interesting part, there exist a code that runs eval()
function that gets the sum operation as a string. It took me hours to figure out something odd in this line of code, until i read this article. The exec()
function is very dangerous because the user can run any code in our environment, for example we can execute the bash using os
library (see Figure 19). If the user is given a wrong permission to the code (as this machine does), our system will very likely doomed! According to the given source:
exec()
can execute all Python source code, whereaseval()
can only evaluate expressions.
The. Expressions.
What do you mean by expressions?
We can combine the sum operation in a string form with exec()
that runs the bash with an and
operator inside the eval()
to gain root access, consider the python3.8 has the right to run as sudo
with no password in this machine. Given the expression that results True
, we can exploit the whole system as Figure 19 below. We finally got the root flag!

Lesson Learned
- Always turned the directory browsing feature off on the server configuration to prevent information disclosure.
- Disable XML external entity and DTD processing in all XML parsers in the application, as per the OWASP Cheat Sheet ‘XXE Prevention’.
- Whenever possible, use less complex data formats such as JSON, and avoiding serialization of sensitive data.
- Implement positive (“whitelisting”) server-side input validation, filtering, or sanitization to prevent hostile data within XML documents, headers, or nodes.
- I know this machine is really CTF-alike, but exposing your credential to be indexed by directory buster is totally a dumb way that nowadays Sysadmin would do.
- Limit sudo right permissions to programs that run a shell or to the program compiler, editor, or interpreter.
- Be careful when using built-in function that can execute commands.
Reference
- https://book.hacktricks.xyz/pentesting-web/xxe-xee-xml-external-entity
- https://blog.finxter.com/python-exec/#Python_exec_vs_eval
- https://hdivsecurity.com/owasp-xml-external-entities-xxe
- https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
