Skip to main content

Command Palette

Search for a command to run...

When Deserialization Meets eval(): Anatomy of a Full-Stack Compromise

Hack the Box: Interpreter

Updated
5 min read
When Deserialization Meets eval(): Anatomy of a Full-Stack Compromise
E
Software Engineer & Security Researcher

Security incidents rarely hinge on a single catastrophic bug. More often, they emerge from layered design shortcuts — each individually survivable, but collectively fatal.

This case study examines a real exploit chain combining:

  • A deserialization RCE in a healthcare integration engine

  • Poor privilege boundaries

  • A Python double-evaluation flaw

  • A misguided input filter

  • And a root-owned service that should never have been privileged

The result: complete system compromise.


Phase 1 — Internet-Facing Deserialization (CVE-2023-43208)

The entry point was CVE-2023-43208, a bypass vulnerability in NextGen Mirth Connect, an integration engine widely used in healthcare environments.

Architectural Risk #1: Integration Engines at the Network Boundary

Integration engines are powerful by design. They:

  • Parse XML

  • Execute transformations

  • Interact with databases

  • Invoke system-level connectors

Exposing such a system directly to the internet already expands your attack surface dramatically.

Now combine that with unsafe XML deserialization.

The Root Cause

Mirth used the XStream library to unmarshal XML into Java objects.

XStream, unless configured with strict type allowlisting, allows construction of arbitrary object graphs. Attackers can leverage this to trigger gadget chains — in this case via java.lang.ProcessBuilder.

This CVE was not an original flaw. It was a patch bypass of a prior deserialization vulnerability.

That detail matters.

It signals systemic fragility, not a one-off mistake.

Impact

A crafted XML payload submitted to exposed API endpoints resulted in arbitrary command execution as the mirth service account.

At this stage, compromise was limited.

That’s important.

Containment was still possible.


Phase 2 — Credential Exposure & Lateral Movement

From the service account foothold:

  • Application configuration files were accessed

  • Database credentials were extracted

  • Password hashes were dumped

  • Offline cracking yielded access to a legitimate system user

This was not “magic escalation.”

It was a predictable consequence of:

  • Flat privilege boundaries

  • Sensitive credentials stored in accessible configuration

  • Reusable authentication material

Still, root was not yet obtained.

The system could have survived here — if privilege separation had been enforced.


Phase 3 — A Root-Owned Flask Service

The final link in the chain was an internal Python application:

/usr/local/bin/notif.py

This service:

  • Parsed XML input

  • Constructed formatted output using f-strings

  • Ran as root

There is no legitimate reason for a notification formatter to run as root.

This decision converted a logic flaw into total system compromise.


The Double Evaluation Vulnerability

The critical flaw:

template = f"Patient {first} {last} ..."
return eval(f"f'''{template}'''")

Let’s break down why this is catastrophic.

Stage 1 — String Interpolation

User-controlled input is inserted into template.

If the input contains {expression}, it survives as literal braces.

Stage 2 — Dynamic f-string Evaluation

The code then executes:

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

This converts the template into a new f-string and evaluates it.

Any {} expressions inside the string are executed as Python code.

This is not simply string formatting.

It is a code execution engine.


Why the Regex Filter Failed

The developer attempted protection via:

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

This blocked:

  • Spaces

  • Semicolons

  • Shell metacharacters

But it allowed:

  • Alphanumeric characters

  • Parentheses

  • Curly braces

  • +, /, =

The mistake was conceptual:

Filtering characters does not remove capability.

If the interpreter is available, an attacker only needs syntax — not shell metacharacters.

Base64 encoding fits perfectly within the allowed character set. A command can be encoded and decoded inside Python before execution.

The filter was syntactically strict — but semantically irrelevant.


The Final Escalation

By injecting a Python expression inside {}:

  • The root-owned process executed arbitrary code

  • A privileged binary was created

  • A root shell was obtained

This was not a clever trick.

It was the inevitable result of:

  1. eval() on user-controlled data

  2. Running the service as root

  3. Assuming regex validation equals safety


Where This Architecture Failed

Let’s step back.

The exploit chain required five independent weaknesses:

  1. Internet-facing deserialization

  2. Unsafe object unmarshalling

  3. Credential reuse & exposure

  4. Root-owned auxiliary service

  5. Dynamic evaluation of user input

Any single corrective measure would have broken the chain.

This is why defense in depth matters.


Preventing This Class of Failure

1. Never Expose Integration Engines Directly

Place integration engines behind:

  • Reverse proxies

  • Authentication gateways

  • Network segmentation

They are orchestration systems, not edge services.


2. Eliminate Unsafe Deserialization

  • Use strict class allowlists in XStream

  • Disable arbitrary object graph reconstruction

  • Validate XML against strict schemas


3. Remove eval() and Dynamic Execution

There is no safe justification for:

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

Use:

  • Proper templating engines

  • Explicit formatting

  • Static evaluation paths

And enforce static analysis rules that reject eval() in CI.


4. Enforce Least Privilege

Even if the Python flaw existed:

  • Running as a non-root service account

  • With no write access to privileged paths

  • Without SUID capabilities

would have prevented total compromise.

Privilege separation is the final safety net.


The Real Lesson

This breach did not succeed because of a single exploit.

It succeeded because:

  • Powerful systems were exposed at the boundary

  • Unsafe libraries were trusted

  • Privilege boundaries were ignored

  • Dangerous functions were left in production

Security failures compound.

Exploit chains are rarely clever.

They are usually inevitable.


Final Reflection

The most important takeaway is not the payload.

It is the pattern:

  • Deserialization + high privilege

  • Dynamic evaluation + user input

  • Filtering instead of eliminating capability

  • Operational shortcuts over architectural discipline

When these patterns appear together, compromise is not a question of “if.”

It is a question of “when.”