Upload files to "redflare/modules"

This commit is contained in:
2026-06-21 17:54:15 +00:00
parent 2230d24e5f
commit 3f2bb5309f
5 changed files with 677 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
from __future__ import annotations
import random
import string
import time
from pathlib import Path
from urllib.parse import urljoin
from redflare.core.models import Finding, ModuleResult, Target
from .base import Module, ModuleContext
from .http import request
DEFAULT_PATHS = [
"admin", "login", "dashboard", "api", "api/health", "graphql",
"swagger", "openapi.json", ".env", "config.json", "metrics", "actuator/health",
]
SENSITIVE = {"admin", "dashboard", ".env", "config.json", "metrics", "graphql", "openapi.json"}
class PathDiscoveryModule(Module):
name = "path_discovery"
description = "Rate-limited path discovery with wildcard response filtering"
def run(self, target: Target, context: ModuleContext) -> ModuleResult:
started = time.monotonic()
result = ModuleResult(self.name, target.url)
paths = load_paths(context.wordlist)[: context.max_paths]
interval = 1 / context.rate if context.rate > 0 else 0
try:
context.emit(target.url, self.name, "progress", f"Loading {len(paths)} paths; rate={context.rate:g}/s")
random_path = "redflare-" + "".join(random.choice(string.ascii_lowercase) for _ in range(18))
baseline = request(urljoin(target.url.rstrip("/") + "/", random_path), context.timeout)
baseline_signature = (baseline.status, len(baseline.body))
context.emit(target.url, self.name, "info", f"Wildcard baseline: status={baseline.status} bytes={len(baseline.body)}")
hits = []
progress_every = max(1, min(25, len(paths) // 10 or 1))
for index, path in enumerate(paths, start=1):
if index == 1 or index % progress_every == 0 or index == len(paths):
context.emit(target.url, self.name, "progress", f"Probing {index}/{len(paths)}: /{path}")
if interval:
time.sleep(interval)
url = urljoin(target.url.rstrip("/") + "/", path.lstrip("/"))
try:
response = request(url, context.timeout, max_body=250_000)
except Exception as exc:
result.errors.append(f"{url}: {type(exc).__name__}: {exc}")
continue
signature = (response.status, len(response.body))
if signature == baseline_signature or response.status == 404:
continue
hit = {"path": path, "url": url, "status": response.status, "bytes": len(response.body)}
hits.append(hit)
context.surface_graph.add_endpoint(
target.url,
url,
method="GET",
source="path-discovery",
content_type=response.headers.get("content-type"),
status=response.status,
)
context.emit(target.url, self.name, "info", f"Response HTTP {response.status}: /{path} ({len(response.body)} bytes)")
if response.status < 400:
severity = "medium" if path in SENSITIVE else "info"
result.findings.append(
Finding(
context.run_id,
target.url,
self.name,
"discovered-path",
f"Accessible path discovered: /{path}",
severity,
0.8,
f"The path returned HTTP {response.status} and differed from the wildcard baseline.",
hit,
["discovery", "web"],
)
)
result.observations.update({"paths_tested": len(paths), "wildcard_signature": baseline_signature, "hits": hits})
except Exception as exc:
result.status = "error"
result.errors.append(f"{type(exc).__name__}: {exc}")
result.duration_seconds = round(time.monotonic() - started, 4)
return result
def load_paths(wordlist: str | None) -> list[str]:
if not wordlist:
return list(DEFAULT_PATHS)
lines = Path(wordlist).read_text(encoding="utf-8").splitlines()
seen = set()
paths = []
for line in lines:
value = line.strip().lstrip("/")
if value and not value.startswith("#") and value not in seen:
seen.add(value)
paths.append(value)
return paths