Updated profile system, including dynamic parsing of hidden heartbeats and setting of response headers.
This commit is contained in:
@@ -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
|
||||
[http-get]
|
||||
uri = [
|
||||
"/tasks",
|
||||
endpoints = [
|
||||
"/get",
|
||||
"/api/v1.2/status.js"
|
||||
]
|
||||
|
||||
# Defines where the heartbeat is placed within the HTTP GET request
|
||||
# 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
|
||||
# Encoding is only applied to the payload and not the prepended or appended strings
|
||||
[http-get.agent.heartbeat]
|
||||
encoding = "base64url"
|
||||
prepend = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
|
||||
append = ".KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
|
||||
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" }
|
||||
# prefix = "PHPSESSID="
|
||||
# suffix = ", path=/"
|
||||
# encoding = { type = "base64", url-safe = true }
|
||||
|
||||
# Other examples
|
||||
# placement = { type = "parameter", name = "id" }
|
||||
# placement = { type = "uri" }
|
||||
# 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
|
||||
[http-get.agent.headers]
|
||||
"Cache-Control" = "no-cache"
|
||||
Cache-Control = "no-cache"
|
||||
|
||||
# Defines arbitrary headers that are added to the server's response
|
||||
[http-get.server.headers]
|
||||
"Server" = "nginx"
|
||||
"X-CONQUEST-VERSION" = "0.1"
|
||||
Server = "nginx"
|
||||
Content-Type = "application/octet-stream"
|
||||
Connection = "Keep-Alive"
|
||||
|
||||
# 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
|
||||
@@ -58,24 +67,21 @@ placement = { type = "body" }
|
||||
# ----------------------------------------------------------
|
||||
# Defines URI endpoints for HTTP POST requests
|
||||
[http-post]
|
||||
uri = [
|
||||
"/results",
|
||||
endpoints = [
|
||||
"/post",
|
||||
"/api/v2/get.js"
|
||||
]
|
||||
request_methods = [
|
||||
"POST",
|
||||
"PUT"
|
||||
]
|
||||
|
||||
[http-post.agent.headers]
|
||||
Content-Type = "application/octet-stream"
|
||||
Connection = "Keep-Alive"
|
||||
Cache-Control = "no-cache"
|
||||
|
||||
[http-post.agent.output]
|
||||
placement = { type = "body" }
|
||||
|
||||
[http-post.server.headers]
|
||||
"Server" = "nginx"
|
||||
"X-CONQUEST-VERSION" = "0.1"
|
||||
Server = "nginx"
|
||||
|
||||
[http-post.server.output]
|
||||
placement = { type = "body" }
|
||||
@@ -1,9 +1,9 @@
|
||||
# Agent configuration
|
||||
-d:ListenerUuid="7147A315"
|
||||
-d:Octet1="127"
|
||||
-d:Octet2="0"
|
||||
-d:Octet3="0"
|
||||
-d:Octet4="1"
|
||||
-d:ListenerPort=5555
|
||||
-d:ListenerUuid="03FBA764"
|
||||
-d:Octet1="172"
|
||||
-d:Octet2="29"
|
||||
-d:Octet3="177"
|
||||
-d:Octet4="43"
|
||||
-d:ListenerPort=7777
|
||||
-d:SleepDelay=5
|
||||
-d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U="
|
||||
|
||||
@@ -157,11 +157,6 @@ type
|
||||
port*: int
|
||||
protocol*: Protocol
|
||||
|
||||
HttpListener* = ref object of Listener
|
||||
register_endpoint*: string
|
||||
get_endpoint*: string
|
||||
post_endpoint*: string
|
||||
|
||||
# Server context structure
|
||||
type
|
||||
KeyPair* = object
|
||||
|
||||
@@ -33,13 +33,13 @@ proc register*(registrationData: seq[byte]): bool =
|
||||
|
||||
return true
|
||||
|
||||
proc getTasks*(checkinData: seq[byte]): seq[seq[byte]] =
|
||||
proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] =
|
||||
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
# Deserialize checkin request to obtain agentId and listenerId
|
||||
let
|
||||
request: Heartbeat = cq.deserializeHeartbeat(checkinData)
|
||||
request: Heartbeat = cq.deserializeHeartbeat(heartbeat)
|
||||
agentId = uuidToString(request.header.agentId)
|
||||
listenerId = uuidToString(request.listenerId)
|
||||
timestamp = request.timestamp
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import prologue, json, terminal, strformat
|
||||
import prologue, json, terminal, strformat, parsetoml, tables
|
||||
import sequtils, strutils, times, base64
|
||||
|
||||
import ./handlers
|
||||
@@ -9,75 +9,113 @@ proc error404*(ctx: Context) {.async.} =
|
||||
resp "", Http404
|
||||
|
||||
#[
|
||||
GET /tasks
|
||||
GET
|
||||
Called from agent to check for new tasks
|
||||
]#
|
||||
proc httpGet*(ctx: Context) {.async.} =
|
||||
|
||||
# Check headers
|
||||
# Heartbeat data is hidden base64-encoded within "Authorization: Bearer" header, between a prefix and suffix
|
||||
if not ctx.request.hasHeader("Authorization"):
|
||||
resp "", Http404
|
||||
return
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
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:
|
||||
var response: seq[byte]
|
||||
let tasks: seq[seq[byte]] = getTasks(checkinData)
|
||||
case heartbeatPlacement:
|
||||
of "header":
|
||||
let heartbeatHeader = cq.profile["http-get"]["agent"]["heartbeat"]["placement"]["name"].getStr()
|
||||
if not ctx.request.hasHeader(heartbeatHeader):
|
||||
resp "", Http404
|
||||
return
|
||||
|
||||
if tasks.len <= 0:
|
||||
resp "", Http200
|
||||
return
|
||||
heartbeatString = ctx.request.getHeader(heartbeatHeader)[0]
|
||||
|
||||
# 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))
|
||||
of "parameter":
|
||||
discard
|
||||
of "uri":
|
||||
discard
|
||||
of "body":
|
||||
discard
|
||||
else: discard
|
||||
|
||||
for task in tasks:
|
||||
response.add(uint32.toBytes(uint32(task.len)))
|
||||
response.add(task)
|
||||
|
||||
await ctx.respond(
|
||||
code = Http200,
|
||||
body = Bytes.toString(response)
|
||||
)
|
||||
# Retrieve and apply data transformation to get raw heartbeat packet
|
||||
let
|
||||
encoding = cq.profile["http-get"]["agent"]["heartbeat"]["encoding"]["type"].getStr("none")
|
||||
prefix = cq.profile["http-get"]["agent"]["heartbeat"]["prefix"].getStr("")
|
||||
suffix = cq.profile["http-get"]["agent"]["heartbeat"]["suffix"].getStr("")
|
||||
|
||||
# Notify operator that agent collected tasks
|
||||
{.cast(gcsafe).}:
|
||||
let encHeartbeat = heartbeatString[len(prefix) ..^ len(suffix) + 1]
|
||||
|
||||
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")
|
||||
cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$response.len} bytes sent.")
|
||||
|
||||
except CatchableError:
|
||||
resp "", Http404
|
||||
except CatchableError:
|
||||
resp "", Http404
|
||||
|
||||
#[
|
||||
POST /results
|
||||
Called from agent to post results of a task
|
||||
POST
|
||||
Called from agent to register itself or post results of a task
|
||||
]#
|
||||
proc httpPost*(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
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
try:
|
||||
# Differentiate between registration and task result packet
|
||||
var unpacker = Unpacker.init(ctx.request.body)
|
||||
let header = unpacker.deserializeHeader()
|
||||
# Check headers
|
||||
# If POST data is not binary data, return 404 error code
|
||||
if ctx.request.contentType != "application/octet-stream":
|
||||
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
|
||||
|
||||
elif cast[PacketType](header.packetType) == MSG_RESULT:
|
||||
handleResult(string.toBytes(ctx.request.body))
|
||||
|
||||
except CatchableError:
|
||||
resp "", Http404
|
||||
except CatchableError:
|
||||
resp "", Http404
|
||||
|
||||
return
|
||||
return
|
||||
@@ -1,5 +1,7 @@
|
||||
import strformat, strutils, sequtils, terminal
|
||||
import prologue
|
||||
import prologue, parsetoml
|
||||
import sugar
|
||||
|
||||
|
||||
import ../utils
|
||||
import ../api/routes
|
||||
@@ -13,15 +15,6 @@ proc delListener(cq: Conquest, listenerName: string) =
|
||||
proc add(cq: Conquest, listener: 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
|
||||
]#
|
||||
@@ -65,13 +58,24 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) =
|
||||
|
||||
var listener = newApp(settings = listenerSettings)
|
||||
|
||||
# Define API endpoints
|
||||
listener.get("get", routes.httpGet)
|
||||
listener.post("post", routes.httpPost)
|
||||
# Define API endpoints based on C2 profile
|
||||
# GET requests
|
||||
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)
|
||||
|
||||
# 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):
|
||||
return
|
||||
|
||||
@@ -97,11 +101,18 @@ proc restartListeners*(cq: Conquest) =
|
||||
)
|
||||
listener = newApp(settings = settings)
|
||||
|
||||
# Define API endpoints
|
||||
listener.get("get", routes.httpGet)
|
||||
listener.post("post", routes.httpPost)
|
||||
listener.registerErrorHandler(Http404, routes.error404)
|
||||
# Define API endpoints based on C2 profile
|
||||
# TODO: Store endpoints for already running listeners is DB (comma-separated) and use those values for restarts
|
||||
# GET requests
|
||||
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:
|
||||
discard listener.runAsync()
|
||||
cq.add(l)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Compiler flags
|
||||
-d:server
|
||||
--threads:on
|
||||
-d:httpxServerName="nginx"
|
||||
-d:httpxServerName=""
|
||||
--outdir:"../bin"
|
||||
--out:"server"
|
||||
Reference in New Issue
Block a user