漏洞简介

近日,joomla官方给出了一个安全公告。从公告可知Joomla! CMS versions 2.5.0 - 3.9.16版本在处理用户组时缺少对根用户组的检查,从而导致了一个提权漏洞的产生(CVE-2020-11890)。

经过我分析之后发现,想要利用这个漏洞,必须先要有一个管理员账号,而这个漏洞的作用仅仅能将管理员提权为超级管理员。

虽然这个漏洞看起来无比鸡肋,但是分析过程却其乐无穷:既了解joomla是如何实现用户组权限划分,又复习了下数据结构。总体上来说漏洞虽小,但分析过程还是很有研究与记录价值的。

漏洞分析

本次漏洞可以将joomla系统中的Administrator用户提权为Super Users。在分析漏洞前,我们来看一下Super Users与Administrator有什么区别:

超级管理员 (Super Users):拥有Joomla的所有权限。并且超级管理员只能由另一个超级管理员来创建。

高级管理员(Administrator):Administrator没有权限将一个用户升级成超级用户或者编辑一个超级用户、不可以修改Joomla的全局设置,没有权限来改变和安装模板和Joomla的语言文件。

作为测试,我们新建三个账号,分别为administrator(administrator用户组)、Super User(Super User用户组)、test(administrator用户组)

54fffed2367d8208a6295e38b5623ed8.png

使用Administrator账号登陆,访问Joomla全局设置链接

/administrator/index.php?option=com_config

2a2f609df0bd270d060804660fa17480.png

可见Administrator用户组权限不可以访问该功能页面。

使用Administration账号编辑test账号的用户组

d0530172f936765cc008a8e8e3a1410f.png

Administrator用户组权限不可以为其他的用户添加super user权限

使用Superuser账号登陆,访问Joomla全局设置链接

2ff54fc05128dca46fb49759eb86b638.png

Superuser权限可以访问Joomla全局设置页面

使用Superuser账号编辑test账号的用户组

31887c18adb4d3718c33b3b8245436f2.png

可以为test账号添加super user权限

关于漏洞的初步猜测

在刚看到漏洞简介时,我猜测会不会是joomla只在前端做了校验,使用Administration账号编辑test账号的用户组时,在前端把super user这个选项卡隐藏起来了,后端并未校验权限,使得漏洞产生。

为了验证我的猜想,我在修改test用户组时抓包并修改其中的jform[groups]值

f0209da75a6809f6aaf7f3916c58770c.png

每一个用户组都有一个id值,这个可以通过数据库中查看得来

05b3d888b44dbde56bdffef80985e726.png

因为我需要将test账号改为super users用户组权限,因此需改数据包中jform[groups]值为8

经过测试发现,这是行不通的

ddf163abdd76dfeedcf5d9c7e1d85046.png

在猜想失败之后,只好动态调试一下源代码,看一下joomla是如何进行权限校验的

动态调试

既然在上文猜想中,我们强行改包时抛出了个Save failed with the following error: User not Super
Administrator错误,那么直接在源代码中找到抛出错误的位置libraries\src\User\User.php

bcf56a673b4558d69adca8eec933d800.png

可见上图中,只要checkGroup方法为真,则进入if分支抛出Save failed with the following error: User not Super Administrator错误

cee907a65d171026cfbfd405f9959868.png

首先来看下getGroupPath

3d43d94db146c4e04462b69faccd57f5.png

getGroupPath的作用是通过传入的groupid参数,获取要查询的用户组分支中叶子节点所属用户组,并返回到树的根节点。简而言之,就是获取用户组列表——groups列表中对应用户组的path属性值

用户组列表(groups)

我们来看下groups列表是什么,是怎么生成的
用户组列表(groups)中记录了所有用户组的属性值,包括名称、id、双亲节点信息、该节点的祖先数组

接下来分析下groups列表是怎么生成的
首先,程序从数据库usergroups表中读取每一个用户组的属性值
数据库中数据如下

05b3d888b44dbde56bdffef80985e726.png

程序读取后赋值到groups数组中

90fdb0beecbb1adb14d78ba4f1ac325e.png

接着调用populateGroupData方法对groups数组中每个用户组数据进行补充

5f76e4975217d3c58d3733c1201559b7.png

在这一环节,程序将为每一个用户组提供path与level属性值

其中path属性就是树形结构中以该用户组节点的祖先(Ancestor)数组、level即为该结点的层次(Level
of Node)

回顾一下数据库中每个用户组的属性值,这里注意parent_id值

9b80914e36313b90f6c77defc4dbf5fc.png

除了Public父节点为0之外,其他的用户组在表中都存在对应的双亲节点。可见Public用户组为树形结构中的根节点,层次为1。

07e4598f33500494dc183fbdb2b39f7e.png

Registered、Manager、Super Users、Guest的双亲节点id皆为1,即Public节点 。层次为2
剩余的用户组节点分别以Registered、Manager、Super Users、Guest四个节点作为双亲节点。

用户节点的树形图如下

d380e0654c8ff7a223e4070d81c2293e.png

动态调试结果如下

a8bb90f8b7f5609f6518342305281bcb.png

从上图可见,这里以Public用户组节点举例:Public作为根节点,其path以及level生成时比较特殊,进入parentid为0的if分支,最终祖先数组path为array(0 => ‘1’), level为0

再以Registered、Manager、Super Users、Guest这四个层次为2的用户组节点中的Guest节点为例

be793cbabb3e94b34461a3b6e9965d27.png

Guest节点的path为array (0 => ‘1’,1 =>‘9’,),level为1。Path是由Guest节点所有祖先组成的集合,level值为该节点层数减一

最后看一下其他层次大于2的节点,以Administrator用户组节点举例

2e7adc594ec19113eba55f8556eb06a5.png

从数据库中可见,Administrator用户组双亲节点id为6,对应 Manager节点,Manager用户组节点的双亲节点id为1,对应Public用户组节点。其层次为3

442d59b0112f8738a3c73b2db904a579.png

通过调试也可看出,Administrator用户组节点的祖先数组path为array (0 => ‘1’, 1 =>‘6’, 2 => ‘7’,),level为2

在弄明白groups列表之后,看一下程序是如何判断当前用户的权限判断的

回到checkGroup方法中

cee907a65d171026cfbfd405f9959868.png

上文以及指导getGroupPath方法的作用了,由于我们请求构造中的$groupid为8,即想把test账号添加到id为8对应的super users组。getGroupPath接收传入的$groupid,返回super user节点的祖先数组array (0=> ‘1’, 1 => ‘8’,)

10e711ae91359192c253acec9ba626d1.png

接着,在libraries\src\Access\Rule.php的allow方法中,程序遍历superuser的祖先数组array
(0 => ‘1’, 1 => ‘8’,)

397c10dfa94bb2ffa87f48fb344cb9b7.png

程序判断superuser的祖先节点是否有在$this->data中出现,$this->data值如下

82817b40c91392827b5ad1386f6d3cae.png

$this->data数组代表目前用户不可以访问的节点id。由于我们使用的是administrator用户组的账号,不可以操作的用户组节点id为8,即super user,因此$this->data数组值为array (8 => 1,)

superuser的祖先数组中的叶子节点值为8,正好在目前用户不可以访问的$this->data数组中

0e9c21d4f8729213a2a274d86391a653.png

因此该用户权限无法进行操作,程序抛出当前用户不是超级管理员的错误

6b8bb78abb697b27a0170e1123bbfc5b.png

漏洞利用

通过分析poc发现,这个漏洞利用特别的脑洞大开

429dbee5ceb4134d398457d1776b09df.png

首先看一下Poc中上图片段,poc把public节点的双亲节点改成100,当然这个数字可以为表中任意不存在的id值
这样做的目的是给原先的根节点public节点安一个双亲节点

由于administrator权限的用户$this->data数组值为array (8 =>1,),仅不允许操作super user权限节点,但public对应的祖先数组array (0 =>‘1’)可不在禁止之列,因此administrator权限的用户可以构造上图poc中的数据包,修改public节点的双亲节点

public节点的双亲节点由0修改为poc中的100后,造成了很大的混乱

程序在处理Public用户组时,由于parentId为100,不再进入if ($parentId ===0)分支,然而id为100对应的parentGroup并不存在,导致$parentGroup->path值为null

bedc5b065d46f7cf00fa1bb271eab7d7.png

这样以来,array_merge将一个null和一个数组进行拼接,由于array_merge首参不接受null作为参数使得程序产生错误,$group->path也变成null

进而由 count($group->path) – 1得出的level值变为-1(0-1得来)

由于根节点Public的path为null,使得所有后续用户组节点的path都是基于其双亲节点的path值array_merge计算而来,所以所有节点的祖先数组全都为null

在检查当前用户无法访问的节点是否在path中时,由于所有节点的path都为null,所有无法将其祖先列表中的节点一一拿出与无法访问节点列表中的值比对,从而得以逃避检查

f60e313e4cf7cb84392152c47dff3941.png

f60e313e4cf7cb84392152c47dff3941.png

关于漏洞利用工具可见如下链接:
https://github.com/HoangKien1020/CVE-2020-11890

题外话

在数据库的用户组表中lft和rgt的作用是什么?

e0e598b530e7352c394fec5266bf6325.png

从表中可以看到有lft和rgt两列,这两列的目的在于:
在树状数据结构中,每一个节点都有一个左右值即lft和rgt。如果右值-左值=1,则代表当前节点为叶子节点;相反,如果右值-左值>1,则代表当前节点有子节点,其他左右值在当前节点左右值之间的节点,即为当前结点的所有子节点。从下图可以很好理解其中关系

140a893d06f2836ca567c059990925e0.png

这个漏洞虽然影响不是很大,joomla官方仅仅给出了低危评分,但是这个漏洞分析起来很有意思,顺带复习了下数据结构。