Php rfc: new custom object serialization mechanism
Содержание:
- Deserializing
- Unserialize() under the hood
- Exploiting PHP deserialization
- Proposal
- Usage
- License
- Introduction
- Usage
- Data Transformation
- Types
- Serialize / Unserialize
- Serializing
- How it works
- How vulnerabilities happen in unserialize()
- Proposal
- API change
- Advanced: Enhancements
- API
- Contributing
- The details
- Prerequisites for the exploit
- How to protect against unserialize() vulnerabilities
- Intro to PHP object injection vulnerabilities
- Dealing with xsd:anyType or xsd:anySimpleType
- Serialization
Deserializing
When you are ready to operate on the object again, you can deserialize the string with unserialize().
<?phpclass User{public $username;public $status;}$user = new User;$user->username = 'vickie';$user->status = 'not admin';$serialized_string = serialize($user);$unserialized_data = unserialize($serialized_string);var_dump($unserialized_data);var_dump($unserialized_data);?>
Unserialize() under the hood
So how does unserialize() work under the hood? And why does it lead to vulnerabilities?
What are PHP magic methods?
PHP magic methods are function names in PHP that have “magical” properties. Learn more about them here.
The magic methods that are relevant for us now are __wakeup() and __destruct(). If the class of the serialized object implements any method named __wakeup() and __destruct(), these methods will be executed automatically when unserialize() is called on an object.
Step 1: Object instantiation
Instantiation is when the program creates an instance of a class in memory. That is what unserialize() does. It takes the serialized string, which specifies the class and the properties of that object. With that data, unserialize() creates a copy of the originally serialized object.
It will then search for a function named __wakeup(), and execute code in that function. __wakeup() reconstructs any resources that the object may have. It is used to reestablish any database connections that have been lost during serialization and perform other reinitialization tasks.
Step 2: Program uses the object
The program operates on the object and uses it to perform other actions.
Step 3: Object destruction
Finally, when no reference to the deserialized object instance exists, __destruct() is called to clean up the object.
Exploiting PHP deserialization
When you control a serialized object that is passed into unserialize(), you control the properties of the created object. You might also be able to hijack the flow of the application by controlling the values passed into automatically executed methods like __wakeup() or __destruct().
This is called a PHP object injection. PHP object injection can lead to variable manipulation, code execution, SQL injection, path traversal, or DoS.
Proposal
The proposal is to amend unserialize() function, allowing to either completely prohibit restoring objects or restrict the objects being restored to a whitelist of objects.
For this purpose, optional second parameter is added to unserialize(), which can take the following values:
- true — default value, allows all objects just as before
- false — no objects allowed
- array of class names, which list the allowed classes for unserialized objects (empty array here means the same as false)
If the class for the object is not allowed, the object is unserialized as an object of “incomplete class”, just as it is done in a case where object’s class does not exist. This means that the serialized data are roundtrip-safe with any settings, but with added security settings the unintended objects will not be accessible and their destructors and other methods will not be called.
Examples
// this will unserialize everything as before $data = unserialize($foo); // this will convert all objects into __PHP_Incomplete_Class object $data = unserialize($foo, false); // this will convert all objects except ones of MyClass and MyClass2 into __PHP_Incomplete_Class object $data = unserialize($foo, array("MyClass", "MyClass2"));
See API Update below.
Usage
How to serialize data
For serializing a Java object into a PHP serialization format string you
just use the static method Pherialize.serialize(). Just pass the object you
want to serialize to this method and you get a string in return which you
can then unserialize in PHP.
Example:
The result is printed to stdout and looks like this:
Now you can use this string in PHP to unserialize it back into a PHP array:
Result is a PHP array with exactly the data and types you have added to the
array with Java:
How to unserialize data
Let’s assume you have serialized the PHP array from the previous example and
you have stored this serialized string in the variable data. For unserializing and
printing the value you just have to use the Pherialize.unserialize() method.
Example:
The correct result printed to stdout is this:
License
Copyright (c) 2006 Klaus Reimer k@ailis.de
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the «Software»),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED «AS IS», WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
Introduction
What is serialization?
In the context of data storage, serialization is the process of translating data structures or object state into a format that can be stored (for example, in a file or memory buffer, or transmitted across a network connection link) and reconstructed later in the same or another computer environment.
Why not and ?
These native functions rely on having the serialized classes loaded and available at runtime and tie your unserialization process to a platform.
If the serialized string contains a reference to a class that cannot be instantiated (e.g. class was renamed, moved namespace, removed or changed to abstract) PHP will immediately die with a fatal error.
Is this a problem? Yes it is. Serialized data is now unusable.
Usage
Suppose that you have all XSD files in , first of all we need a configuration file
(as example ) that will keep all the namespace and directory mappings information.
# config.yml # Linux Users: PHP Namespaces use back slash \ rather than a forward slash / # So for destinations_php, the namespace would be TestNs\MyApp xsd2php: namespaces: 'http://www.example.org/test/': 'TestNs\MyApp' destinations_php: 'TestNs\MyApp': soap/src # 'TestNs\MyApp': soap\src # on Windows destinations_jms: 'TestNs\MyApp': soap/metadata # 'TestNs\MyApp': soap\metadata # on Windows # Uncomment this section if you want to have also symfony/validator metadata to be generated from XSD # destinations_validation: # 'TestNs\MyApp': soap/validation # 'TestNs\MyApp': soap\validation # on Windows aliases: # optional 'http://www.example.org/test/': MyCustomXSDType: 'MyCustomMappedPHPType' naming_strategy: short # optional and default path_generator: psr4 # optional and default # known_locations: # optional # "http://www.example.org/test/somefile.xsd": somefile.xsd # known_namespace_locations: # optional # "urn:veloconnect:catalog-1.1": xsd/catalog-1.1.xsd
Here is an explanation on the meaning of each parameter:
-
(required) defines the mapping between XML namespaces and PHP namespaces.
(in the example we have the XML namespace mapped to ) -
(required) specifies the directory where to save the PHP classes that belongs to
PHP namespace. (in this example classes will be saved into directory. -
(required) specifies the directory where to save JMS Serializer metadata files
that belongs to PHP namespace.
(in this example metadata will be saved into directory. -
(optional) specifies some mappings that are handled by custom JMS serializer handlers.
Allows to specify to do not generate metadata for some XML types, and assign them directly a PHP class.
For that PHP class is necessary to create a custom JMS serialize/deserialize handler. -
(optional) specifies the naming strategy to use when converting XML names PHP classes.
-
(optional) specifies the strategy to use for path generation and file saving
-
(optional) override remote location with a local file.
-
(optional) Specify schema location by namespace.
This can be used to read schemas which import namespaces but do not specify schemaLocation attributes.
Data Transformation
Transformer classes greatly differ from a class because these cannot as all class references are lost in the process of transformation.
To obtain transformations instead of the class usage of is required.
The Serializer library comes with a set of defined Transformers that implement the .
Usage is as simple as before, pass a Transformer as a .
For instance:
//...same as before ... $serializer = new DeepCopySerializer(new JsonTransformer()); echo $serializer->serialize($post);
Following, there are some examples and its output, given the object as data to be Transformed.
Array Transformer
NilPortugues\Serializer\Transformer\ArrayTransformer
array( 'postId' => 9, 'title' => 'Hello World', 'content' => 'Your first post', 'author' => array( 'userId' => 1, 'name' => 'Post Author', ), 'comments' => array( => array( 'commentId' => 1000, 'dates' => array( 'created_at' => '2015-07-18T12:13:00+02:00', 'accepted_at' => '2015-07-19T00:00:00+02:00', ), 'comment' => 'Have no fear, sers, your king is safe.', 'user' => array( 'userId' => 2, 'name' => 'Barristan Selmy', ), ), ), );
Flat Array Transformer
NilPortugues\Serializer\Transformer\FlatArrayTransformer
array( 'postId' => 9, 'title' => 'Hello World', 'content' => 'Your first post', 'author.userId' => 1, 'author.name' => 'Post Author', 'comments.0.commentId' => 1000, 'comments.0.dates.created_at' => '2015-07-18T12:13:00+02:00', 'comments.0.dates.accepted_at' => '2015-07-19T00:00:00+02:00', 'comments.0.comment' => 'Have no fear, sers, your king is safe.', 'comments.0.user.userId' => 2, 'comments.0.user.name' => 'Barristan Selmy', );
XML Transformer
NilPortugues\Serializer\Transformer\XmlTransformer
<?xml version="1.0" encoding="UTF-8"?> <data> <postId type="integer">9</postId> <title type="string">Hello World</title> <content type="string">Your first post</content> <author> <userId type="integer">1</userId> <name type="string">Post Author</name> </author> <comments> <sequential-item> <commentId type="integer">1000</commentId> <dates> <created_at type="string">2015-07-18T12:13:00+02:00</created_at> <accepted_at type="string">2015-07-19T00:00:00+02:00</accepted_at> </dates> <comment type="string">Have no fear, sers, your king is safe.</comment> <user> <userId type="integer">2</userId> <name type="string">Barristan Selmy</name> </user> </sequential-item> </comments> </data>
YAML Transformer
NilPortugues\Serializer\Transformer\YamlTransformer
title: 'Hello World' content: 'Your first post' author: userId: 1 name: 'Post Author' comments: - { commentId: 1000, dates: { created_at: '2015-07-18T12:13:00+02:00', accepted_at: '2015-07-19T00:00:00+02:00' }, comment: 'Have no fear, sers, your king is safe.', user: { userId: 2, name: 'Barristan Selmy' } }
Json Transformer
JsonTransformer comes in 2 flavours. For object to JSON transformation the following transformer should be used:
NilPortugues\Serializer\Transformer\JsonTransformer
Output
{ "postId": 9, "title": "Hello World", "content": "Your first post", "author": { "userId": 1, "name": "Post Author" }, "comments": }
If your desired output is for API consumption, you may like to check out the JsonTransformer library, or require it using:
$ composer require nilportugues/json
JSend Transformer
JSend Transformer has been built to transform data into valid JSend specification resources.
Please check out the JSend Transformer or download it using:
$ composer require nilportugues/jsend
JSON API Transformer
JSON API Transformer has been built to transform data into valid JSON API specification resources.
Please check out the JSON API Transformer or download it using:
$ composer require nilportugues/json-api
HAL+JSON Transformer
HAL+JSON Transformer has been built for HAL+JSON API creation. Given an object and a series of mappings a valid HAL+JSON resource representation is given as output.
Please check out the HAL+JSON API Transformer or download it using:
$ composer require nilportugues/haljson
Types
Because the data types in PHP are different to the types in Java conversion
is not always possible without switching to a different data type. If for
example you serialize a Java Byte then you will get a PHP int because PHP
does only knows the number types int and double. So when you unserialize this
int back into Java then you will end up with an Integer and not with a Byte.
The same problem comes up with Long objects. Pherialize serializes a Long
into a int if it fits an Integer. Otherwise it’s serialized into a double
To make life easier while unserializing PHP data into Java all PHP data
types are wrapped by an object of type Mixed.
From this object you can
convert the data easily in whatever you need (as long as the conversion makes
sense).
Here is a complete list of the conversions performed when serializing Java
data types and unserializing them in PHP:
Java class | PHP type |
---|---|
null | null |
Boolean | boolean |
Character | string |
Byte | int |
Short | int |
Integer | int |
Long | int or double (Depending on how large the value is) |
Float | double |
Double | double |
String | string |
Collection | array |
Map | array |
Serializable | object |
Enum | Value used as a string |
Mixed | Depends on the raw type in the Mixed wrapper |
And here is a list of performed conversion when you unserialize PHP
data in Java. But note that you will never get in touch with these
conversions because it’s completely hidden by the .
PHP type | Java class |
---|---|
null | null |
boolean | Boolean |
int | Integer |
double | Double |
string | String |
array | Map |
Serialize / Unserialize
vendor/bin/xsd2php convert:jms-yaml \ `/home/my/ota/OTA_HotelAvail*.xsd \ --ns-map='http://www.opentravel.org/OTA/2003/05;Mercurio/OTA/2007B/' \ --ns-dest='Mercurio/OTA/2007B/;src/Metadata/JMS;' \ --alias-map='http://www.opentravel.org/OTA/2003/05;CustomOTADateTimeFormat;Vendor/Project/CustomDateClass'
What about namespaces?
http://www.opentravel.org/OTA/2003/05 will be converted into Mercurio/OTA/2007B PHP namespace
Where place the files?
http://www.opentravel.org/OTA/2003/05 will be placed into src/Metadata/JMS directory
What about custom types?
-
Add xsd2php dependency to satisfy BaseTypesHandler and XmlSchemaDateHandler.
"require" : { "goetas-webservices/xsd2php-runtime":"^0.2.2", }
use JMS\Serializer\SerializerBuilder; use JMS\Serializer\Handler\HandlerRegistryInterface; use GoetasWebservices\Xsd\XsdToPhpRuntime\Jms\Handler\BaseTypesHandler; use GoetasWebservices\Xsd\XsdToPhpRuntime\Jms\Handler\XmlSchemaDateHandler; $serializerBuilder = SerializerBuilder::create(); $serializerBuilder->addMetadataDir('metadata dir', 'DemoNs'); $serializerBuilder->configureHandlers(function (HandlerRegistryInterface $handler) use ($serializerBuilder) { $serializerBuilder->addDefaultHandlers(); $handler->registerSubscribingHandler(new BaseTypesHandler()); // XMLSchema List handling $handler->registerSubscribingHandler(new XmlSchemaDateHandler()); // XMLSchema date handling // $handler->registerSubscribingHandler(new YourhandlerHere()); }); $serializer = $serializerBuilder->build(); // deserialize the XML into Demo\MyObject object $object = $serializer->deserialize('<some xml/>', 'DemoNs\MyObject', 'xml'); // some code .... // serialize the Demo\MyObject back into XML $newXml = $serializer->serialize($object, 'xml');
Serializing
When you need to store a PHP object or transfer it over the network, you use serialize() to pack it up.
serialize(): PHP object -> plain old string that represents the obj
When you need to use that data, use unserialize() to unpack and get the underlying object.
unserialize(): string containing object data -> original object
For example, this code snippet will serialize the object “user”.
<?phpclass User{public $username;public $status;}$user = new User;$user->username = 'vickie';$user->status = 'not admin';echo serialize($user);?>
Run the code snippet, and you will get the serialized string that represents the “user” object.
O:4:"User":2:{s:8:"username";s:6:"vickie";s:6:"status";s:9:"not admin";}
How it works
unserialize() in a (very big) nutshell.
Step 0: What are PHP magic methods?
PHP magic methods are function names in PHP that have “magical” properties. Learn more about them here.
The magic methods that are relevant for us now are __wakeup() and __destruct(). If the class of the serialized object implements any method named __wakeup() and __destruct(), these methods will be executed automatically when unserialize() is called on an object.
Step 0.1: Unserialize prerequisites
When you serialize an object in PHP, serialize() will save all properties in the object. But it will not store the methods of the class of the object, just the name of the class.
Thus, in order to unserialize() an object, the class of the object will have to be defined in advance (or be autoloaded). That is, the definition of the class needs to be present in the file that you unserialize() the object in.
If the class is not already defined in the file, the object will be instantiated as __PHP_Incomplete_Class, which has no methods and the object will essentially be useless.
Step 1: Object instantiation
Instantiation is when the program creates an instance of a class in memory. And that is what unserialize() does. It takes the serialized string, which specifies the class of the object to be created and the properties of that object. With that data, unserialize() creates a copy of the originally serialized object.
It will then search for a function named __wakeup(), and execute code in that function if it is defined for the class. __wakeup() is called to reconstruct any resources that the object may have. It is often used to reestablish any database connections that may have been lost during serialization and perform other reinitialization tasks.
Step 2: Program uses the object
The program can then operate on the object and use it to perform other actions.
Step 3: Object destruction
Finally, when no reference to the deserialized object instance exists, __destruct() is called.
How vulnerabilities happen in unserialize()
When an attacker controls a serialized object that is passed into unserialize(), she can control the properties of the created object. This will then allow her the opportunity to hijack the flow of the application, by controlling the values passed into automatically executed methods like __wakeup().
This is called a PHP object injection. PHP object injection can lead to code execution, SQL injection, path traversal or DoS, depending on the context in which it happened.
For example, consider this vulnerable code snippet: (taken from https://www.owasp.org/index.php/PHP_Object_Injection)
class Example2{ private $hook; function __construct() { // some PHP code... } function __wakeup() { if (isset($this->hook)) eval($this->hook); }}// some PHP code...$user_data = unserialize($_COOKIE);// some PHP code...
An attacker can achieve RCE using this deserialization flaw because a user-provided object is passed into unserialize, and the class Example2 has a magic function that runs eval() on user-provided input.
To exploit this RCE, the attacker simply has to set her data cookie to a serialized Example2 object with the hook property set to whatever PHP code that she wants executed. She can generate the serialized object using the following code snippet:
class Example2{ private $hook = "phpinfo();";}print urlencode(serialize(new Example2));
In this case, passing the above-generated string into the data cookie will cause phpinfo() to be executed. Once the attacker passes the serialized object into the program, the following is what will happen in detail:
- The attacker passes a serialized Example2 object into the program as the data cookie.
- The program calls unserialize() on the data cookie.
- Because the data cookie is a serialized Example2 object, unserialize() instantiates a new Example2 object.
- unserialize() sees that the Example2 class has __wakeup() implemented, so __wakeup() is called.
- __wakeup() looks for the $hook property of the object, and if it is not NULL, it runs eval($hook).
- $hook is not NULL, and is set to “phpinfo();”, so eval(“phpinfo();”) is run.
- RCE is achieved.
Proposal
The proposed serialization mechanism tries to combine the generality of with the implementation approach of /.
Two new magic methods are added:
// Returns array containing all the necessary state of the object. public function __serialize() array; // Restores the object state from the given data array. public function __unserialize(array $data) void;
The usage is very similar to the interface. From a practical perspective the main difference is that instead of calling inside , you directly return the data that should be serialized as an array.
The following example illustrates how / are used, and how they compose under inheritance:
class A { private $prop_a; public function __serialize() array { return "prop_a" => $this->prop_a; } public function __unserialize(array $data) { $this->prop_a = $data"prop_a"; } } class B extends A { private $prop_b; public function __serialize() array { return "prop_b" => $this->prop_b, "parent_data" => parent::__serialize(), ; } public function __unserialize(array $data) { parent::__unserialize($data"parent_data"); $this->prop_b = $data"prop_b"; } }
This resolves the issues with by leaving the actual serialization and unserialization to the implementation of the serializer. This means that we don’t have to share the serialization state anymore, and thus avoid issues related to backreference ordering. It also allows us to delay calls to the end of unserialization.
Encoding and interoperability
The and methods reuse the serialization format used by ordinary object serialization, as well as /. This means that the data array returned by will be stored as-if it represented object properties.
In principle, this makes existing strings serialized in format fully interoperable with the new serialization mechanism, the data is just provided in a different way (for in properties, for as an explicit array). If a class has both and , then the latter will be preferred. If a class has both and then the latter will be preferred.
If a class both implements and /, then serialization will prefer the new mechanism, while unserialization can make use of either, depending on whether the (Serializable) or (__unserialize) format is used. As such, old serialized strings encoded in format can still be decoded, while new strings will be produced in format.
Magic methods vs interface
This RFC proposes the addition of new magic methods, but using an interface instead would also be possible, though it will require some naming gymnastics to avoid .
This proposal uses magic methods for two reasons. First, they interoperate well. and can be added to a class without compatibility concerns: They will be used on PHP 7.4 or newer and ignored on PHP 7.3 or older. Using an interface instead requires either raising the version requirement to PHP 7.4, or dealing with the definition of a stub interface in a compatible manner.
Second, they are semantically more correct. In PHP all objects are serializable by default. The interface is a misnomer in that sense, because an object that does not implement can be (and usually is) still serializable. On the contrary, might be implemented specifically for the purpose of forbidding serialization, by throwing an exception. The magic methods and are just hooks to customize the serialization functionality, they do not determine whether an object can be serialized, and code should generally have no reason to check for their presence or absence.
Creating objects in __unserialize()
Some people have expressed a desire to make a static method which creates and returns the unserialized object (rather than first constructing the object and then calling to initialize it).
This would allow an even greater degree of control over the serialization mechanism, for example it would allow to return an already existing object from .
However, allowing this would once again require immediately calling functions (interleaved with unserialization) to make the object available for backreferences, which would reintroduce some of the problems that suffers from. As such, this will not be supported.
API change
After some thought and discussion, I have decided to slightly change the API:
// this will unserialize everything as before $data = unserialize($foo); // this will convert all objects into __PHP_Incomplete_Class object $data = unserialize($foo, "allowed_classes" => false); // this will convert all objects except ones of MyClass and MyClass2 into __PHP_Incomplete_Class object $data = unserialize($foo, "allowed_classes" => "MyClass", "MyClass2"); //accept all classes as in default $data = unserialize($foo, "allowed_classes" => true);
This will allow to extend the options array in the future if we ever want to add more parameters. No objections were voiced on the list regarding this API change.
Advanced: Enhancements
Fast destruct
PHPGGC implements a () flag, that will make sure your serialized object will be destroyed right after the call, and not at the end of the script. I’d recommend using it for every vector, as it improves reliability. For instance, if PHP script raises an exception after the call, the method of your object might not be called. As it is processed at the same time as encoders, it needs to be set first.
ASCII Strings
Uses the serialization format instead of the standard . This replaces every non-ASCII value to an hexadecimal representation:
->
This can be useful when for some reason non-ascii characters are not allowed (NULL BYTE for instance). Since payloads generally contain them, this makes sure that the payload consists only of ASCII values.
Note: this is experimental and it might not work in some cases.
Plus Numbers
Sometimes, PHP scripts verify that the given serialized payload does not contain objects by using a regex such as . This is easily bypassed using instead of . One can use , or , to automatically add these signs in front of symbols.
For instance, to obfuscate objects and strings, one can use: . Please note that since PHP 7.2, only i and d (float) types can have a +.
API
Instead of using PHPGGC as a command line tool, you can program PHP scripts:
<?php # Include PHPGGC include("phpggc/lib/PHPGGC.php"); # Include guzzle/rce1 $gc = new \GadgetChain\Guzzle\RCE1(); # Always process parameters unless you're doing something out of the ordinary $parameters = $gc->process_parameters(); # Generate the payload $object = $gc->generate($parameters); # Most (if not all) GC's do not use process_object and process_serialized, so # for quick & dirty code you can omit those two $object = $gc->process_object($object); # Serialize the payload $serialized = serialize($object); $serialized = $gc->process_serialized($serialized); # Display it print($serialized . "\n"); # Create a PHAR file from this payload $phar = new \PHPGGC\Phar\Tar($serialized); file_put_contents('output.phar.tar', $phar->generate());
This allows you to tweak the parameters or write exploits more easily.
Note: This is pretty experimental at the moment, so please, report bugs.
Contributing
Pull requests are more than welcome. Please follow these simple guidelines:
- is always the best vector
- Specify at least the version of the library you’ve built the payload on
- Refrain from using references unless it is necessary or drastically reduces the size of the payload. If the payload is modified by hand afterwards, this might cause problems.
- Do not include unused parameters in the gadget definition if they keep their default values. It just makes the payload bigger.
Codewise, the directory structure is fairly straightforward: gadgets in gadgets.php, description + logic in chain.php.
You can define pre- and post- processing methods, if parameters need to be modified.
Hopefully, the already implemented gadgets should be enough for you to build yours.
Otherwise, I’d be glad to answer your questions.
The command-line option can be used to create the directory and file structure for a new gadget chain.
For instance, use would create a new Drupal RCE gadgetchain.
The details
According to PHP docs, unserialize() “creates a PHP value from a stored representation”, and ”takes a single serialized variable and converts it back into a PHP value”.
It takes two parameters: str and options. str is the parameter containing the serialized string waiting to be deserialized. options is the array containing the options that control certain function behaviors. In unserialize() particularly, the only valid user-defined option is allowed_classes. allowed_classes specify the class names that should be accepted.
We’ll dive into allowed_classes further, but essentially, when unserialize() encounters an object of a class that isn’t to be accepted, then the object will be instantiated as __PHP_Incomplete_Class instead.
Prerequisites for the exploit
There are a few conditions that have to be met for this to be exploitable. Let’s look at this chart again, as it points to important exploit prerequisites:
In order for an attacker to exploit an insecure unserialize() function, two conditions must be met:
- The class of the deserialized object needs to be defined (or autoloaded) and needs to be one of the allowed classes.
- The class of the object must implement some magic method that allows an attacker to inject code into.
And there you have it! That’s how unserialize() leads to dangerous vulnerabilities.
How to protect against unserialize() vulnerabilities
In order to prevent PHP object injections from happening, it is recommended to never pass untrusted user input into unserialize(). Consider using JSON to pass serializes data to and from the user. And if you do need to pass untrusted, serialized data into unserialize(), be sure to implement rigorous data validation in order to minimize the risk of a critical vulnerability.
Mitigation advice is taken from php.net. Read more about the unserialize() function here:
Intro to PHP object injection vulnerabilities
Vickie LiFollow
May 17 · 5 min read
PHP: Hypertext Preprocessor | Logo by Colin Viebrock on php.net
Serialization is when an object in a programming language (say, a Java or PHP object) is converted into a format that can be stored or transferred. Whereas deserialization refers to the opposite: it’s when the serialized object is read from a file or the network and converted back into an object.
Insecure deserialization vulnerabilities happen when applications deserialize objects without proper sanitization. An attacker can then manipulate serialized objects to change the program’s flow.
Today, let’s talk about PHP object injections. They are insecure deserialization vulnerabilities that happen when developers deserialize PHP objects recklessly.
Dealing with xsd:anyType or xsd:anySimpleType
If your XSD contains or types you have to specify a handler for this.
When you generate the JMS metadata you have to specify a custom handler:
bin/xsd2php.php convert:jms-yaml \ ... various params ... \ --alias-map='http://www.w3.org/2001/XMLSchema;anyType;MyCustomAnyTypeHandler' \ --alias-map='http://www.w3.org/2001/XMLSchema;anyType;MyCustomAnySimpleTypeHandler' \
Now you have to create a custom serialization handler:
use JMS\Serializer\XmlSerializationVisitor; use JMS\Serializer\XmlDeserializationVisitor; use JMS\Serializer\Handler\SubscribingHandlerInterface; use JMS\Serializer\GraphNavigator; use JMS\Serializer\VisitorInterface; use JMS\Serializer\Context; class MyHandler implements SubscribingHandlerInterface { public static function getSubscribingMethods() { return array( array( 'direction' => GraphNavigator::DIRECTION_DESERIALIZATION, 'format' => 'xml', 'type' => 'MyCustomAnyTypeHandler', 'method' => 'deserializeAnyType' ), array( 'direction' => GraphNavigator::DIRECTION_SERIALIZATION, 'format' => 'xml', 'type' => 'MyCustomAnyTypeHandler', 'method' => 'serializeAnyType' ) ); } public function serializeAnyType(XmlSerializationVisitor $visitor, $data, array $type, Context $context) { // serialize your object here } public function deserializeAnyType(XmlDeserializationVisitor $visitor, $data, array $type) { // deserialize your object here } }
Serialization
For the serializer to work, all you need to do is pass in a PHP Object to the serializer and a Strategy to implement its string representation.
Serializers (JSON, XML, YAML)
- NilPortugues\Serializer\JsonSerializer
- NilPortugues\Serializer\XmlSerializer
- NilPortugues\Serializer\YamlSerializer
Example
In the following example a object is serialized into JSON.
Code
use NilPortugues\Serializer\Serializer; use NilPortugues\Serializer\Strategy\JsonStrategy; //Example object $post = new Post( new PostId(9), 'Hello World', 'Your first post', new User( new UserId(1), 'Post Author' ), [ new Comment( new CommentId(1000), 'Have no fear, sers, your king is safe.', new User(new UserId(2), 'Barristan Selmy'), [ 'created_at' => (new DateTime('2015/07/18 12:13:00'))->format('c'), 'accepted_at' => (new DateTime('2015/07/19 00:00:00'))->format('c'), ] ), ] ); //Serialization $serializer = new JsonSerializer(); $serializedObject = $serializer->serialize($post); //Returns: true var_dump($post == $serializer->unserialize($serializedObject)); echo $serializedObject;
The object, before it’s transformed into an output format, is an array with all the necessary data to be rebuild using unserialize method.
Output
{ "@type": "Acme\\\\Domain\\\\Dummy\\\\Post", "postId": { "@type": "Acme\\\\Domain\\\\Dummy\\\\ValueObject\\\\PostId", "postId": { "@scalar": "integer", "@value": 14 } }, "title": { "@scalar": "string", "@value": "Hello World" }, "content": { "@scalar": "string", "@value": "Your first post" }, "author": { "@type": "Acme\\\\Domain\\\\Dummy\\\\User", "userId": { "@type": "Acme\\\\Domain\\\\Dummy\\\\ValueObject\\\\UserId", "userId": { "@scalar": "integer", "@value": 1 } }, "name": { "@scalar": "string", "@value": "Post Author" } }, "comments": { "@map": "array", "@value": } }'
Custom Serializers
If a custom serialization strategy is preferred, the class should be used instead. A must implement the .
Usage is as follows:
use NilPortugues\Serializer\Serializer; use NilPortugues\Serializer\Strategy\CustomStrategy; $serializer = new Serializer(new CustomStrategy()); echo $serializer->serialize($post);