diff --git a/README.md b/README.md index 7e9e12a..7610f85 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,13 @@ python3 scanner.py -u https://example.com --path /_next --path /api python3 scanner.py -u https://example.com --path-file paths.txt ``` +Scan through a proxy: + +``` +python3 scanner.py -u https://example.com --proxy http://proxy.example.com:8080 +python3 scanner.py -l hosts.txt --proxy socks5://127.0.0.1:1080 +``` + ## Options ``` @@ -110,6 +117,7 @@ python3 scanner.py -u https://example.com --path-file paths.txt --waf-bypass-size Size of junk data in KB (default: 128) --path Custom path to test (can be used multiple times) --path-file File containing paths to test (one per line) +--proxy Proxy URL (e.g., http://proxy.example.com:8080 or socks5://proxy.example.com:1080) ``` ## Credits diff --git a/scanner.py b/scanner.py index 9d0bdb2..3d48443 100755 --- a/scanner.py +++ b/scanner.py @@ -203,7 +203,7 @@ def build_rce_payload(windows: bool = False, waf_bypass: bool = False, waf_bypas return body, content_type -def resolve_redirects(url: str, timeout: int, verify_ssl: bool, max_redirects: int = 10) -> str: +def resolve_redirects(url: str, timeout: int, verify_ssl: bool, proxy: Optional[dict[str, str]] = None, max_redirects: int = 10) -> str: """Follow redirects only if they stay on the same host.""" current_url = url original_host = urlparse(url).netloc @@ -214,6 +214,7 @@ def resolve_redirects(url: str, timeout: int, verify_ssl: bool, max_redirects: i current_url, timeout=timeout, verify=verify_ssl, + proxies=proxy, allow_redirects=False ) if response.status_code in (301, 302, 303, 307, 308): @@ -239,7 +240,7 @@ def resolve_redirects(url: str, timeout: int, verify_ssl: bool, max_redirects: i return current_url -def send_payload(target_url: str, headers: dict, body: str, timeout: int, verify_ssl: bool) -> Tuple[Optional[requests.Response], Optional[str]]: +def send_payload(target_url: str, headers: dict, body: str, timeout: int, verify_ssl: bool, proxy: Optional[dict[str, str]] = None) -> Tuple[Optional[requests.Response], Optional[str]]: """Send the exploit payload to a URL. Returns (response, error).""" try: # Encode body as bytes to ensure proper Content-Length calculation @@ -251,6 +252,7 @@ def send_payload(target_url: str, headers: dict, body: str, timeout: int, verify data=body_bytes, timeout=timeout, verify=verify_ssl, + proxies=proxy, allow_redirects=False ) return response, None @@ -290,7 +292,7 @@ def is_vulnerable_rce_check(response: requests.Response) -> bool: return bool(re.search(r'.*/login\?a=11111.*', redirect_header)) -def check_vulnerability(host: str, timeout: int = 10, verify_ssl: bool = True, follow_redirects: bool = True, custom_headers: Optional[dict[str, str]] = None, safe_check: bool = False, windows: bool = False, waf_bypass: bool = False, waf_bypass_size_kb: int = 128, vercel_waf_bypass: bool = False, paths: Optional[list[str]] = None) -> dict: +def check_vulnerability(host: str, timeout: int = 10, verify_ssl: bool = True, follow_redirects: bool = True, custom_headers: Optional[dict[str, str]] = None, safe_check: bool = False, windows: bool = False, waf_bypass: bool = False, waf_bypass_size_kb: int = 128, vercel_waf_bypass: bool = False, paths: Optional[list[str]] = None, proxy: Optional[dict[str, str]] = None) -> dict: """ Check if a host is vulnerable to CVE-2025-55182/CVE-2025-66478. @@ -379,7 +381,7 @@ def build_response_str(resp: requests.Response) -> str: result["final_url"] = test_url result["request"] = build_request_str(test_url) - response, error = send_payload(test_url, headers, body, timeout, verify_ssl) + response, error = send_payload(test_url, headers, body, timeout, verify_ssl, proxy) if error: # In RCE mode, timeouts indicate not vulnerable (patched servers hang) @@ -406,10 +408,10 @@ def build_response_str(resp: requests.Response) -> str: # Path not vulnerable - try redirect path if enabled if follow_redirects: try: - redirect_url = resolve_redirects(test_url, timeout, verify_ssl) + redirect_url = resolve_redirects(test_url, timeout, verify_ssl, proxy) if redirect_url != test_url: # Different path, test it - response, error = send_payload(redirect_url, headers, body, timeout, verify_ssl) + response, error = send_payload(redirect_url, headers, body, timeout, verify_ssl, proxy) if error: # Continue to next path @@ -534,6 +536,8 @@ def main(): %(prog)s -u https://example.com --path /_next %(prog)s -u https://example.com --path /_next --path /api %(prog)s -u https://example.com --path-file paths.txt + %(prog)s -u https://example.com --proxy http://proxy.example.com:8080 + %(prog)s -l hosts.txt --proxy socks5://127.0.0.1:1080 """ ) @@ -649,6 +653,11 @@ def main(): help="File containing list of paths to test (one per line, e.g., '/_next', '/api')" ) + parser.add_argument( + "--proxy", + help="Proxy URL (e.g., http://proxy.example.com:8080 or socks5://proxy.example.com:1080)" + ) + args = parser.parse_args() if args.no_color or not sys.stdout.isatty(): @@ -707,6 +716,8 @@ def main(): print(colorize(f"[*] WAF bypass enabled ({args.waf_bypass_size}KB junk data)", Colors.CYAN)) if args.vercel_waf_bypass: print(colorize("[*] Vercel WAF bypass mode enabled", Colors.CYAN)) + if args.proxy: + print(colorize(f"[*] Using proxy: {args.proxy}", Colors.CYAN)) if args.insecure: print(colorize("[!] SSL verification disabled", Colors.YELLOW)) print() @@ -718,12 +729,21 @@ def main(): verify_ssl = not args.insecure custom_headers = parse_headers(args.headers) + # Parse proxy configuration + proxy = None + if args.proxy: + # requests library expects proxy dict with http/https/socks5 keys + proxy = { + "http": args.proxy, + "https": args.proxy + } + if args.insecure: import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) if len(hosts) == 1: - result = check_vulnerability(hosts[0], timeout, verify_ssl, custom_headers=custom_headers, safe_check=args.safe_check, windows=args.windows, waf_bypass=args.waf_bypass, waf_bypass_size_kb=args.waf_bypass_size, vercel_waf_bypass=args.vercel_waf_bypass, paths=paths) + result = check_vulnerability(hosts[0], timeout, verify_ssl, custom_headers=custom_headers, safe_check=args.safe_check, windows=args.windows, waf_bypass=args.waf_bypass, waf_bypass_size_kb=args.waf_bypass_size, vercel_waf_bypass=args.vercel_waf_bypass, paths=paths, proxy=proxy) results.append(result) if not args.quiet or result["vulnerable"]: print_result(result, args.verbose) @@ -732,7 +752,7 @@ def main(): else: with ThreadPoolExecutor(max_workers=args.threads) as executor: futures = { - executor.submit(check_vulnerability, host, timeout, verify_ssl, custom_headers=custom_headers, safe_check=args.safe_check, windows=args.windows, waf_bypass=args.waf_bypass, waf_bypass_size_kb=args.waf_bypass_size, vercel_waf_bypass=args.vercel_waf_bypass, paths=paths): host + executor.submit(check_vulnerability, host, timeout, verify_ssl, custom_headers=custom_headers, safe_check=args.safe_check, windows=args.windows, waf_bypass=args.waf_bypass, waf_bypass_size_kb=args.waf_bypass_size, vercel_waf_bypass=args.vercel_waf_bypass, paths=paths, proxy=proxy): host for host in hosts }