Hack The Box —BountyHunter Writeup

For those who are experienced with Python 3 scripting and XXE Injection, or at least willing to learn this stuff, this is a proper machine…

Hack The Box —BountyHunter Writeup
Old UI of Hack The Box Info Card
New UI of Hack The Box Info Card

For those who are experienced with Python 3 scripting and XXE Injection, or at least willing to learn this stuff, this is a proper machine for you. Also, this machine forces you to perform a code review in order to detect anomalies in specific lines of code that potentially vulnerable to exploitation. Without further ado, let’s begin!

Reconnaissance & Enumeration

As usual, we’re using Nmap to scan the ports with the result below.

  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

Nmap shows 2 ports open.

  • Port 22: OpenSSH 8.2p1
  • Port 80: Apache httpd 2.4.41

Before trying to check Port 80, let’s try to dig further information about it using Nikto.

  - Nikto v2.1.6
 — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -
+ Target IP: 10.129.189.253
+ Target Hostname: 10.129.189.253
+ Target Port: 80
+ Start Time: 2021–07–25 19:59:58 (GMT7)
 — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -
+ Server: Apache/2.4.41 (Ubuntu)
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use ‘-C all’ to force check all possible dirs)
+ Web Server returns a valid response with junk HTTP methods, this may cause false positives.
+ OSVDB-3093: /db.php: This might be interesting… has been seen in web logs from an unknown scanner.
+ 7916 requests: 0 error(s) and 5 item(s) reported on remote host
+ End Time: 2021–07–25 20:28:39 (GMT7) (1721 seconds)
 — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -
+ 1 host(s) tested

We need to take a note at the most interesting information regarding to the Nikto result, the /db.php directory. Let’s save it for future usage.

We then take a look at port 80.

Figure 1. Bounty Hunter’s port 80 page

It’s just a simple landing page, no login page detected. I checked all over the page and found something interesting. The portal page looks redirecting to somewhere.

Figure 2. Portal page redirected to another page

A form! Let’s fill it with some arbitrary values and see what it shows.

Figure 3. Bounty Report System Form
Figure 4. Form output

The output shows the input value as it is, thought of something?

One thing that crossed my mind is to execute any injection payloads. But before doing any further, let’s perform a directory scanning using Gobuster.

Figure 5. Gobuster scan result

From the result, we see several accessible pages, the index.php (obviously) with 200 status code, the rest are any assets/resource pages such as /assets, /css, /js, and /resources with 301 status code which means redirected to another page. The /resources page makes me the most curious of all, let’s take a look at it.

Figure 6. Hidden directory page

Here it is, the directory listing with an information disclosure vulnerability. The bountylog.js seems suspicious.

Figure 7. The code behind the form

As we see from the above result, the bountylog.js is a script that handles POST data to tracker_diRbPr00f314.php endpoint from submitted user input at log_submit.php page before. The bountySubmit() function decodes the XML script into base64 using btoa() built-in JS function, and then call the returnSecret() function asynchronously to POST the data with the data key by Ajax request.

Let’s try intercept the request with BurpSuite!

Figure 8. Intercepted request using BurpSuite

The POST request in data key sent something in base64 encoding, but apparently it converted to another form before converted to base64. We then use the Inspector menu to trace the decoding steps by selecting the decoded string (see Figure 9). From the Inspector result, the string is decoded from URL encoding first before being converted to base64. In other word, in order to send a valid request, we must first convert the XML request to base64 and then to URL encoding.

Figure 9. Decoding steps using BurpSuite Inspector

To make things clearer, we use Cyber Chef to show the decoding process as Figure 10 below. The final decoded form is in XML (Extensive Markup Language), which potentially exploitable by XXE (XML External Entity) Injection.

Figure 10. Decoding process with CyberChef

Initial Foothold

Speaking of XXE, let’s try a payload which reads the /etc/passwd directory as shown in Figure 11 and 12.

Figure 11. Encoded XXE /etc/passwd payload
Figure 12. XXE /etc/passwd content response

Based on the above response, we can conclude that the system is vulnerable to XXE. Take a note of the user credential, development, with id 1000 for future reference.

Using the previously obtained hidden directory from Nikto, /db.php, we make this as a XXE payload. In my first attempt, i somehow failed to perform an injection because i treated the directory as a normal directory, while in fact, it is not. I was stuck here quite a while. The .php extension should be treated using PHP filter as shown in Figure 13.

Figure 13. Encoded XXE /db.php payload
Figure 14. XXE /db.php content response

The Injection worked! The response surprisingly returned an extremely confidential thing. Same as before, we used the payload as the POST request parameter and got an intriguing response, which is a PHP code containing the database credential after being decoded.<?php
// TODO -> Implement login system with the database.
$dbserver = “localhost”;
$dbname = “bounty”;
$dbusername = “admin”;
$dbpassword = “m19RoAU0hP41A1sTsq6K”;
$testuser = “test”;
?>

We may use the username or password as the SSH login credential. I tried the $testuser and $dbusername as login username but they were both invalid. Prior to our previous discovery, the development user was a valid credential when combined with our obtained $dbpassword.

Figure 15. Obtained user flag

Finally! We got the user flag.

Privilege Escalation

To gain the root access, we first try to list all possible commands that have the right to use with sudo using sudo -l.

Figure 16. List the commands we have the right to use with sudo

The one and only command that sudo allows with no password is Python3.8 on /opt/skytrain_inc/ticketValidator.py. Shall we need to open the code?

Figure 17. Python code at /opt/skytrain_inc/ticketValidator.py

The code expected path to file from user’s input, if the filename ends with .md extension, it opens the file and executes the next condition as written above. If the ticket is valid, the console prints “Valid ticket.”, else it prints “Invalid ticket.”. Before we exploit the code, I used the positive test case approach. Let’s make a valid .md file with the condition met below.

Figure 18. Valid sample of code response

The reason why i put 39+70 is from the if int(ticketCode) % 7 == 4 line and 2 lines after that, which forces the first number (in this case, 39) to be 4 after modulo by 7 and the sum of two numbers (39 and 70) to be greater than 100. It can be everything, but the numbers popped out in my mind at the first place. The result prints “Valid ticket” as expected (see Figure 18).

Now here comes the interesting part, there exist a code that runs eval() function that gets the sum operation as a string. It took me hours to figure out something odd in this line of code, until i read this article. The exec() function is very dangerous because the user can run any code in our environment, for example we can execute the bash using os library (see Figure 19). If the user is given a wrong permission to the code (as this machine does), our system will very likely doomed! According to the given source:

exec() can execute all Python source code, whereas eval() can only evaluate expressions.

The. Expressions.

What do you mean by expressions?

We can combine the sum operation in a string form with exec() that runs the bash with an and operator inside the eval() to gain root access, consider the python3.8 has the right to run as sudo with no password in this machine. Given the expression that results True, we can exploit the whole system as Figure 19 below. We finally got the root flag!

Figure 19. Obtained root flag

Lesson Learned

  1. Always turned the directory browsing feature off on the server configuration to prevent information disclosure.
  2. Disable XML external entity and DTD processing in all XML parsers in the application, as per the OWASP Cheat Sheet ‘XXE Prevention’.
  3. Whenever possible, use less complex data formats such as JSON, and avoiding serialization of sensitive data.
  4. Implement positive (“whitelisting”) server-side input validation, filtering, or sanitization to prevent hostile data within XML documents, headers, or nodes.
  5. I know this machine is really CTF-alike, but exposing your credential to be indexed by directory buster is totally a dumb way that nowadays Sysadmin would do.
  6. Limit sudo right permissions to programs that run a shell or to the program compiler, editor, or interpreter.
  7. Be careful when using built-in function that can execute commands.

Reference

  1. https://book.hacktricks.xyz/pentesting-web/xxe-xee-xml-external-entity
  2. https://blog.finxter.com/python-exec/#Python_exec_vs_eval
  3. https://hdivsecurity.com/owasp-xml-external-entities-xxe
  4. https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html