diff --git a/zefie_wtvp_minisrv/testirc_run.js b/zefie_wtvp_minisrv/irconly.js similarity index 100% rename from zefie_wtvp_minisrv/testirc_run.js rename to zefie_wtvp_minisrv/irconly.js diff --git a/zefie_wtvp_minisrv/testirc_tests.js b/zefie_wtvp_minisrv/testirc_tests.js deleted file mode 100644 index 3d53a8f6..00000000 --- a/zefie_wtvp_minisrv/testirc_tests.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict'; -const path = require('path'); -var classPath = path.resolve(__dirname + path.sep + "includes" + path.sep + "classes" + path.sep) + path.sep; -require(classPath + "Prototypes.js"); -const WTVIRC = require(classPath + "WTVIRC.js"); -const { WTVShared, clientShowAlert } = require(classPath + "WTVShared.js"); -const wtvshared = new WTVShared(); // creates minisrv_config -const net = require('net'); - -var minisrv_config = wtvshared.readMiniSrvConfig(true, false, true); - -if (!minisrv_config.config.irc || !minisrv_config.config.irc.enabled) { - console.error('IRC is not enabled in the configuration.'); - process.exit(1); -} - -const HOST = 'localhost'; -const PORT = minisrv_config.config.irc.port; // Example IRC port - -const client = new net.Socket(); - -var lastLine = ''; - -client.connect(PORT, HOST, () => { - console.log(`Connected to ${HOST}:${PORT}`); - // You can send data here, e.g.: - // client.write('NICK testuser\r\n'); - // client.write('USER testuser 0 * :Test User\r\n'); -}); - -client.on('data', (data) => { - console.log('Received:', data.toString()); - lastLine = data.toString(); -}); - -client.on('close', () => { - console.log('Connection closed'); -}); - -client.on('error', (err) => { - console.error('Connection error:', err); -}); - - -testCase4(); - -function testCase1() { - // Try to auth as a server when we are not allowed to - client.write('SERVER testserver 0 * :Test Server\r\n'); - client.write('PASS \b\b\b\b\b\b\b\b\b\b\r\n'); - client.write('SERVER testserver 0 * :Test Server\r\n'); - client.write(`SVINFO 6 6 0 :-1\r\n`); - client.write('PASS \b\b\b\b\b\b\b\b\b\b\r\n'); - // we should be disconnected here -} - -function testCase2() { - // Malformed user authentication - client.write('NICK invaliduser\r\n'); - client.write('USER invaliduser\r\n'); - client.write('NICK invaliduser2\r\n'); - client.write('NICK invaliduser\r\n'); - client.write('USER invaliduser\r\n'); - client.write('USER invaliduser\r\n'); - client.write('USER invaliduser\r\n'); - client.write('USER invaliduser\r\n'); - // we should be disconnected here -} - -async function waitFor(expectedResponse) { - while (!lastLine.includes(expectedResponse)) { - await new Promise(resolve => setTimeout(resolve, 10)); // wait for 10ms - } -} - -async function testCase3() { - // join, msg, quit - client.write('NICK testuser\r\n'); - client.write('USER testuser 0 * :Test User\r\n'); - await waitFor("005"); - client.write('JOIN #testchannel\r\n'); - await new Promise(resolve => setTimeout(resolve, 10)); // wait for 10ms - client.write('PRIVMSG #testchannel :Hello, world!\r\n'); - await new Promise(resolve => setTimeout(resolve, 10)); // wait for 10ms - client.write('PART #testchannel\r\n'); - await new Promise(resolve => setTimeout(resolve, 10)); // wait for 10ms - client.write('QUIT :Goodbye\r\n'); - // we should be disconnected here -} - -function testCase4() { - // Arbitrary commands - client.write('NICK testuser\r\n'); - client.write('MODE testuser +i\r\n'); - client.write('TOPIC #testchannel :New topic\r\n'); - client.write('KICK #testchannel testuser :You have been kicked\r\n'); - client.write('NOTICE testuser :This is a notice\r\n'); - client.write('INVITE testuser #testchannel\r\n'); - client.write('WHO #testchannel\r\n'); - client.write('LIST\r\n'); -} \ No newline at end of file diff --git a/zefie_wtvp_minisrv/unroll_rc4.js b/zefie_wtvp_minisrv/unroll_rc4.js index 8d05af07..92c22d0e 100644 --- a/zefie_wtvp_minisrv/unroll_rc4.js +++ b/zefie_wtvp_minisrv/unroll_rc4.js @@ -1,71 +1,98 @@ const fs = require('fs'); const pcap = require('pcap-parser'); const WTVSec = require('./includes/classes/WTVSec.js'); -const WTVLZPF = require('./includes/classes/WTVLzpf.js'); -const httpHeaderParser = require('http-string-parser'); -const server_ip = '192.168.11.26'; // šŸ” Replace with actual IP -const connections = new Map(); // (key: `${srcIP}:${srcPort}<->${dstIP}:${dstPort}`) +// A map to hold the state of each TCP connection. +const connections = new Map(); -function connectionKey(src, sport, dst, dport) { - return `${src}:${sport}<->${dst}:${dport}`; +/** + * A simple, resilient function to parse HTTP headers from a buffer. + * @param {Buffer} buffer - The buffer containing HTTP headers. + * @returns {object|null} An object containing the headers, or null if headers are incomplete. + */ +function parseHeaders(buffer) { + const headers = {}; + const headerString = buffer.toString('utf8'); + const headersEnd = headerString.indexOf('\r\n\r\n'); + if (headersEnd === -1) { + return null; // Incomplete headers + } + + const lines = headerString.slice(0, headersEnd).split('\r\n'); + for (const line of lines) { + const parts = line.split(':'); + if (parts.length > 1) { + const key = parts.shift().trim().toLowerCase(); + const value = parts.join(':').trim(); + headers[key] = value; + } + } + return headers; } +/** + * Represents the state of a single direction of a TCP connection. + */ class ConnectionState { constructor() { - this.packets = []; this.buffer = Buffer.alloc(0); - this.rc4 = null; - this.secure = false; - this.wtv = null; + this.securityState = 'PLAINTEXT'; // PLAINTEXT, AWAITING_SECURE_RESPONSE, SECURE + this.wtvSec = null; + this.initialKey = null; + this.challenge = null; this.incarnation = null; + this.isClient = false; } + /** + * Appends new data to the connection's buffer. + * @param {Buffer} data - The raw TCP payload data. + */ feed(data) { this.buffer = Buffer.concat([this.buffer, data]); - this.packets.push(data); + } - // Parse headers if not done - const text = this.buffer.toString(); - if (!this.wtv && text.includes('wtv-initial-key')) { - const headers = httpHeaderParser.parseResponse(text).headers; - if (headers['wtv-initial-key'] && headers['wtv-challenge']) { - const initialKey = headers['wtv-initial-key'].trim(); - const challenge = headers['wtv-challenge'].trim(); - - this.wtv = new WTVSec({ - config: { - keys: { - initial_shared_key: initialKey, - }, - debug_flags: { - debug: false - } - } - }); - - this.wtv.ProcessChallenge(challenge); + /** + * Initializes the WTVSec instance for this connection. + * @param {string} initialKey - The wtv-initial-key from the server. + * @param {string} challenge - The wtv-challenge from the server. + */ + initializeSecurity(initialKey, challenge) { + this.wtvSec = new WTVSec({ + config: { + keys: { initial_shared_key: initialKey }, + debug_flags: { debug: false } } - } + }); + this.wtvSec.ProcessChallenge(challenge); + console.log("šŸ”‘ Security context initialized."); + } - if (!this.secure && text.includes('SECURE ON')) { - const headers = httpHeaderParser.parseRequest(text).headers; - let incarnationHeader = Object.keys(headers).find(k => k.toLowerCase() === 'wtv-incarnation'); - let incarnationValue = incarnationHeader ? headers[incarnationHeader].trim() : "1"; - - this.incarnation = parseInt(incarnationValue); - if (this.wtv) { - this.wtv.set_incarnation(this.incarnation); - this.secure = true; - console.log("šŸ” SECURE ON -- Starting decryption (incarnation =", this.incarnation + ")"); - } + /** + * Sets up the RC4 keys for an encrypted session. + * @param {number} incarnation - The wtv-incarnation value. + */ + setupEncryption(incarnation) { + if (this.wtvSec) { + this.incarnation = incarnation; + this.wtvSec.set_incarnation(this.incarnation); + console.log(`šŸ” Encryption keys prepared for this stream (incarnation=${this.incarnation})`); } } + /** + * Decrypts data using the appropriate RC4 key. + * @param {Buffer} data - The data to decrypt. + * @returns {Buffer} The decrypted or original data. + */ decrypt(data) { - if (this.secure && this.wtv) { + if (this.wtvSec && data.length > 0) { try { - return this.wtv.Decrypt(0, data); // Use key1 by default + // Client encrypts with key 0, server with key 1. + const keyNum = this.isClient ? 0 : 1; + const decrypted = this.wtvSec.Decrypt(keyNum, data); + console.log(`šŸ“¦ Decrypted ${data.length} bytes for ${this.isClient ? 'client' : 'server'} stream.`); + return Buffer.from(decrypted); } catch (e) { console.error("Decryption failed:", e); return data; @@ -75,54 +102,219 @@ class ConnectionState { } } -// Main PCAP processing -const parser = pcap.parse(fs.createReadStream('pcap.pcap')); +/** + * Processes the reassembled buffer for a connection, handling state transitions. + * @param {ConnectionState} state - The state object for the current connection direction. + * @param {ConnectionState} oppositeState - The state for the opposite direction of the connection. + */ +function processConnectionBuffer(state, oppositeState) { + while (state.buffer.length > 0) { + switch (state.securityState) { + case 'AWAITING_SECURE_RESPONSE': { + const headersEndIndex = state.buffer.indexOf('\r\n\r\n'); + if (headersEndIndex === -1) { + return; // Wait for the full headers. + } -parser.on('packet', packet => { - const data = packet.data; - const ethType = data.readUInt16BE(12); - if (ethType !== 0x0800) return; // Not IPv4 + const bodyStartIndex = headersEndIndex + 4; + const plaintextHeaders = state.buffer.slice(0, bodyStartIndex); + const encryptedBody = state.buffer.slice(bodyStartIndex); - const ipHeader = data.slice(14, 34); - const protocol = ipHeader[9]; - if (protocol !== 6) return; // Not TCP + process.stdout.write(plaintextHeaders); - const srcIP = ipHeader.slice(12, 16).join('.'); - const dstIP = ipHeader.slice(16, 20).join('.'); - const tcpHeaderStart = 34; - const srcPort = data.readUInt16BE(tcpHeaderStart); - const dstPort = data.readUInt16BE(tcpHeaderStart + 2); - const tcpHeaderLen = (data[tcpHeaderStart + 12] >> 4) * 4; - const tcpPayloadOffset = tcpHeaderStart + tcpHeaderLen; + if (encryptedBody.length > 0) { + const decryptedBody = state.decrypt(encryptedBody); + process.stdout.write(decryptedBody); + } - const payload = data.slice(tcpPayloadOffset); + state.buffer = Buffer.alloc(0); + state.securityState = 'SECURE'; + continue; + } + + case 'SECURE': { + const output = state.decrypt(state.buffer); + process.stdout.write(output); + state.buffer = Buffer.alloc(0); + return; + } + + case 'PLAINTEXT': + default: { + const headersEndIndex = state.buffer.indexOf('\r\n\r\n'); + if (headersEndIndex === -1) { + return; + } + + const headerSectionLength = headersEndIndex + 4; + const headerBuffer = state.buffer.slice(0, headerSectionLength); + const headers = parseHeaders(headerBuffer); + + const requestLine = headerBuffer.toString('utf8').split('\r\n')[0]; + if (state.isClient && requestLine.includes('SECURE ON')) { + console.log("ā–¶ļø Client sent SECURE ON. Transitioning to encrypted mode."); + let incarnation = headers['wtv-incarnation'] ? parseInt(headers['wtv-incarnation'].trim(), 10) : 1; + + state.setupEncryption(incarnation); + state.securityState = 'SECURE'; + if (oppositeState) { + oppositeState.setupEncryption(incarnation); + oppositeState.securityState = 'AWAITING_SECURE_RESPONSE'; + } + + process.stdout.write(headerBuffer); + const remainingData = state.buffer.slice(headerSectionLength); + + if (remainingData.length > 0) { + const decryptedBody = state.decrypt(remainingData); + process.stdout.write(decryptedBody); + } + state.buffer = Buffer.alloc(0); + return; + } + + if (!state.isClient && headers) { + if (headers['wtv-initial-key']) { + state.initialKey = headers['wtv-initial-key']; + if(oppositeState) oppositeState.initialKey = headers['wtv-initial-key']; + console.log("Found wtv-initial-key."); + } + if (headers['wtv-challenge']) { + state.challenge = headers['wtv-challenge']; + if(oppositeState) oppositeState.challenge = headers['wtv-challenge']; + console.log("Found wtv-challenge."); + } + + if (state.initialKey && state.challenge && !state.wtvSec) { + state.initializeSecurity(state.initialKey, state.challenge); + if (oppositeState) oppositeState.initializeSecurity(state.initialKey, state.challenge); + } + } + + let fullMessageLength = headerSectionLength; + if (headers && headers['content-length']) { + const bodyLength = parseInt(headers['content-length'], 10); + fullMessageLength += bodyLength; + } + + if (state.buffer.length < fullMessageLength) { + return; + } + + const fullMessage = state.buffer.slice(0, fullMessageLength); + process.stdout.write(fullMessage); + state.buffer = state.buffer.slice(fullMessageLength); + } + } + } +} + + +// --- Main PCAP Processing Logic --- + +const args = process.argv.slice(2); +const inputFile = args[args.indexOf('-i') + 1]; +const serverIP = args[args.indexOf('-h') + 1]; + +if (!inputFile || !serverIP) { + console.error('Usage: node unroll_rc4.js -i -h '); + process.exit(1); +} + +const parser = pcap.parse(fs.createReadStream(inputFile)); +console.log(`šŸš€ Starting pcap parser for ${inputFile} with server IP ${serverIP}`); + +let totalPackets = 0; +let processedPackets = 0; +let linkLayerType = -1; +let ipHeaderOffset = 14; + +parser.on('globalHeader', (globalHeader) => { + linkLayerType = globalHeader.linkLayerType; + console.log(`[INFO] PCAP Link-Layer Header Type: ${linkLayerType}. Adjusting offsets.`); + switch (linkLayerType) { + case 0: ipHeaderOffset = 4; break; + case 1: ipHeaderOffset = 14; break; + case 101: ipHeaderOffset = 0; break; + case 113: ipHeaderOffset = 16; break; + default: + console.warn(`[WARN] Unsupported link-layer type: ${linkLayerType}. Assuming Ethernet.`); + ipHeaderOffset = 14; + } +}); + +parser.on('packet', (packet) => { + totalPackets++; + + let isIPv4 = false; + switch (linkLayerType) { + case 0: isIPv4 = (packet.data.length > 4) && (packet.data.readUInt32LE(0) === 2); break; + case 1: isIPv4 = packet.data.readUInt16BE(12) === 0x0800; break; + case 101: isIPv4 = (packet.data.length > 0) && ((packet.data[0] >> 4) === 4); break; + case 113: isIPv4 = packet.data.readUInt16BE(14) === 0x0800; break; + default: return; + } + if (!isIPv4) return; + + const protocolOffset = ipHeaderOffset + 9; + if (packet.data.length <= protocolOffset || packet.data[protocolOffset] !== 6) return; + + const ipHeaderLength = (packet.data[ipHeaderOffset] & 0x0F) * 4; + const tcpHeaderBase = ipHeaderOffset + ipHeaderLength; + const tcpHeaderLength = (packet.data[tcpHeaderBase + 12] >> 4) * 4; + const payloadOffset = tcpHeaderBase + tcpHeaderLength; + if (packet.data.length <= payloadOffset) return; + const payload = packet.data.slice(payloadOffset); if (payload.length === 0) return; - const isServer = srcIP === server_ip; - const connKey = connectionKey(srcIP, srcPort, dstIP, dstPort); - - if (!connections.has(connKey)) { - connections.set(connKey, new ConnectionState()); + processedPackets++; + + const srcIP = packet.data.slice(ipHeaderOffset + 12, ipHeaderOffset + 16).join('.'); + const dstIP = packet.data.slice(ipHeaderOffset + 16, ipHeaderOffset + 20).join('.'); + const srcPort = packet.data.readUInt16BE(tcpHeaderBase); + const dstPort = packet.data.readUInt16BE(tcpHeaderBase + 2); + + if (srcIP != serverIP && dstIP !== serverIP) { + return; } - const state = connections.get(connKey); + const currentKey = `${srcIP}:${srcPort}->${dstIP}:${dstPort}`; + const oppositeKey = `${dstIP}:${dstPort}->${srcIP}:${srcPort}`; + + if (!connections.has(currentKey)) { + let isClientToServer; + const payloadString = payload.toString('utf8'); + console.log(payloadString); + if (srcIP === serverIP && dstIP === serverIP) { + isClientToServer = payloadString.startsWith('GET') || payloadString.startsWith('POST') || payloadString.startsWith('SECURE ON'); + } else { + isClientToServer = dstIP === serverIP; + } + + connections.set(currentKey, new ConnectionState()); + connections.set(oppositeKey, new ConnectionState()); + connections.get(currentKey).isClient = isClientToServer; + connections.get(oppositeKey).isClient = !isClientToServer; + } + + const state = connections.get(currentKey); + const oppositeState = connections.get(oppositeKey); + state.feed(payload); + processConnectionBuffer(state, oppositeState); + processConnectionBuffer(oppositeState, state); - const decrypted = state.decrypt(payload); - - /* TODO - if (decrypted.includes("wtv-lzpf")) { - const headers = decrypted.toString('utf8').split("\n\n")[0]; - const lzpf_data = decrypted.slice(headers.length + 2); - const lzpf = new WTVLZPF(); - process.stdout.write(headers); - const decomp_data = lzpf.Decompress(lzpf_data); - process.stdout.write(decomp_data); - } -*/ - process.stdout.write(decrypted); }); parser.on('end', () => { - console.log('\nāœ… Done parsing PCAP.'); + console.log('\n[INFO] End of PCAP file reached. Processing any remaining buffered data...'); + for(const [key, state] of connections.entries()){ + const parts = key.split('->'); + const oppositeKey = `${parts[1]}->${parts[0]}`; + if(connections.has(oppositeKey)) { + const oppositeState = connections.get(oppositeKey); + processConnectionBuffer(state, oppositeState); + } + } + console.log(`\nāœ… Done parsing PCAP. Processed ${processedPackets} out of ${totalPackets} total packets.`); });