前言
网上关于CVE-2020-11890 的复现文章有很多,但是大多都是给了一个脚本,docker pull一下,然后攻击就算是复现。
现如今,我已经对脚本小子这个标签有着很深的怨念,所以并不想只是单纯的拿脚本EXP攻击、结束这样,而是真正理解攻击手法,学到一些有价值的东西。
环境搭建
1.Vmware bugku靶场镜像
2.python3环境
指令:
docker pull hoangkien1020/joomla:3.9.16 docker run -d --rm -it -p 8080:80 hoangkien1020/joomla:3.9.16
得到虚拟机IP:192.168.247.128
于是可以在本机打开192.168.247.128:8080,就可以访问漏洞场景。
漏洞分析
分析肯定是要看源码的。docker中,查看容器相关信息的指令为如下。
docker inspect 容器id(可只选前几位,不用全部输入)
结果如下:
找到如图所示的路径,大概率就是文件路径,但是我没有查阅过资料三种文件夹的属性,我都是直接一个个尝试
/var/lib/docker/overlay2/4db83ff0b3b3d18da4cc5a8e64a0bb74d7f2965d78ae6dbec3a050c45ff47548/merged
然后进入var/www/html 即可看到源码(有时候源码存在于opt或者root目录下,不会花费很多时间,动手翻一下就行)
由于这个只是个鸡肋的漏洞,是可以将低权限用户提升为高级管理员。
那么与之相关的一定是对于roleid等类似参数的越权修改,与权限检查规避。从数据库中获取数据结构,在usergroups表中存在以下字段。
id parent_id lft rgt title 其中super user的 id为8 administrator的id为7.parent id代表他的亲节点。
根据两个参数可以画出以下权限关系
这里另外两个参数 lft 和rft。根据思考后找到规律,如果当前记录的 rft-lft == 1 则代表其下没有子节点(参考publisher和super user)。相反,rft-lft如果大于0,则代表存在子节点。且子节点的个数为((rft-lft)-1)/2
全局搜索源代码,找到修改权限为super user的相关代码
可见,只要checkgroup为admin,则当前管理员账号是administrator,则直接抛出异常结束程序,导致无法修改。
于是进入checkgroup方法
继续查看getgrouppath
方法的作用就是,根据获取的groupid,返回用户组groups列表的path属性。
那么这个path属性是如何产生的,就需要去查看初始化groups列表时候的一系列方法了。
此方法对每个用户组数据进行填充,path属性即为该用户组的祖先数组。level为层次数。以pulic用户组为例,它非常特殊因为在数据库中他的parentid为0,故进入分支使得他的层次为0,他的path为自己的id1。动态调试后,可得path为array(0 => '1'), level为0。
以guest为例,动态调试,Guest节点的path为array (0 => '1',1 =>'9',),level为1。同理,Administrator用户组节点的祖先数组path为array (0 => '1', 1 =>'6', 2 => '7',),level为2。
回到checkgroup方法, 我们肯定是想设置他的id为8,成为superuser,于是会返回path,为array (0 => '1',1 =>'8',)。这里getassetrules方法,会去访问获取当前用户不可访问的的用户组id。这在libraries\src\Access\Rule.php的allow方法中。
$this->data数组代表目前用户不可以访问的节点id。我们使用的是administrator用户账号,通过此方法,可得,不可以操作的用户组节点id为8,即super user,因此$this->data数组值为array (8 ->1),而这个数组值存在于祖先数组中(path的获取),导致程序错误,抛出异常说我不是超级管理员的错误。
而漏洞的利用,是将public的双亲节点设置了一个很大的数字,而不是单纯的0。
为什么administrator可以操作public?上面已经解释了,通过rule方法返回,只是返回了不可操作的用户数组为array (8 ->1),没有public相关的array(0 => '1')。
public的双亲节点由0修改为poc中的任意数字后,程序在处理Public用户组时,不再进入if ($parentId ===0)的判断分支,但id为其他数字(足够大)对应的parentGroup并不存在,使得$parentGroup->path值为null。
之后,array_merge将null和数组进行拼接,使得程序产生错误(array_merge首个参数不能为null),导致$group->path也变成null,最后由 count($group->path) – 1把level值变为-1。
根节点Public的path为null,使得所有后续节点的path都是基于其双亲节点的path值array_merge计算而来,所以所有节点的祖先数组全都为null。回到rule方法,在检查当前用户无法访问的节点是否在path中,所有节点的path都为null,所有无法将祖先数组节点值与无法访问节点列表中的值进行比较,从而顺利bypass检查,导致权限提升。
所以,简单的来说,这个漏洞的产生是因为没有对于public权限进行防护,仅仅对于superuser有保护。进而修改双亲节点,导致判断错乱,使得逃避了检查权限提升。
后记