Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand All @@ -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
Expand Down
36 changes: 28 additions & 8 deletions scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
"""
)

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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
}

Expand Down