Joomla 权限提升漏洞(CVE-2016-9838)分析

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.漏洞分析

官方没有给出具体的分析,只给了描述:

翻译过来就是:

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

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

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

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

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

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

1.寻找输入点

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

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是我们原始的请求数据,并没有被过滤,所以基本可以把这里当作我们的输入点。

我们来验证一下,首先随便注册一个用户,然后再注册同样的用户并开启动态调试:

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

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

由于存储的是请求数据,所以我们还可以通过构造请求来向 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:

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

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

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

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

之前的 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.补丁分析

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

0x02 修复方案

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

0x03 参考

原文发布于微信公众号 - Seebug漏洞平台(seebug_org)

原文发表时间:2016-12-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏智能计算时代

Envoy架构概览(10):热启动,动态配置,初始化,排水,脚本

热启动 易于操作是特使的主要目标之一。除了强大的统计数据和本地管理界面之外,Envoy还具有“热”或“实时”重启的能力。这意味着Envoy可以完全重新加载自己(...

3972
来自专栏木子昭的博客

精析Python3实现动态web服务(附服务端源码)如果我们提供一个动态网站服务,至少应考虑以下四点:一个优秀的动态web框架应该是这样的:关于WSGI标准WIGS模型的要点:实现源码小结:

实现一个简单的静态web网站,只需将写好的html页面上传到特定的web服务器软件即可,但静态网页其实和图片没什么区别,每次更新网站内容,都需要重新制作htm...

38112
来自专栏xingoo, 一个梦想做发明家的程序员

汇编语言 手记3

从读写属性上存储器分为:随机存储器RAM和只读存储器ROM 从功能和连接上分类: 随机存储器RAM 装有BIOS的ROM 接口卡上的RAM ? 上述的存储器物理...

21510
来自专栏Python中文社区

基于Sanic的微服务基础架构

介绍 使用python做web开发面临的一个最大的问题就是性能,在解决C10K问题上显的有点吃力。有些异步框架Tornado、Twisted、Gevent 等就...

1.5K7
来自专栏Java帮帮-微信公众号-技术文章全总结

POI导入导出【面试+工作】

POI导入导出【面试+工作】 1.场景一 近期项目中的excel导入导出功能需求频繁的出现,趁此机会,今天笔者对POI的Excel数据的导入导出做一...

4204
来自专栏互联网大杂烩

操作系统-进程和线程

进程线程的区别 1、进程是什么? 是具有一定独立功能的程序、它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是可以独立运行...

1154
来自专栏前端杂货铺

Nodejs cluster模块深入探究

由表及里 HTTP服务器用于响应来自客户端的请求,当客户端请求数逐渐增大时服务端的处理机制有多种,如tomcat的多线程、nginx的事件循环等。而对于nod...

55610
来自专栏静晴轩

浅谈android中的目录结构

之前在android游戏开发中就遇到本地数据存储的问题:一般情形之下就将动态数据写入SD中存储,在没有SD卡的手机上就需另作处理了;再有在开发android应用...

34610
来自专栏JavaEdge

操作系统之文件管理一、文件与文件系统二、文件控制块和文件目录三、文件的物理结构四、文件系统的实现五、文件系统实例(UNIX)六、UNIX文件系统一、文件系统实例(FAT)二、文件操作的实现三、文件系统

5716
来自专栏小狼的世界

在Codeigniter框架中使用NuSOAP

NuSOAP 是一组功能强大的PHP类,这个工具的发布让使用和创建SOAP消息变得相当简单。 NuSOAP有Dirtrich Ayala编写,可以无缝的与许多最...

871

扫码关注云+社区

领取腾讯云代金券