bbscanner / app /arjun_runner.py
Wuhp
Fix keyless WPScan, Arjun parameter 404 handling and discovery identity policy checks
bc2250b
Raw History Blame Contribute Delete
4.22 kB
"""Run installed Arjun with exact-origin, bounded GET transport and fixed telemetry."""
import contextlib
import io
import json
import os
import runpy
import sys
from pathlib import Path
from urllib.parse import urlsplit
import requests
from .scope import public_host
from .sampling import origin
class Stop(BaseException):
def __init__(self, reason, code=None):
self.reason, self.code = reason, code
def run(argv):
args = list(argv)
index = args.index("--asfb-report")
report_path = Path(args[index + 1])
del args[index:index + 2]
target = args[args.index("-u") + 1]
expected = origin(target)
stats = {"status": "running", "requests": 0}
original = requests.sessions.Session.request
def request(session, method, url, **kwargs):
if origin(url) != expected or method.upper() != "GET":
raise Stop("scope_blocked")
if stats["requests"] >= 64:
raise Stop("request_budget")
public_host(expected[1], os.environ.get("ALLOW_PRIVATE_TARGETS") == "1")
session.trust_env = False
session.cookies.clear()
kwargs.update(verify=True, allow_redirects=False, timeout=8, stream=True)
stats["requests"] += 1
try:
response = original(session, method, url, **kwargs)
code = response.status_code
if code in (301, 302, 303, 307, 308):
response.close()
raise Stop("redirect_not_followed", code)
if code in (400, 413, 418, 429) or code >= 500:
response.close()
raise Stop("http_unavailable", code)
# A missing baseline is inapplicable. Later 404/410 responses can
# be parameter-dependent differences for native Arjun to compare.
if code >= 400 and not (code in (404, 410) and stats["requests"] > 1):
response.close()
raise Stop("endpoint_unavailable", code)
content = bytearray()
for chunk in response.iter_content(16384):
if len(content) + len(chunk) > 262144:
response.close()
raise Stop("response_limit", code)
content.extend(chunk)
response._content, response._content_consumed = bytes(content), True
response.close()
return response
except requests.RequestException:
raise Stop("request_failed")
# Native stable_request drops the query and may change scheme. Preserve the
# selected URL and use the same guarded transport instead.
def stable_request(url, headers):
response = requests.get(url, headers=headers)
if not any(kind in response.headers.get("Content-Type", "").lower()
for kind in ("text", "html", "json", "xml")):
raise Stop("content_unavailable")
return url
import arjun.core.utils as utils
old_stable, old_argv = utils.stable_request, sys.argv
requests.sessions.Session.request = request
utils.stable_request = stable_request
sys.argv = ["arjun"] + args
try:
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
runpy.run_module("arjun.__main__", run_name="__main__")
stats["status"] = "completed"
except Stop as exc:
policy = exc.reason in {"scope_blocked", "request_budget", "redirect_not_followed", "response_limit"}
stats.update(status="completed" if policy else "partial", stop_reason=exc.reason,
early_stop=policy, coverage_complete=False)
if type(exc.code) is int:
stats["http_status"] = exc.code
except SystemExit as exc:
stats["status"] = "completed" if exc.code in (None, 0) else "failed"
except Exception:
stats.update(status="failed", stop_reason="native_error")
finally:
requests.sessions.Session.request = original
utils.stable_request, sys.argv = old_stable, old_argv
report_path.write_text(json.dumps(stats), encoding="utf-8")
return 0 if stats["status"] in ("completed", "partial") else 2
if __name__ == "__main__":
raise SystemExit(run(sys.argv[1:]))