Author: p0wd3r (知道创宇404安全实验室)

Date: 2016-12-21

0x00 漏洞概述

1.漏洞简介

Joomla 于12月13日发布了3.6.5的升级公告,此次升级修复了三个安全漏洞,其中 CVE-2016-9838 被官方定为高危。根据官方的描述,这是一个权限提升漏洞,利用该漏洞攻击者可以更改已存在用户的用户信息,包括用户名、密码、邮箱和权限组 。经过分析测试,成功实现了水平用户权限突破,但没有实现垂直权限提升为管理员。

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

翻译过来就是:

对表单验证失败时存储到 session 中的未过滤数据的不正确使用会导致对现有用户帐户的修改,包括重置其用户名,密码和用户组分配。

因为没有具体细节,所以我们先从补丁下手,其中这个文件的更改引起了我的注意:

https://github.com/joomla/joomla-cms/commit/435a2226118a4e83ecaf33431ec05f39c640c744

Alt text

可以看到这里的$temp是 session 数据,而该文件又与用户相关,所以很有可能就是漏洞点。

我们下面通过这样两个步骤来分析:

  1. 寻找输入点
  2. 梳理处理逻辑

1.寻找输入点

我们找一下这个 session 是从哪里来的:

Alt text

components/com_users/controllers/registration.php中设置,在components/com_users/models/registration.php中获取。我们看components/com_users/controllers/registration.php中第108-204行的register函数:

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

由于这个用户之前注册过,所以验证出错,从而将请求数据写入了 session 中。

取 session 的地方在components/com_users/models/registration.phpgetData函数,该函数在访问注册页面时就会被调用一次,我们在这时就可以看到 session 的值:

Alt text

由于存储的是请求数据,所以我们还可以通过构造请求来向 session 中写入一些额外的变量。

2.梳理处理逻辑

输入点找到了,下面来看我们输入的数据在哪里被用到。我们看components/com_users/models/registration.phpregister函数:

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行:

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行:

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中:

/**
 * 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 ,由于在bindsave中并没有对这个属性进行过滤,那么最终保存的数据就会带有 id 这个主键,从而变成了更新操作,也就是用我们请求的数据更新了一个已存在的用户。

实际操作一下,我们之前注册了一个名字为 victim 的用户,数据库中的 id 是57:

Alt text

然后我们以相同的用户名再发起一次请求,然后截包,添加一个值为57名为jform[id]的属性:

Alt text

放行后由于重复注册从而发生错误,程序随后将请求数据记录到了 session 中:

Alt text

接下来我们发送一个新的注册请求,用户名邮箱均为之前未注册过的,在save函数处下断点:

Alt text

id 被写进了$user中。然后放行请求,即可在数据库中看到结果:

Alt text

之前的 victim 已被新用户 attacker 取代。

整个攻击流程总结如下:

  1. 注册用户A
  2. 重复注册用户A,请求包中加上想要攻击的用户C的 id
  3. 注册用户B
  4. 用户B替代了用户C

(上面的演示中A和C是同一个用户)

需要注意的是我们不能直接发送一个带有 id 的请求来更新用户,这样的请求会在validate函数中被过滤掉,在components/com_users/controllers/registration.phpregister函数中:

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做了强制赋值:

$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

使用 session 时仅允许使用指定的属性。

0x02 修复方案

升级至3.6.5 https://www.joomla.org/announcements/release-news/5693-joomla-3-6-5-released.html

0x03 参考


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/152/