Programming Languages and Serialization CVEs
Taking a look at a recent critical SolarWinds CVE

I’ve written a number of articles in the past about the SolarWinds breach in 2020 which was the largest breach of US government systems at the time. The breach stemmed from a malicious implant into SolarWinds systems. It had a widespread impact because so many large companies used that product to monitor systems and networks, and attackers gained access to credentials with administrative privileges. They used those privileges to grant themselves administrative access in other places to maintain persistent access.
https://medium.com/cloud-security/solar-winds-breach-eae3ca6773d
Any time I see another vulnerability come out in the SolarWinds systems it catches my eye. Recently I read about a critical CVE that involved a serialization flaw in a SolarWinds product and wanted to take a look at it in a bit more detail.
Serialization vulnerabilities in SolarWinds Web Help Desk (WHD)
In the January 2026 security update for Web Help Desk (WHD), two of the six disclosed vulnerabilities specifically involve the deserialization of untrusted data.
CVE-2025-40551 (CVSS 9.8): A critical vulnerability where the AjaxProxy component improperly handles user-supplied data. This allows unauthenticated attackers to bypass security filters by placing “allowed” terms at the beginning of a JSON payload, ultimately leading to Remote Code Execution (RCE) with system-level privileges.
CVE-2025-40553 (CVSS 9.8): A second critical deserialization flaw that similarly enables unauthenticated RCE. This vulnerability allows attackers to run arbitrary OS commands by exploiting unsafe data processing within the application.
Watch for updates to the know exploited vulnerabilities catalog because often these types of vulnerabilities are added within days of their release.
https://www.cisa.gov/known-exploited-vulnerabilities-catalog
Serialization
To understand the threat posed by these vulnerabilities we first need to understand serialization and deserialization. Computers store complex data in memory in applications in different ways. One way is in objects that include data and actions related to a particular component in the application code.
For example, in Java you might have a class that represents some data. When the application runs that class is used to instantiate an object. That’s a fancy way of saying the class is used as a template to create a system component of a particular type. When the application needs to send the data represented by that object over the network to some other component, a Java application may serialize it. The component on the other side turns it back into a Java object (deserializes it) so it can use it in that application.
Here’s an example of a Java Class in an application.
import java.io.Serializable;
public class MaintenanceTask implements Serializable {
private String taskName;
private String targetServer;
public MaintenanceTask(String taskName, String targetServer) {
this.taskName = taskName;
this.targetServer = targetServer;
}
// This method is called automatically during deserialization
// THIS IS THE DANGER ZONE FOR ATTACKERS
private void readObject(java.io.ObjectInputStream in) throws Exception {
in.defaultReadObject(); // Standard unpacking
System.out.println("Executing task: " + taskName + " on " + targetServer);
// A real app might trigger a script here based on 'taskName'
}
}The serialized object may be sent over the network using a network socket in enterprise applications using a class like this:
import java.io.ObjectOutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
try (Socket socket = new Socket("helpdesk.internal", 1234);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
// Send the request to the server
MaintenanceTask task = new MaintenanceTask("CleanTempFiles", "Server-01");
out.writeObject(task);
} catch (Exception e) { e.printStackTrace(); }
}
}On the network socket, Java serialized data is binary, meaning it's not human-readable like JSON or HTML. I wrote about the importance of understanding binary, hex, dissecting packets, etc. if you work in cybersecurity in these posts on Cybersecurity Math:
When the data is serialized it might look something like this on the network:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII
00000000 AC ED 00 05 73 72 00 0F 4D 61 69 6E 74 65 6E 61 ..í..sr..Maintena
00000010 6E 63 65 54 61 73 6B B1 4C 73 4F E5 CD 33 F4 02 nceTask±LsOåÍ3ô.
00000020 00 02 4C 00 08 74 61 73 6B 4E 61 6D 65 74 00 12 ..L..taskNamet..
00000030 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E Ljava/lang/Strin
00000040 67 3B 4C 00 0C 74 61 72 67 65 74 53 65 72 76 65 g;L..targetServe
00000050 72 71 00 7E 00 01 78 70 74 00 0E 43 6C 65 61 6E rq.~..xpt..Clean
00000060 54 65 6D 70 46 69 6C 65 73 74 00 09 53 65 72 76 TempFilest..Serv
00000070 65 72 2D 30 31 er-01When the packets reach the other endpoint on the socket that serialized data gets converted back into a Java object that the other application can use. The vulnerability happens on the server side. The server has a listener waiting for any object:
java
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // <--- CRITICAL FAILURE POINTThe deserialization flaw
The problem occurs when the recipient component that does not properly inspect and validate the data passed to it. An attacker may be able to insert some malicious input that is accepted and processed by the recipient in a manner that allows attackers to run commands on the system. If the system is trying to pass an object with executable functions over the network and an attacker can replace a valid object with their own malicious object and the recipient component accepts the malicious object, the attackers may be able to get the program to execute functions in that object which perform the attackers desired actions.
In the recent SolarWinds AjaxProxy flaws, attackers used replaced the valid class names with "gadgets" like org.apache.commons.collections.functors.InvokerTransformer. By modifying the values in network packets, they forced the server to execute arbitrary OS commands instead of harmless strings.
The automagic problem in Java and gadgets
When the server calls readObject(), it triggers the attacker’s hidden code before the server even realizes it’s not a valid object. This is exactly how the AjaxProxy vulnerability in WHD worked—the application blindly “unpacked” data from a network socket.
That happens because of how Java’s “unpacking” engine works. Java has special “magic methods” like readObject(), readExternal(), and readResolve(). If a class has one of these, Java must run it during the unpacking process to restore the object’s state. In Java, readObject() doesn’t just read data; it reconstructs a living object. In the process an attacker may be able to find a way to get the application to execute code stored within the object.
To get code to execute, an attacker finds a “Gadget” - a legitimate class already present on the recipient server - that they can leverage to perform some action. For example, there may be a function that opens a network connection. They trigger the legitimate code to carry out their malicious command.
With Java Serialization, the attackers aren’t actually send “code” (compiled bytes). They send state (data). The “code” that runs is already part of the Gadget class on the victim’s server. The attacker provides data that tricks that existing code into calling a dangerous function like java.lang.Runtime.exec() passing in parameters that help them execute their desired commands.
Outer Layer: A perfectly “safe” object the server expects. The server is expecting a specific class, like MaintenanceTask. To get past the first "door," the attacker might wrap their trap inside a common Java container that the server has to open to see what's inside, like a java.util.HashMap or PriorityQueue.
Inner Layer: A “Gadget” class (the trap). In Java, an object can have "fields" (variables) that are other objects. An attacker takes a common, valid object and swaps out one of its normal fields for a malicious one. The malicious object contains the payload. The payload is triggered by some action the application has to take to process the data in the class and uses a gadget to perform some action on the server.
The Payload: Data designed to make the gadget to run some command like:
cmd.exe /c calcIn this example, cmd.exe /c calc is sent over as a string - some piece of data - but because of how it gets processed by the system it ends up executing on the victim’s server.
By sending “Data” that acts like code, attackers bypass most security filters.
Firewalls look for executable files (.exe, .bin). They don’t usually block the words “calc.exe” inside a data stream.
Java doesn’t think it’s running a new program; it thinks it’s just “filling in the blanks” of an object it already knows.
This is exactly why CVE-2025-40551 was so effective—the AjaxProxy took that “data” and handed it directly to a “Gadget” that knew how to turn text into action.
Remote Code Execution (RCE)
The reason this CVE is particularly dangerous is due to the fact that attackers can get remote code execution (RCE) using this vulnerability. That means they can run commands on your system. The damage attackers can do at that point depends on the privileges that system has once compromised.
Can it create new users? Can it create new malicious files or attack other systems on the network? In help desk software, can it insert a new ticket to trick someone into resetting a password to something an attacker knows, calling an attacker, or performing some other unwanted action in your environment by impersonating a high-level executive?
Any time a vulnerability leads to RCE it generally should be dealt with and fixed immediately. Even if it seems like the system is in a locked down network or sandbox, the risk of escaping that contained environment is generally not worth taking a chance that could happen. The results may be devastating for an organization.
Using network scanners to try to stop the attack
One way to try to stop the attack would be to scan the packets for potentially malicious contents. Some people have set up Yara rules in various security monitoring tools to scan for a magic number that indicates a serialized Java object.
Every Java serialized object must start with the hex bytes AC ED 00 05 (often represented in text as ¬í ..). If a server receives data starting with these bytes, it triggers the ObjectInputStream to start reconstructing—or "re-animating"—an object.
Google AI claims these are the most commonly used scanners that leverage Yara rules these days. I have been out of the SOC business for a while since getting certified in such things so I don’t really know if this is accurate - and I am not in the business of recommending tools so do your own research.
CrowdStrike & SentinelOne (EDR): These are the most common tools for "live" protection. They use YARA or YARA-like logic to scan files as they are written to a disk. For example, CrowdStrike Falcon integrates custom rules to catch the rO0 (Base64 for AC ED) signature in real-time.
Velociraptor (Forensics): This has become the most common open-source choice for "hunting". If you want to know if any of your 5,000 servers have a malicious SolarWinds payload, you push a YARA rule via Velociraptor and get the answer across your entire fleet in minutes.
THOR (Compromise Assessment): This is the go-to tool for deep "point-in-time" scans. Security consultants commonly use THOR because it contains thousands of high-quality YARA rules specifically tuned to find advanced threats and "gadget chains" that automated EDRs might miss.
Suricata (Network): This is the most common way to block the exploit at the network level. It inspects packets as they fly by, looking for that AC ED header before the data even reaches the server. The problem with scanners
The problem with scanners is this - what if you need serialized objects? What if that is a valid thing your applications need to do? What if the attackers can trick or bypass the scanners (which happens all. the. time.)? Or even turn off the scanners? Scanners are really a reactive way to deal with security problems. Although they are very important and good to have in your environment, I’d rather use a more foolproof proactive approach.
Using scanners doesn’t really feel like the best way to prevent this problem. It seems like we need a more robust approach at the point applications are developed. How can we prevent serialization flaws at the root? Well, we’re using Java serialization which doesn’t really have a strict mechanism for ensuring executable code doesn’t get into the wrong places in our applications.
So that got me thinking about serialization and the root cause of these problems. At one point an executive at Oracle said they were going to completely get rid of serialization due to all the problems it causes. Well, there are reasons for sending these objects and data over the network, but perhaps there is a safer way to do it.
This is where the less experienced programmer who doesn’t want to declare types in their application or deal with strict rules starts to understand why those things exist. Sure it’s easier not to use them but this is why you need to think about it - how can your application be tricked into doing something it is not supposed to do? The better you validate your data the harder it is for the attackers to find a flaw that lets them do such things.
How different programming languages handle serialization
Are some programming languages better than others at handling serialization flaws? I’ve written before about how different languages help you prevent various security problems. When Java came out it made buffer overflows difficult - on of the most prevalent sources of critical security problems at the time. Golang came along and helped prevent concurrency problems and race conditions that could lead to tricky application flaws and security problems. Any strongly typed language will help you ensure that your data passed into a particular application is what it is supposed to be. I wrote about those things in this series:
https://medium.com/cloud-security/secure-code-by-design-4ee8814021e6
When it comes to serialization, the clear winner is Rust. Rust helps with all of the above but it also helps prevent serialization flaws in a couple of different ways.
At compile time: Rust's compiler prevents the creation of "living objects" that can execute arbitrary code during reconstruction.
At deserialization time: Most Rust developers use Serde, a framework that requires you to explicitly define every field being unpacked. It does not "re-animate" code; it only populates data into pre-defined structures.
Golang avoids serialization problems with no magic methods and static mappings. It will ignore objects it doesn’t recognize.
Java has (used in the latest SolarWinds 2026.1 patches) allows developers to set a JEP 290 filter. This filter checks the “blueprint” of the object before the unpacking process starts. If it’s not on a pre-approved list, the server rejects it.
Alternatively, use static, non-executable data to pass information between systems. Regardless of the language, the OWASP Deserialization Cheat Sheet recommends moving away from language-specific formats to JSON or Protocol Buffers. These are “data-only” formats that cannot execute code, making them inherently safer for untrusted network traffic. That said - you have to make sure you are not passing executable code in your JSON object into something that will execute it or has a flaw that allows an attacker to inject code into your application.
Python’s pickle is vulnerable by design. It is even more dangerous than Java because it doesn’t even need a “Gadget Chain” to be exploited. Python has a “magic method” called __reduce__. If an attacker sends a pickled object containing this method, Python will execute whatever is inside it the moment pickle.load() is called.
In Swift, avoid the legacy NSCoding method and use Codable instead. The latter uses static data formats.
Avoid serialization in JavasScript and Node. Use static data formats instead.
node-serialize: A notorious library vulnerable to Remote Code Execution (RCE). It allows attackers to send an Immediately Invoked Function Expression (IIFE), which executes as soon as the data is unpacked.serialize-to-js: Similar tonode-serialize, this library usesnew Function()during reconstruction, which can be exploited to run arbitrary code if the input is untrusted.Prototype Pollution: This is a JavaScript-specific serialization risk where an attacker can modify the “blueprint” of all objects in the application, leading to a system-wide compromise. This occurs when an attacker uses a serialization flaw to inject properties into the
__proto__orprototypeof a base object.
If you see JSON.parse(), the application is likely safe from these flaws. If you see an import for node-serialize or funcster being used on a network request, it is a high-severity red flag
Ruby has a native serialization format called Marshal which has the same automagic trap as Java and pickle so should be avoided.
Ruby’s default YAML parser (Psych) was historically vulnerable to the same thing. If you called YAML.load() on untrusted data, it would recreate Ruby objects and trigger code execution.
Safer alternatives in Ruby include JSON.parse and YAML.safe_load.
As for .NET, Microsoft has essentially declared their own native binary serialization too dangerous to fix. Use strict mapping with no magic methods.
When it comes to C and C++ you’re on your own. There’s no native deserialization process and you can have much bigger problems in many different ways if you don’t know what you’re doing. This language is generally used by people who need extreme performance so it’s a whole different ballgame if you’re using C and C++. You’re responsible for checking every line of code and input to make sure nothing can go wrong - including serialization, memory problems like buffer overflows, concurrency problems and so on.
Separate your executable code from your data
In general, when you are designing a system, my recommendation is to separate your executable code from your data. Make sure your data cannot end up in some block of code that treats it like executable code. That’s why I don’t like the integration of executable code into things like AWS CloudFormation, which to me is data. Your CloudFormation template describes a state in which you want your infrastructure to exist. It’s not executable code. Let’s keep it that way.
All day long on a pentest most of what I am doing is trying to inject executable code into places the system will process that lets me perform some action I’m not supposed to be performing. There may be some logic flaws or the ability to insert some invalid data to get a desired result as well, but many, many flaws are a result of executable code passed as a piece of data ending up some place it shouldn’t that causes the system to execute it. Make sure your data is only data and always treated as data - not executable code.
Choose your programming languages and constructs wisely
Whenever you are choosing a programming language, consider the risks that language helps you defend against. If you are stuck with a legacy language or application, look for methods you can use to modernize it and minimize the risk of serialization security vulnerabilities.
Subscribe for more posts like this on Security Bugs.
—Teri Radichel


