Implemented agent registration to match new binary structure instead of json.

This commit is contained in:
Jakob Friedl
2025-07-21 22:07:25 +02:00
parent 99f55cc04f
commit 9f15026fd1
28 changed files with 452 additions and 327 deletions

View File

@@ -3,37 +3,39 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys
import ../[utils, globals]
import ../db/database
import ../task/packer
import ../../common/types
import ../../common/[types, utils]
import sugar
# Utility functions
proc add*(cq: Conquest, agent: Agent) =
cq.agents[agent.name] = agent
cq.agents[agent.agentId] = agent
#[
Agent API
Functions relevant for dealing with the agent API, such as registering new agents, querying tasks and posting results
]#
proc register*(agent: Agent): bool =
proc register*(registrationData: seq[byte]): bool =
# The following line is required to be able to use the `cq` global variable for console output
{.cast(gcsafe).}:
# Check if listener that is requested exists
# TODO: Verify that the listener accessed is also the listener specified in the URL
# This can be achieved by extracting the port number from the `Host` header and matching it to the one queried from the database
if not cq.dbListenerExists(agent.listener.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n")
let agent: Agent = deserializeNewAgent(registrationData)
# Validate that listener exists
if not cq.dbListenerExists(agent.listenerId.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listenerId}.", "\n")
return false
# Store agent in database
# # Store agent in database
if not cq.dbStoreAgent(agent):
cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.name} into database.", "\n")
cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.agentId} into database.", "\n")
return false
cq.add(agent)
let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.name, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listener, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.agentId, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listenerId, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
return true

View File

@@ -3,84 +3,71 @@ import sequtils, strutils, times, base64
import ./handlers
import ../[utils, globals]
import ../../common/types
proc encode(bytes: seq[seq[byte]]): string =
result = ""
for task in bytes:
result &= encode(task)
import ../../common/[types, utils]
proc error404*(ctx: Context) {.async.} =
resp "", Http404
#[
POST /{listener-uuid}/register
POST /register
Called from agent to register itself to the conquest server
]#
proc register*(ctx: Context) {.async.} =
# Check headers
# If POST data is not JSON data, return 404 error code
if ctx.request.contentType != "application/json":
# If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/octet-stream":
resp "", Http404
return
# The JSON data for the agent registration has to be in the following format
#[
{
"username": "username",
"hostname":"hostname",
"domain": "domain.local",
"ip": "ip-address",
"os": "operating-system",
"process": "agent.exe",
"pid": 1234,
"elevated": false.
"sleep": 10
}
]#
try:
let
postData: JsonNode = parseJson(ctx.request.body)
agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData)
agentUuid: string = generateUUID()
listenerUuid: string = ctx.getPathParams("listener")
date: DateTime = now()
let agent: Agent = Agent(
name: agentUuid,
listener: listenerUuid,
username: agentRegistrationData.username,
hostname: agentRegistrationData.hostname,
domain: agentRegistrationData.domain,
process: agentRegistrationData.process,
pid: agentRegistrationData.pid,
ip: agentRegistrationData.ip,
os: agentRegistrationData.os,
elevated: agentRegistrationData.elevated,
sleep: agentRegistrationData.sleep,
jitter: 0.2,
tasks: @[],
firstCheckin: date,
latestCheckin: date
)
# Fully register agent and add it to database
if not agent.register():
# Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed
# Return a 404 error code either way
resp "", Http404
return
# If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results
resp agent.name
let agentId = register(ctx.request.body.toBytes())
resp "Ok", Http200
except CatchableError:
# JSON data is invalid or does not match the expected format (described above)
resp "", Http404
return
# try:
# let
# postData: JsonNode = parseJson(ctx.request.body)
# agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData)
# agentUuid: string = generateUUID()
# listenerUuid: string = ctx.getPathParams("listener")
# date: DateTime = now()
# let agent: Agent = Agent(
# name: agentUuid,
# listener: listenerUuid,
# username: agentRegistrationData.username,
# hostname: agentRegistrationData.hostname,
# domain: agentRegistrationData.domain,
# process: agentRegistrationData.process,
# pid: agentRegistrationData.pid,
# ip: agentRegistrationData.ip,
# os: agentRegistrationData.os,
# elevated: agentRegistrationData.elevated,
# sleep: agentRegistrationData.sleep,
# jitter: 0.2,
# tasks: @[],
# firstCheckin: date,
# latestCheckin: date
# )
# # Fully register agent and add it to database
# if not agent.register():
# # Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed
# # Return a 404 error code either way
# resp "", Http404
# return
# # If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results
# resp agent.name
# except CatchableError:
# # JSON data is invalid or does not match the expected format (described above)
# resp "", Http404
# return
#[
GET /{listener-uuid}/{agent-uuid}/tasks
@@ -122,17 +109,11 @@ proc getTasks*(ctx: Context) {.async.} =
resp "", Http404
#[
POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results
POST /results
Called from agent to post results of a task
]#
proc postResults*(ctx: Context) {.async.} =
let
listener = ctx.getPathParams("listener")
agent = ctx.getPathParams("agent")
task = ctx.getPathParams("task")
# Check headers
# If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/octet-stream":

View File

@@ -3,12 +3,12 @@ import terminal, strformat, strutils, tables, times, system, osproc, streams
import ../utils
import ../task/dispatcher
import ../db/database
import ../../common/types
import ../../common/[types, utils]
# Utility functions
proc addMultiple*(cq: Conquest, agents: seq[Agent]) =
for a in agents:
cq.agents[a.name] = a
cq.agents[a.agentId] = a
proc delAgent*(cq: Conquest, agentName: string) =
cq.agents.del(agentName)
@@ -65,8 +65,8 @@ proc agentInfo*(cq: Conquest, name: string) =
# TODO: Improve formatting
cq.writeLine(fmt"""
Agent name (UUID): {agent.name}
Connected to listener: {agent.listener}
Agent name (UUID): {agent.agentId}
Connected to listener: {agent.listenerId}
──────────────────────────────────────────
Username: {agent.username}
Hostname: {agent.hostname}
@@ -113,9 +113,9 @@ proc agentInteract*(cq: Conquest, name: string) =
var command: string = ""
# Change prompt indicator to show agent interaction
cq.setIndicator(fmt"[{agent.name}]> ")
cq.setIndicator(fmt"[{agent.agentId}]> ")
cq.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")])
cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.name, resetStyle, ". Type 'help' to list available commands.\n")
cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.agentId, resetStyle, ". Type 'help' to list available commands.\n")
cq.interactAgent = agent
while command.replace(" ", "") != "back":

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import system, terminal, tiny_sqlite, times
import ../utils
import ../../common/types
import ../../common/[types, utils]
#[
Agent database functions
@@ -14,7 +14,7 @@ proc dbStoreAgent*(cq: Conquest, agent: Agent): bool =
conquestDb.exec("""
INSERT INTO agents (name, listener, process, pid, username, hostname, domain, ip, os, elevated, sleep, jitter, firstCheckin, latestCheckin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
""", agent.name, agent.listener, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss"))
""", agent.agentId, agent.listenerId, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss"))
conquestDb.close()
except:
@@ -31,11 +31,11 @@ proc dbGetAllAgents*(cq: Conquest): seq[Agent] =
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents;"):
let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let (agentId, listenerId, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let a = Agent(
name: name,
listener: listener,
agentId: agentId,
listenerId: listenerId,
sleep: sleep,
pid: pid,
username: username,
@@ -66,11 +66,11 @@ proc dbGetAllAgentsByListener*(cq: Conquest, listenerName: string): seq[Agent] =
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents WHERE listener = ?;", listenerName):
let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let (agentId, listenerId, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let a = Agent(
name: name,
listener: listener,
agentId: agentId,
listenerId: listenerId,
sleep: sleep,
pid: pid,
username: username,

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import times, strformat, terminal, tables, json, sequtils, strutils
import ./[parser]
import ../utils
import ../../common/types
import ../../common/[types, utils]
proc initAgentCommands*(): Table[string, Command] =
var commands = initTable[string, Command]()
@@ -158,7 +158,7 @@ proc handleAgentCommand*(cq: Conquest, input: string) =
if input.replace(" ", "").len == 0: return
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, input)
cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.agentId}] ", resetStyle, styleBright, input)
# Convert user input into sequence of string arguments
let parsedArgs = parseInput(input)

View File

@@ -1,7 +1,6 @@
import strutils, strformat, streams
import strutils, strformat, streams, times
import ../utils
import ../../common/types
import ../../common/serialize
import ../../common/[types, utils, serialize]
proc serializeTask*(task: Task): seq[byte] =
@@ -98,4 +97,66 @@ proc deserializeTaskResult*(resultData: seq[byte]): TaskResult =
resultType: resultType,
length: length,
data: data
)
)
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()
)

View File

@@ -1,6 +1,6 @@
import strutils, strformat, times
import ../utils
import ../../common/types
import ../../common/[types, utils]
proc parseInput*(input: string): seq[string] =
var i = 0
@@ -77,8 +77,8 @@ proc parseTask*(cq: Conquest, command: Command, arguments: seq[string]): Task =
# Construct the task payload prefix
var task: Task
task.taskId = uuidToUint32(generateUUID())
task.agentId = uuidToUint32(cq.interactAgent.name)
task.listenerId = uuidToUint32(cq.interactAgent.listener)
task.agentId = uuidToUint32(cq.interactAgent.agentId)
task.listenerId = uuidToUint32(cq.interactAgent.listenerId)
task.timestamp = uint32(now().toTime().toUnix())
task.command = cast[uint16](command.commandType)
task.argCount = uint8(arguments.len)

View File

@@ -1,7 +1,7 @@
import strutils, terminal, tables, sequtils, times, strformat, random, prompt
import std/wordwrap
import ../common/types
import ../common/[types, utils]
# Utility functions
proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] =
@@ -16,49 +16,6 @@ proc validatePort*(portStr: string): bool =
except ValueError:
return false
proc generateUUID*(): string =
# Create a 4-byte HEX UUID string (8 characters)
(0..<4).mapIt(rand(255)).mapIt(fmt"{it:02X}").join()
proc uuidToUint32*(uuid: string): uint32 =
return fromHex[uint32](uuid)
proc uuidToString*(uuid: uint32): string =
return uuid.toHex(8)
proc toString*(data: seq[byte]): string =
result = newString(data.len)
for i, b in data:
result[i] = char(b)
proc toBytes*(data: string): seq[byte] =
result = newSeq[byte](data.len)
for i, c in data:
result[i] = byte(c.ord)
proc toHexDump*(data: seq[byte]): string =
for i, b in data:
result.add(b.toHex(2))
if i < data.len - 1:
if (i + 1) mod 4 == 0:
result.add(" | ") # Add | every 4 bytes
else:
result.add(" ") # Regular space
proc toBytes*(value: uint16): seq[byte] =
return @[
byte(value and 0xFF),
byte((value shr 8) and 0xFF)
]
proc toBytes*(value: uint32): seq[byte] =
return @[
byte(value and 0xFF),
byte((value shr 8) and 0xFF),
byte((value shr 16) and 0xFF),
byte((value shr 24) and 0xFF)
]
# Function templates and overwrites
template writeLine*(cq: Conquest, args: varargs[untyped]) =
cq.prompt.writeLine(args)
@@ -153,7 +110,7 @@ proc drawTable*(cq: Conquest, listeners: seq[Listener]) =
for l in listeners:
# Get number of agents connected to the listener
let connectedAgents = cq.agents.values.countIt(it.listener == l.name)
let connectedAgents = cq.agents.values.countIt(it.listenerId == l.name)
let rowCells = @[
Cell(text: l.name, fg: fgGreen),
@@ -217,14 +174,14 @@ proc drawTable*(cq: Conquest, agents: seq[Agent]) =
for a in agents:
var cells = @[
Cell(text: a.name, fg: fgYellow, style: styleBright),
Cell(text: a.agentId, fg: fgYellow, style: styleBright),
Cell(text: a.ip),
Cell(text: a.username),
Cell(text: a.hostname),
Cell(text: a.os),
Cell(text: a.process, fg: if a.elevated: fgRed else: fgWhite),
Cell(text: $a.pid, fg: if a.elevated: fgRed else: fgWhite),
a.timeSince(cq.agents[a.name].latestCheckin)
a.timeSince(cq.agents[a.agentId].latestCheckin)
]
# Highlight agents running within elevated processes