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

@@ -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,13 +60,21 @@ 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)
# Store listener in database
@@ -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