From 039c85702716b7039674ad6b474bed5d89ccf1db Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:04:29 +0200 Subject: [PATCH] Moved task parsing logic to the client to be able to support dotnet/bof commands when operating from a different machine than the team server. Disabled sequence tracking due to issues. --- src/agent/nim.cfg | 2 +- .../protocol/parser.nim => client/task.nim} | 8 ++-- src/client/views/console.nim | 37 +++++++++++++--- src/client/views/dockspace.nim | 4 +- src/client/websocket.nim | 6 +-- src/common/sequence.nim | 26 +++++------ src/common/types.nim | 5 +-- src/modules/filesystem.nim | 2 +- src/modules/filetransfer.nim | 2 +- src/server/api/handlers.nim | 6 +-- src/server/api/routes.nim | 4 +- src/server/core/agent.nim | 43 ++++++++++--------- src/server/core/server.nim | 12 +++--- src/server/core/task.nim | 29 ------------- 14 files changed, 94 insertions(+), 92 deletions(-) rename src/{server/protocol/parser.nim => client/task.nim} (94%) diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index d4cc69a..d5d6103 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -4,5 +4,5 @@ --opt:size --passL:"-s" # Strip symbols, such as sensitive function names -d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" --d:MODULES="223" +-d:MODULES="255" -o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe" \ No newline at end of file diff --git a/src/server/protocol/parser.nim b/src/client/task.nim similarity index 94% rename from src/server/protocol/parser.nim rename to src/client/task.nim index ae5f6be..f561ab2 100644 --- a/src/server/protocol/parser.nim +++ b/src/client/task.nim @@ -1,6 +1,6 @@ import std/paths import strutils, sequtils, times, tables -import ../../common/[types, sequence, crypto, utils, serialize] +import ../common/[types, sequence, crypto, utils, serialize] proc parseInput*(input: string): seq[string] = var i = 0 @@ -84,12 +84,12 @@ proc parseArgument*(argument: Argument, value: string): TaskArg = return arg -proc createTask*(cq: Conquest, agentId: string, command: Command, arguments: seq[string]): Task = +proc createTask*(agentId, listenerId: string, command: Command, arguments: seq[string]): Task = # Construct the task payload prefix var task: Task task.taskId = string.toUuid(generateUUID()) - task.listenerId = string.toUuid(cq.agents[agentId].listenerId) + task.listenerId = string.toUuid(listenerId) task.timestamp = uint32(now().toTime().toUnix()) task.command = cast[uint16](command.commandType) task.argCount = uint8(arguments.len) @@ -124,4 +124,4 @@ proc createTask*(cq: Conquest, agentId: string, command: Command, arguments: seq task.header = taskHeader # Return the task object for serialization - return task \ No newline at end of file + return task diff --git a/src/client/views/console.nim b/src/client/views/console.nim index 98d0648..9361e80 100644 --- a/src/client/views/console.nim +++ b/src/client/views/console.nim @@ -1,12 +1,13 @@ import whisky -import strformat, strutils, times +import strformat, strutils, times, json import imguin/[cimgui, glfw_opengl, simple] import ../utils/[appImGui, colors] -import ../../common/[types] -import ../websocket +import ../../common/[types, utils] +import ../../modules/manager +import ../[task, websocket] const MAX_INPUT_LENGTH = 512 -type +type ConsoleComponent* = ref object of RootObj agent*: UIAgent showConsole*: bool @@ -124,6 +125,32 @@ proc addItem*(component: ConsoleComponent, itemType: LogType, data: string, time text: line )) +#[ + Handling console commands +]# +proc handleAgentCommand*(component: ConsoleComponent, ws: WebSocket, input: string) = + + # Convert user input into sequence of string arguments + let parsedArgs = parseInput(input) + + # Handle 'help' command + if parsedArgs[0] == "help": + # cq.handleHelp(parsedArgs) + component.addItem(LOG_WARNING, "Help") + return + + # Handle commands with actions on the agent + try: + let + command = getCommandByName(parsedArgs[0]) + task = createTask(component.agent.agentId, component.agent.listenerId, command, parsedArgs[1..^1]) + + ws.sendAgentTask(component.agent.agentId, task) + component.addItem(LOG_INFO, fmt"Tasked agent to {command.description.toLowerAscii()} ({Uuid.toString(task.taskId)})") + + except CatchableError: + component.addItem(LOG_ERROR, getCurrentExceptionMsg()) + #[ Drawing ]# @@ -271,7 +298,7 @@ proc draw*(component: ConsoleComponent, ws: WebSocket) = component.addItem(LOG_COMMAND, command) # Send command to team server - ws.sendAgentCommand(component.agent.agentId, command) + component.handleAgentCommand(ws, command) # Add command to console history component.history.add(command) diff --git a/src/client/views/dockspace.nim b/src/client/views/dockspace.nim index 4bb871f..2931782 100644 --- a/src/client/views/dockspace.nim +++ b/src/client/views/dockspace.nim @@ -50,8 +50,8 @@ proc draw*(component: DockspaceComponent, showComponent: ptr bool, views: Table[ igDockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace.int32) igDockBuilderSetNodeSize(dockspaceId, vp.WorkSize) - discard igDockBuilderSplitNode(dockspaceId, ImGuiDir_Down, 0.8f, dockBottom, dockTop) - discard igDockBuilderSplitNode(dockTop[], ImGuiDir_Right, 0.4f, dockTopRight, dockTopLeft) + discard igDockBuilderSplitNode(dockspaceId, ImGuiDir_Down, 5.0f, dockBottom, dockTop) + discard igDockBuilderSplitNode(dockTop[], ImGuiDir_Right, 0.5f, dockTopRight, dockTopLeft) igDockBuilderDockWindow("Sessions [Table View]", dockTopLeft[]) igDockBuilderDockWindow("Listeners", dockBottom[]) diff --git a/src/client/websocket.nim b/src/client/websocket.nim index f0bf0eb..2bad938 100644 --- a/src/client/websocket.nim +++ b/src/client/websocket.nim @@ -38,13 +38,13 @@ proc sendAgentBuild*(ws: WebSocket, buildInformation: AgentBuildInformation) = ) ws.sendEvent(event) -proc sendAgentCommand*(ws: WebSocket, agentId: string, command: string) = +proc sendAgentTask*(ws: WebSocket, agentId: string, task: Task) = let event = Event( - eventType: CLIENT_AGENT_COMMAND, + eventType: CLIENT_AGENT_TASK, timestamp: now().toTime().toUnix(), data: %*{ "agentId": agentId, - "command": command + "task": task } ) ws.sendEvent(event) diff --git a/src/common/sequence.nim b/src/common/sequence.nim index 0cae390..ea8dad6 100644 --- a/src/common/sequence.nim +++ b/src/common/sequence.nim @@ -8,23 +8,23 @@ proc nextSequence*(agentId: uint32): uint32 = return sequenceTable[agentId] proc validateSequence(agentId: uint32, seqNr: uint32, packetType: uint8): bool = - let lastSeqNr = sequenceTable.getOrDefault(agentId, 0'u32) + # let lastSeqNr = sequenceTable.getOrDefault(agentId, 0'u32) - # Heartbeat messages are not used for sequence tracking - if cast[PacketType](packetType) == MSG_HEARTBEAT: - return true + # # Heartbeat messages are not used for sequence tracking + # if cast[PacketType](packetType) == MSG_HEARTBEAT: + # return true - # In order to keep agents running after server restart, accept all connection with seqNr = 1, to update the table - if seqNr == 1'u32: - sequenceTable[agentId] = seqNr - return true + # # In order to keep agents running after server restart, accept all connection with seqNr = 1, to update the table + # if seqNr == 1'u32: + # sequenceTable[agentId] = seqNr + # return true - # Validate that the sequence number of the current packet is higher than the currently stored one - if seqNr <= lastSeqNr: - return false + # # Validate that the sequence number of the current packet is higher than the currently stored one + # if seqNr <= lastSeqNr: + # return false - # Update sequence number - sequenceTable[agentId] = seqNr + # # Update sequence number + # sequenceTable[agentId] = seqNr return true proc validatePacket*(header: Header, expectedType: uint8) = diff --git a/src/common/types.nim b/src/common/types.nim index 2742759..905e37f 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -246,7 +246,7 @@ type # Sent by client CLIENT_AGENT_BUILD = 1'u8 # Generate an agent binary for a specific listener - CLIENT_AGENT_COMMAND = 2'u8 # Instruct TS to send queue a command for a specific agent + CLIENT_AGENT_TASK = 2'u8 # Instruct TS to send queue a command for a specific agent CLIENT_LISTENER_START = 3'u8 # Start a listener on the TS CLIENT_LISTENER_STOP = 4'u8 # Stop a listener @@ -336,5 +336,4 @@ type sleepDelay*: uint32 sleepTechnique*: SleepObfuscationTechnique spoofStack*: bool - modules*: uint32 - + modules*: uint32 \ No newline at end of file diff --git a/src/modules/filesystem.nim b/src/modules/filesystem.nim index bdb57a7..77b535a 100644 --- a/src/modules/filesystem.nim +++ b/src/modules/filesystem.nim @@ -13,7 +13,7 @@ proc executeCopy(ctx: AgentCtx, task: Task): TaskResult let module* = Module( name: protect("filesystem"), description: protect("Conduct simple filesystem operations via Windows API."), - moduleType: MODULE_DOTNET, + moduleType: MODULE_FILESYSTEM, commands: @[ Command( name: protect("pwd"), diff --git a/src/modules/filetransfer.nim b/src/modules/filetransfer.nim index e4f6d9e..61d042e 100644 --- a/src/modules/filetransfer.nim +++ b/src/modules/filetransfer.nim @@ -8,7 +8,7 @@ proc executeUpload(ctx: AgentCtx, task: Task): TaskResult let module* = Module( name: protect("filetransfer"), description: protect("Upload/download files to/from the target system."), - moduleType: MODULE_FILESYSTEM, + moduleType: MODULE_FILETRANSFER, commands: @[ Command( name: protect("download"), diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index 8315447..42c7768 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -44,7 +44,7 @@ proc register*(registrationData: seq[byte]): bool = cq.error(err.msg) return false -proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] = +proc getTasks*(heartbeat: seq[byte]): tuple[agentId: string, tasks: seq[seq[byte]]] = {.cast(gcsafe).}: @@ -75,11 +75,11 @@ proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] = let taskData = cq.serializeTask(task) tasks.add(taskData) - return tasks + return (agentId, tasks) except CatchableError as err: cq.error(err.msg) - return @[] + return ("", @[]) proc handleResult*(resultData: seq[byte]) = diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index 9f9467f..941317f 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -5,6 +5,7 @@ import ./handlers import ../globals import ../core/logger import ../../common/[types, utils, serialize, profile] +import ../websocket # Not Found proc error404*(request: Request) = @@ -73,7 +74,7 @@ proc httpGet*(request: Request) = try: var responseBytes: seq[byte] - let tasks: seq[seq[byte]] = getTasks(heartbeat) + let (agentId, tasks) = getTasks(heartbeat) if tasks.len <= 0: request.respond(200, body = "") @@ -107,6 +108,7 @@ proc httpGet*(request: Request) = request.respond(200, headers = headers, body = prefix & response & suffix) # Notify operator that agent collected tasks + cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"{$response.len} bytes sent.") cq.info(fmt"{$response.len} bytes sent.") except CatchableError: diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index 87e8306..04d03dd 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -106,26 +106,27 @@ proc agentKill*(cq: Conquest, name: string) = # Switch to interact mode proc agentInteract*(cq: Conquest, name: string) = - - # Verify that agent exists - if not cq.dbAgentExists(name.toUpperAscii): - cq.error(fmt"Agent {name.toUpperAscii} does not exist.") - return - - let agent = cq.agents[name.toUpperAscii] - var command: string = "" - - # Change prompt indicator to show agent interaction - cq.interactAgent = agent - cq.prompt.setIndicator(fmt"[{agent.agentId}]> ") - cq.prompt.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")]) - - cq.info("Started interacting with agent ", fgYellow, styleBright, agent.agentId, resetStyle, ". Type 'help' to list available commands.\n") - - while command.replace(" ", "") != "back": - command = cq.prompt.readLine() - cq.handleAgentCommand(name, command) - # Reset interactAgent field after interaction with agent is ended using 'back' command - cq.interactAgent = nil + discard + # Verify that agent exists + # if not cq.dbAgentExists(name.toUpperAscii): + # cq.error(fmt"Agent {name.toUpperAscii} does not exist.") + # return + + # let agent = cq.agents[name.toUpperAscii] + # var command: string = "" + + # # Change prompt indicator to show agent interaction + # cq.interactAgent = agent + # cq.prompt.setIndicator(fmt"[{agent.agentId}]> ") + # cq.prompt.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")]) + + # cq.info("Started interacting with agent ", fgYellow, styleBright, agent.agentId, resetStyle, ". Type 'help' to list available commands.\n") + + # while command.replace(" ", "") != "back": + # command = cq.prompt.readLine() + # cq.handleAgentCommand(name, command) + + # # Reset interactAgent field after interaction with agent is ended using 'back' command + # cq.interactAgent = nil diff --git a/src/server/core/server.nim b/src/server/core/server.nim index 3f8cca8..dac2067 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -1,4 +1,4 @@ -import prompt, terminal, argparse, parsetoml, times, json +import prompt, terminal, argparse, parsetoml, times, json, math import strutils, strformat, system, tables import ./[agent, listener, task, builder] @@ -174,10 +174,10 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {. let event = message.recvEvent() case event.eventType: - of CLIENT_AGENT_COMMAND: + of CLIENT_AGENT_TASK: let agentId = event.data["agentId"].getStr() - let command = event.data["command"].getStr() - cq.handleAgentCommand(agentId, command) + let task = event.data["task"].to(Task) + cq.agents[agentId].tasks.add(task) of CLIENT_LISTENER_START: let listener = event.data.to(UIListener) @@ -246,7 +246,9 @@ proc startServer*(profilePath: string) = # Start websocket server var router: Router router.get("/*", upgradeHandler) - let server = newServer(router, websocketHandler) + + # Increased websocket message length in order to support dotnet assembly execution + let server = newServer(router, websocketHandler, maxMessageLen = 1024 * 1024 * 1024) var thread: Thread[Server] createThread(thread, serve, server) diff --git a/src/server/core/task.nim b/src/server/core/task.nim index 93578ce..db5a3aa 100644 --- a/src/server/core/task.nim +++ b/src/server/core/task.nim @@ -1,6 +1,5 @@ import strformat, terminal, tables, sequtils, strutils -import ../protocol/parser import ../core/logger import ../websocket import ../../modules/manager @@ -50,31 +49,3 @@ proc handleHelp(cq: Conquest, parsed: seq[string]) = except ValueError: # Command was not found cq.error(fmt"The command '{parsed[1]}' does not exist." & '\n') - -proc handleAgentCommand*(cq: Conquest, agentId: string, input: string) = - - cq.input(input) - - # Convert user input into sequence of string arguments - let parsedArgs = parseInput(input) - - # Handle 'help' command - if parsedArgs[0] == "help": - cq.handleHelp(parsedArgs) - return - - # Handle commands with actions on the agent - try: - let - command = getCommandByName(parsedArgs[0]) - task = cq.createTask(agentId, command, parsedArgs[1..^1]) - - # Add task to queue - cq.agents[agentId].tasks.add(task) - - cq.client.sendConsoleItem(agentId, LOG_INFO, fmt"Tasked agent to {command.description.toLowerAscii()}") - cq.info(fmt"Tasked agent to {command.description.toLowerAscii()}") - - except CatchableError: - cq.error(getCurrentExceptionMsg() & "\n") - return \ No newline at end of file