**Author: p0wd3r (知道创宇404安全实验室)**
**Date: 2016-12-21**
## 0x00 漏洞概述
### 1.漏洞简介
[Joomla](https://www.joomla.org/) 于12月13日发布了3.6.5的[升级公告](https://www.joomla.org/announcements/release-news/5693-joomla-3-6-5-released.html),此次升级修复了三个安全漏洞,其中 [CVE-2016-9838](https://developer.joomla.org/security-centre/664-20161201-core-elevated-privileges.html) 被官方定为高危。根据官方的描述,这是一个权限提升漏洞,利用该漏洞攻击者可以更改已存在用户的用户信息,包括用户名、密码、邮箱和权限组 。经过分析测试,成功实现了水平用户权限突破,但没有实现垂直权限提升为管理员。
### 2.漏洞影响
触发漏洞前提条件:
1. 网站开启注册功能
2. 攻击者知道想要攻击的用户的 **id** (不是用户名)
成功攻击后攻击者可以更改已存在用户的用户信息,包括用户名、密码、邮箱和权限组 。
### 3.影响版本
1.6.0 - 3.6.4
## 0x01 漏洞复现
### 1. 环境搭建
docker-compose.yml:
```
version: '2'
services:
db:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=hellojm
- MYSQL_DATABASE=jm
app:
image: joomla:3.6.3
depends_on:
- db
links:
- db
ports:
- "127.0.0.1:8080:80"
```
然后在 docker-compose.yml 所在目录执行`docker-compose up`,访问后台开启注册再配置SMTP即可。
### 2.漏洞分析
官方没有给出具体的分析,只给了描述:
![Alt text](https://images.seebug.org/content/images/2016/12/description.png)
翻译过来就是:
对表单验证失败时存储到 session 中的未过滤数据的不正确使用会导致对现有用户帐户的修改,包括重置其用户名,密码和用户组分配。
因为没有具体细节,所以我们先从补丁下手,其中这个文件的更改引起了我的注意:
[https://github.com/joomla/joomla-cms/commit/435a2226118a4e83ecaf33431ec05f39c640c744](https://github.com/joomla/joomla-cms/commit/435a2226118a4e83ecaf33431ec05f39c640c744)
![Alt text](https://images.seebug.org/content/images/2016/12/patch-1.png)
可以看到这里的`$temp`是 session 数据,而该文件又与用户相关,所以很有可能就是漏洞点。
我们下面通过这样两个步骤来分析:
1. 寻找输入点
2. 梳理处理逻辑
**1.寻找输入点**
我们找一下这个 session 是从哪里来的:
![Alt text](https://images.seebug.org/content/images/2016/12/find-session.png)
在`components/com_users/controllers/registration.php`中设置,在`components/com_users/models/registration.php`中获取。我们看`components/com_users/controllers/registration.php`中第108-204行的`register`函数:
```php
public function register()
{
...
$data = $model->validate($form, $requestData);
// Check for validation errors.
if ($data === false)
{
...
// Save the data in the session.
$app->setUserState('com_users.registration.data', $requestData);
...
}
// Attempt to save the data.
$return = $model->register($data);
// Check for errors.
if ($return === false)
{
// Save the data in the session.
$app->setUserState('com_users.registration.data', $data);
...
}
...
}
```
这两处设置 session 均在产生错误后进行,和漏洞描述相符,并且`$requestData`是我们原始的请求数据,并没有被过滤,所以基本可以把这里当作我们的输入点。
我们来验证一下,首先随便注册一个用户,然后再注册同样的用户并开启动态调试:
![Alt text](https://images.seebug.org/content/images/2016/12/save-session.png)
由于这个用户之前注册过,所以验证出错,从而将请求数据写入了 session 中。
取 session 的地方在`components/com_users/models/registration.php`的`getData`函数,该函数在访问注册页面时就会被调用一次,我们在这时就可以看到 session 的值:
![Alt text](https://images.seebug.org/content/images/2016/12/session-value.png)
由于存储的是请求数据,所以我们还可以通过构造请求来向 session 中写入一些**额外**的变量。
**2.梳理处理逻辑**
输入点找到了,下面来看我们输入的数据在哪里被用到。我们看`components/com_users/models/registration.php`的`register`函数:
```php
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;
}
// Prepare the data for the user object.
$data['email'] = JStringPunycode::emailToPunycode($data['email1']);
$data['password'] = $data['password1'];
$useractivation = $params->get('useractivation');
$sendpassword = $params->get('sendpassword', 1);
...
// Bind the data.
if (!$user->bind($data))
{
$this->setError(JText::sprintf('COM_USERS_REGISTRATION_BIND_FAILED', $user->getError()));
return false;
}
// Load the users plugin group.
JPluginHelper::importPlugin('user');
// Store the data.
if (!$user->save())
{
$this->setError(JText::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError()));
return false;
}
...
}
```
在这里调用了之前的`getData`函数,然后使用请求数据对`$data`赋值,再用`$data`对用户数据做更改。
首先跟进`$user->bind($data)`,在`libraries/joomla/user/user.php`中第595-693行:
```php
public function bind(&$array)
{
...
// Bind the array
if (!$this->setProperties($array))
{
$this->setError(JText::_('JLIB_USER_ERROR_BIND_ARRAY'));
return false;
}
// Make sure its an integer
$this->id = (int) $this->id;
return true;
}
```
这里根据我们传入的数据对对象的属性进行赋值,`setProperties`并没有对赋值进行限制。
接下来我们看`$user->save($data)`,在`libraries/joomla/user/user.php`中第706-818行:
```php
public function save($updateOnly = false)
{
// Create the user table object
$table = $this->getTable();
$this->params = (string) $this->_params;
$table->bind($this->getProperties());
...
if (!$table->check())
{
$this->setError($table->getError());
return false;
}
...
// Store the user data in the database
$result = $table->store();
...
}
```
具体内容就是将`$user`的属性绑定到`$table`中,然后对`$table`进行检查,这里仅仅是过滤特殊符号和重复的用户名和邮箱,如果检查通过,将数据存入到数据库中,存储数据的函数在`libraries/joomla/table/user.php`中:
```php
/**
* Method to store a row in the database from the JTable instance properties.
*
* If a primary key value is set the row with that primary key value will be updated with the instance property values.
* If no primary key value is set a new row will be inserted into the database with the properties from the JTable instance.
*
* @param boolean $updateNulls True to update fields even if they are null.
*
* @return boolean True on success.
*
* @since 11.1
*/
public function store($updateNulls = false)
```
如果主键存在则更新,主键不存在则插入。
整个的流程看下来我发现这样一个问题:
**如果`$data`中有`id`这个属性并且其值是一个已存在的用户的 id ,由于在`bind`和`save`中并没有对这个属性进行过滤,那么最终保存的数据就会带有 id 这个主键,从而变成了更新操作,也就是用我们请求的数据更新了一个已存在的用户。**
实际操作一下,我们之前注册了一个名字为 victim 的用户,数据库中的 id 是57:
![Alt text](https://images.seebug.org/content/images/2016/12/initial-id.png)
然后我们以相同的用户名再发起一次请求,然后截包,添加一个值为57名为`jform[id]`的属性:
![Alt text](https://images.seebug.org/content/images/2016/12/add-id.png)
放行后由于重复注册从而发生错误,程序随后将请求数据记录到了 session 中:
![Alt text](https://images.seebug.org/content/images/2016/12/session-id.png)
接下来我们发送一个新的注册请求,用户名邮箱均为之前未注册过的,在`save`函数处下断点:
![Alt text](https://images.seebug.org/content/images/2016/12/user-id.png)
id 被写进了`$user`中。然后放行请求,即可在数据库中看到结果:
![Alt text](https://images.seebug.org/content/images/2016/12/change-user.png)
之前的 victim 已被新用户 attacker 取代。
整个攻击流程总结如下:
1. 注册用户A
2. 重复注册用户A,请求包中加上想要攻击的用户C的 id
3. 注册用户B
4. 用户B替代了用户C
(上面的演示中A和C是同一个用户)
需要注意的是我们不能直接发送一个带有 id 的请求来更新用户,这样的请求会在`validate`函数中被过滤掉,在`components/com_users/controllers/registration.php`的`register`函数中:
```php
public function register()
{
...
$data = $model->validate($form, $requestData);
// Check for validation errors.
if ($data === false)
{
...
// Save the data in the session.
$app->setUserState('com_users.registration.data', $requestData);
...
}
// Attempt to save the data.
$return = $model->register($data);
...
}
```
所以我们采用的是先通过`validate`触发错误来将 id 写到 session 中,然后发送正常请求,在`register`中读取 session 来引入 id,这样就可以绕过`validate`了。
另外一点,实施攻击后被攻击用户的权限会被改为新注册用户的权限(一般是 Registered),这个权限目前我们无法更改,因为在`getData`函数中对`groups`做了强制赋值:
```php
$temp = (array) $app->getUserState('com_users.registration.data', array());
...
// Get the groups the user should be added to after registration.
$this->data->groups = array();
// Get the default new user group, Registered if not specified.
$system = $params->get('new_usertype', 2);
$this->data->groups[] = $system;
```
所以目前只是实现了水平权限的提升,至于是否可以垂直权限提升以及怎么提升还要等官方的说明或者是大家的分析。
由于没有技术细节,一切都是根据自己的推断而来,如有错误,还望指正 :)
### 3.补丁分析
![Alt text](https://images.seebug.org/content/images/2016/12/patch-2.png)
使用 session 时仅允许使用指定的属性。
## 0x02 修复方案
升级至3.6.5 [https://www.joomla.org/announcements/release-news/5693-joomla-3-6-5-released.html](https://www.joomla.org/announcements/release-news/5693-joomla-3-6-5-released.html)
## 0x03 参考
* [https://www.joomla.org/announcements/release-news/5693-joomla-3-6-5-released.html](https://www.joomla.org/announcements/release-news/5693-joomla-3-6-5-released.html)
* [https://developer.joomla.org/security-centre/664-20161201-core-elevated-privileges.html](https://developer.joomla.org/security-centre/664-20161201-core-elevated-privileges.html)
* [https://github.com/joomla/joomla-cms/commit/435a2226118a4e83ecaf33431ec05f39c640c744](https://github.com/joomla/joomla-cms/commit/435a2226118a4e83ecaf33431ec05f39c640c744)
全部评论 (1)