Joomla提权漏洞(CVE-2016-9838)

漏洞描述

Joomla 1.6.0~3.6.4版本存在提权漏洞,根据官方描述利用该漏洞可以更改已存在用户的用户信息,包括用户名,密码,邮箱和权限组
CVE

漏洞环境

http://oss.lou00.top/img/CVE-2016-9838/cve-2016-9838.zip

前提条件

1.需要开启注册
2.知道目标id

漏洞分析

攻击点在components/com_users/controllers/registration.phpregister()函数

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
public function register()
{
...
// Get the user data.
$requestData = $this->input->post->get('jform', array(), 'array');
...
// 这里检验了用户名和邮箱是否在数据库中存在
$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);
...
}
// Flush the data from the session.
$app->setUserState('com_users.registration.data', null);
...
}

可以看到3个$app->setUserState('com_users.registration.data', xxx)
用户名密码存在时赋$requestData
注册不通过是赋$data
注册成功时刷新session
所赋值会在$app->getUserState('com_users.registration.data')中获得
位置在components/com_users/models/registration.phpgetData()函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function getData()
{
if ($this->data === null)
{
...
// Override the base user data with any data in the session.
$temp = (array) $app->getUserState('com_users.registration.data', array());

foreach ($temp as $k => $v)
{
$this->data->$k = $v;
}
...
}
return $this->data
}

发现getData会在GET注册页面以及同类下的register($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
public function register($temp)
{
...
$data = (array) $this->getData();
// Merge in the registration data.
foreach ($temp as $k => $v)
{
$data[$k] = $v;
}
...
// Bind the data.
if (!$user->bind($data))
{
$this->setError(JText::sprintf('COM_USERS_REGISTRATION_BIND_FAILED', $user->getError()));
return false;
}
...
// Store the data.
if (!$user->save())
{
$this->setError(JText::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError()));

return false;
}
...
}

跟进$user->bind($data)

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

没有对赋值造成影响
跳出跟进$user->save()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
...
}

过滤特殊符号以及重复用户名及邮箱
继续跟进$table->store()

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 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)

如果主键存在则更新,主键不存在则插入。

漏洞复现

step1

先注册一个账户

step2

再次注册这个账户并多post一个jform[id]为46的字段

step3

再发一个get请求,发现$temp已经有id了

step4

按正常流程再注册一个号

step5

成功更新 水平越权

修复方案

只允许指定的参数赋值