PHP反序列化相关漏洞总结[CVE-2016-7124、SugarCRM、session 反序列化、HITCON 2016]

在做题中,遇见了不止一次反序列化相关的内容,在这里做一个总结。

serialize / unserialize

编程语言一般都会提供序列化和反序列化的功能,从而使得对象和字符串进行相互转换,从而完成持久化,PHP提供了serializeunserialize,python提供了pickle.dumps()pickle.loads(),以PHP为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class item{
private $date;
private $price;
private $name;
private $a=array();
public function __construct($name) {
$this->name = $name;
}
function set_price($price){
$this->price = $price;
}
function set_array($args){
$this->a=$args;
}
}
$i = new item('apple');
$i->set_array(array('s1','s2','s3'));
echo serialize($i);

运行后可以看到结果:

1
O:4:"item":4:{s:10:"itemdate";N;s:11:"itemprice";N;s:10:"itemname";s:5:"apple";s:7:"itema";a:3:{i:0;s:2:"s1";i:1;s:2:"s2";i:2;s:2:"s3";}}

仔细的人会发现,itemdate部分为什么长度是10,不应该是8吗?,这一点非常坑,我们把它用文件输出然后拖到hex里面看看,可以发现,其类名和变量名之前都添加了%00字符,而在终端中是没办法赋值%00字符的,导致问题的出现,所以对于反序列化的操作,最好放到文件中,不要从终端直接操作,如果谁有可以复制的,请mail推荐给我! iterm2好像不行:

字段内容

当然,支持多种类型的PHP变量integers / floats / boolean/ strings / array / objects具体的字段含义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[boolean]
b:;
b:1; // True
b:0; // False
[int]
i:;
i:1; // 1
i:-3; // -3
[object]
O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}
其中object size 等于对象的变量数
[NULL]
N; //NULL
[string]
s:5:"hello"; //s:size:value;
[array]
a:3:{s"key1";s"value1";s"value2";} //a:size:{key, value pairs};

要从字符串中反序列化得到object只需要调用unserialize($str)即可。

反序列化漏洞一般是对输入进行反序列化$obj=unserialize($_GET['object']),但仅仅将字符串反序列化为对象并没有什么用,还需要利用魔术方法或者其他敏感函数进行恶意操作

__sleep & __wakeup

两个魔术方法,当一个对象被序列化时,PHP会调用__sleep方法(如果有的话),这个功能可以用于清理对象,并返回一个包含对象中变量名称的数组,例如下面例子,我们在序列化的过程中,清楚掉$id字段内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class item{
private $id;
private $name;
public function __construct($name) {
$this->name = $name;
$this->id = uniqid();
}
function __sleep(){
return array('name');
}
}
$i = new item('apple');
echo serialize($i);
?>
结果为:
O:4:"item":1:{s:10:"itemname";s:5:"apple";}
可以看到id信息被抹去了

而同理,__wakeup用于反序列化的相关操作,比如数据库重新连接,变量初始化等内容。

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
<?php
class item{
private $id;
private $name;
public function __construct($name) {
$this->name = $name;
$this->id = uniqid();
}
function __sleep(){
return array('name');
}
function __wakeup(){
$this->id = uniqid();
}
}
$i = new item('ban');
$str = serialize($i);
$obj = unserialize($str);
var_dump($obj);
?>
结果:
object(item)#2 (2) {
["id":"item":private]=>
string(13) "5ac4e794dc058"
["name":"item":private]=>
string(3) "ban"
}

可以看到,反序列化时,调用了__wakeup函数进行了变量赋值。

利用其他程序逻辑

比如,以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
class User
{
public $age = 0;
public $name = '';
public function __toString(){
return 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';
}
}
$obj = unserialize($_GET['evil']);
echo $obj;

在原始逻辑中对反序列化的对象进行了某些操作,然后造成可以利用。这种方式不仅限于魔术方法,还包括正常逻辑的函数。对于__toString()来说,不只只有echo $obj的时候才会触发,其他的例如字符串拼接、格式化字符串、字符串比较、in_array()等等,当然在针对对象的操作中,例如class_exists()、对象创建、对象执行、对象销毁都可能触发。

CVE-2016-7124

__wakeup一般用于数据反序列化的清理,CVE-2016-7124发现,在PHP低版本中,当输入的反序列化的对象个数大于真实的个数时,会使得__wakeup函数失效从而可以绕过,同时对象被销毁,执行__destruct().所以攻击者可以绕过__wakeup调用精心设计的__destruct()方法

影响版本:

PHP5 < 5.6.25
PHP7 < 7.0.10

首先看正常的反序列化内容结果:

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 User
{
public $age;
public $name;
function __construct($name){
$this->name = $name;
}
function __destruct(){
echo "destruct\n";
echo $this->name;
}
function __wakeup(){
echo "wakeup\n";
echo $this->name;
$this->name = "";
}
}
// $user = new User('test');
// $str = serialize($user);
// $file = fopen('file.txt','w');
// fwrite($file, $str);
$str = file_get_contents('file.txt');
$obj = unserialize($str);
var_dump( $obj);
?>

输出结果为:

1
2
3
4
5
6
7
8
9
wakeup
testobject(User)#1 (2) {
["age"]=>
NULL
["name"]=>
string(0) ""
}
destruct

我们可以看到__wakeup()调用的时候,对象已经被创建成功,然后对name字段进行清理,然后调用var_dump发现反序列化后的对象数据确实干净,最终程序执行完成退出时,调用__destruct()销毁对象。

我们将file.txt文件内容改为O:4:"User":3:{s:3:"age";N;s:4:"name";s:4:"test";} (属性个数2改为了3),我们看在不同版本php下的结果:

1
2
3
4
5
6
[php 5.4.45]
Notice: unserialize(): Unexpected end of serialized data in C:\phpStudy\PHPTutorial\WWW\index.php on line 27 destruct test Notice: unserialize(): Error at offset 48 of 49 bytes in C:\phpStudy\PHPTutorial\WWW\index.php on line 27 bool(false)
[php 7.1.7]
bool(false)

可以看到 绕过了__wakeup()的清理,并执行了__destruct()方法

SugarCRM v6.5.23 PHP反序列化漏洞

由于sugarCRM这个开源项目已经被删除了,所以没办法复现了…尴尬 那么就根据文章来学习一下好了
参考文章为这篇:SugarCRM v6.5.23 PHP反序列化 对象注入漏洞

具体的写的非常清晰,总结一下:

主要利用的是被反序列化字符串过滤正则的不完善,(其本身意图想过滤所有对象类型,但o:4->o:+4,使用加号可以绕过)和php低版本__wakeup()的绕过,最终寻找到一个__destruct()方法调用了写入文件,从而可以将payload恶意内容写入,使用的是SugarCacheFile对象

观察其补丁:

1
2
3
4
5
6
7
8
function sugar_unserialize($value)
{
preg_match('/[oc]:[^:]*\d+:/i', $value, $matches);
if (count($matches)) {
return false;
}
return unserialize($value);
}

更改了正则表达式,无法反序列化对象内容

HITCON 2016 web 审计

一道源码审计,基于注入和反序列化,源码如下:

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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<?php
include "config.php";
class HITCON{
private $method;
private $args;
private $conn;
public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
$this->__conn();
}
function show() {
list($username) = func_get_args();
$sql = sprintf("SELECT * FROM users WHERE username='%s'", $username);
$obj = $this->__query($sql);
if ( $obj != false ) {
$this->__die( sprintf("%s is %s", $obj->username, $obj->role) );
} else {
$this->__die("Nobody Nobody But You!");
}
}
function login() {
global $FLAG;
list($username, $password) = func_get_args();
$username = strtolower(trim(mysql_escape_string($username)));
$password = strtolower(trim(mysql_escape_string($password)));
$sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, $password);
if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
$this->__die("Orange is so shy. He do not want to see you.");
}
$obj = $this->__query($sql);
if ( $obj != false && $obj->role == 'admin' ) {
$this->__die("Hi, Orange! Here is your flag: " . $FLAG);
} else {
$this->__die("Admin only!");
}
}
function source() {
highlight_file(__FILE__);
}
function __conn() {
global $db_host, $db_name, $db_user, $db_pass, $DEBUG;
if (!$this->conn)
$this->conn = mysql_connect($db_host, $db_user, $db_pass);
mysql_select_db($db_name, $this->conn);
if ($DEBUG) {
$sql = "CREATE TABLE IF NOT EXISTS users (
username VARCHAR(64),
password VARCHAR(64),
role VARCHAR(64)
) CHARACTER SET utf8";
$this->__query($sql, $back=false);
$sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')";
$this->__query($sql, $back=false);
}
mysql_query("SET names utf8");
mysql_query("SET sql_mode = 'strict_all_tables'");
}
function __query($sql, $back=true) {
$result = @mysql_query($sql);
if ($back) {
return @mysql_fetch_object($result);
}
}
function __die($msg) {
$this->__close();
header("Content-Type: application/json");
die( json_encode( array("msg"=> $msg) ) );
}
function __close() {
mysql_close($this->conn);
}
function __destruct() {
$this->__conn();
if (in_array($this->method, array("show", "login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
} else {
$this->__die("What do you do?");
}
$this->__close();
}
function __wakeup() {
foreach($this->args as $k => $v) {
$this->args[$k] = strtolower(trim(mysql_escape_string($v)));
}
}
}
if(isset($_GET["data"])) {
@unserialize($_GET["data"]);
} else {
new HITCON("source", array());
}

好吧,代码有点长,我们一点点分析。

看完代码,发现flaglogin函数中,得到flag的方式,需要使得login方法中$obj不为空且为admin,但上面由于:

1
2
3
4
5
if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
$this->__die("Orange is so shy. He do not want to see you.");
}

看起来是矛盾的,这里发现php字符编码不是UTF-8,可以用Ą、Ã绕过。为了获得orange密码,可以发现在show函数中存在注入点,$username没有过滤,而在__wakeup()中对字符进行了转义,可以使用之前CVE的方式使其失效,然后刚好可以利用__destruct()进行执行。这样就得出了整道题的思路。

session反序列化相关

关于php session的资料网上有很多,可以去搜一下,这里总结一下相关字段的含义:

字段 含义 常见值 额外说明
Registered save handlers 支持的session存储类型 files user sqlite memcache 可保存为文件、用户定义、数据库或memcached(在内存中存储seesion)
Registered serializer handlers 支持的seesion序列环类型 php php_binary wddx php类型格式为:键值+|+序列化内容
php_serialize格式为:序列化内容
session.auto_start 是否自动创建session on/off on的时候不需要调用session_start(),自动添加
session.cache_expire 当前缓存的到期时间 180分钟
session.cache_limiter 缓存限制器 publicprivateprivate_no_expirenocache
session.cookie_domain cookie支持的域名
session.cookie_httponly cookie httponly
session.cookie_path cookie存储的相对路径 / session.save_path合用
session.cookie_secure 是否只有https的时候才加上cookie
session.entropy_file 设定session值的文件
session.entropy_length session值长度
session.gc_divisor 设置进程比率 1000
session.gc_maxlifetime (垃圾收集)被处理前的生存期 1440秒
session.gc_probability 垃圾收集的处理几率 1 默认千分之一的比率回收,概率=session.gc_probability/session.gc_divisor
session.hash_bits_per_character 定义当转换2进制hash数据为一些可读的数据时,每个字符存储多少个比特 5 5 比特: 0-9, a-v
session.hash_function 选择一个HASH函数,0为MD5(128比特强度),1为SHA-1(160比特强度) 0 默认MD5
session.name session名称 PHPSESSID 默认PHPSESSID
session.referer_check 包含有用来检查每个 HTTP Referer 的子串 no value 如果客户端发送了 Referer 信息但是在其中并未找到该子串,则嵌入的会话ID 会被标记为无效。默认为空字符串
session.save_handler 保存session的类型 files 用文件存储session
session.save_path session存储绝对路径 C:\phpStudy\PHPTutorial\tmp\tmp
session.serialize_handler 使用的seesion序列化引擎 php
session.upload_progress.cleanup 是否在上传完成后及时删除进度数据 On 上传完毕后删除
session.upload_progress.enabled 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态 On
session.upload_progress.freq 选项控制了上传进度信息应该多久被重新计算一次
session.upload_progress.min_freq 类似同上
session.upload_progress.name PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.prefix upload_progress_
session.use_cookies On
session.use_only_cookies On
session.use_trans_sid 0

相关安全隐患

#session.serialize_handler

当反序列化的数据与反序列化处理器不同时,通过特殊构造,可以伪造数据:
由于php handlerphp_serialize handler的结构不同,若将

1
2
3
4
5
6
$_SESSION['nyrae']='|O:4:"item":0:{}';
在php_serialize处理器下结果为:
a:1:{s:5:"nyrae";s:16:"|O:4:"item":0:{}";}
若用php处理器反序列化,则会将|之前的内容都认为是其键值,值为item对象

注意,php 5.4之后 默认session.serialize_handlerphp_serialize

#upload_progress

当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值。

根据说明,我们可以得出:

upload_progress.enabled开启时,当POST一个session.upload_progress.name同名变量时,会向$_SESSIONsession.upload_progress.prefix. session.upload_progress.name连接在一起的字段中写入内容

可以从一道题中,看出其利用方式,题目在这里http://web.jarvisoj.com:32784/

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
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

根据代码结构可以想到是一个反序列化的题目,应该是想办法创建OowoO对象,等到对象销毁时执行eval命令得到flag。由于没有unserialize函数,没有明显的写入点。这时,upload_progress就有了作用。

整体思想就是:利用upload_progress上传一个名字为PHP_SESSION_UPLOAD_PROGRESS的字段,然后利用session.serialize_handler的不一致,由于其会向$_SESSIONupload_progress_PHP_SESSION_UPLOAD_PROGRESS字段写入内容,其必然会调用php 反序列化处理器,我们向其中构造一个OowoO对象,mdzz字段为要执行的代码,当程序运行完,执行析构函数时,便会执行我们构造的恶意代码。

所以可以构造表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
</body>
</html>

然后修改其值:

upload_progress