From 42cc58b30bd052c992091895ef6eff4e551c4638 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Fri, 19 Sep 2025 18:31:45 +0200 Subject: [PATCH] Replaced prologue implementation with mummy for listener management, since it seems more suitable for future use (websockets, etc.). --- conquest.nimble | 4 +- src/agent/nim.cfg | 2 +- src/client/layout.ini | 10 +-- src/common/types.nim | 4 +- src/server/api/routes.nim | 71 ++++++++++++-------- src/server/core/builder.nim | 2 +- src/server/core/listener.nim | 123 +++++++++++++++++++---------------- src/server/core/server.nim | 2 +- 8 files changed, 122 insertions(+), 96 deletions(-) diff --git a/conquest.nimble b/conquest.nimble index ad3812b..f13fe73 100644 --- a/conquest.nimble +++ b/conquest.nimble @@ -25,8 +25,8 @@ requires "argparse >= 4.0.2" requires "parsetoml >= 0.7.2" requires "nimcrypto >= 0.6.4" requires "tiny_sqlite >= 0.2.0" -requires "prologue >= 0.6.6" requires "winim >= 3.9.4" requires "ptr_math >= 0.3.0" requires "imguin >= 1.92.2.1" -requires "zippy >= 0.10.16" \ No newline at end of file +requires "zippy >= 0.10.16" +requires "mummy >= 0.4.6" \ No newline at end of file diff --git a/src/agent/nim.cfg b/src/agent/nim.cfg index 741e861..58ce698 100644 --- a/src/agent/nim.cfg +++ b/src/agent/nim.cfg @@ -3,6 +3,6 @@ -d:release --opt:size --passL:"-s" # Strip symbols, such as sensitive function names --d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" +-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER" -d:MODULES=1 -o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe" \ No newline at end of file diff --git a/src/client/layout.ini b/src/client/layout.ini index 5f04485..a2e9a8b 100644 --- a/src/client/layout.ini +++ b/src/client/layout.ini @@ -1,24 +1,24 @@ [Window][Sessions [Table View]] Pos=10,43 -Size=1533,389 +Size=1533,946 Collapsed=0 DockId=0x00000003,0 [Window][Listeners] Pos=10,43 -Size=1533,389 +Size=1533,946 Collapsed=0 DockId=0x00000003,1 [Window][Eventlog] Pos=1545,43 -Size=353,389 +Size=353,946 Collapsed=0 DockId=0x00000004,0 [Window][Dear ImGui Demo] Pos=1545,43 -Size=353,389 +Size=353,946 Collapsed=0 DockId=0x00000004,1 @@ -139,6 +139,6 @@ DockNode ID=0x00000009 Pos=100,200 Size=754,103 Selected=0x64D005CF DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y DockNode ID=0x00000005 Parent=0x85940918 SizeRef=1888,389 Split=X DockNode ID=0x00000003 Parent=0x00000005 SizeRef=1533,159 CentralNode=1 Selected=0x61E02D75 - DockNode ID=0x00000004 Parent=0x00000005 SizeRef=353,159 Selected=0x0FA43D88 + DockNode ID=0x00000004 Parent=0x00000005 SizeRef=353,159 Selected=0x5E5F7166 DockNode ID=0x00000006 Parent=0x85940918 SizeRef=1888,555 Selected=0x65D642C0 diff --git a/src/common/types.nim b/src/common/types.nim index b51a639..7753b01 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -2,6 +2,7 @@ import prompt import tables import times import parsetoml +import mummy # Custom Binary Task structure const @@ -196,6 +197,7 @@ type HTTP = "http" Listener* = ref object of RootObj + server*: Server listenerId*: string address*: string port*: int @@ -212,7 +214,7 @@ type Conquest* = ref object prompt*: Prompt dbPath*: string - listeners*: Table[string, Listener] + listeners*: Table[string, tuple[listener: Listener, thread: Thread[Listener]]] agents*: Table[string, Agent] interactAgent*: Agent keyPair*: KeyPair diff --git a/src/server/api/routes.nim b/src/server/api/routes.nim index bf9e97c..9f9467f 100644 --- a/src/server/api/routes.nim +++ b/src/server/api/routes.nim @@ -1,4 +1,4 @@ -import prologue, terminal, strformat, parsetoml, tables +import mummy, terminal, strformat, parsetoml, tables import strutils, base64 import ./handlers @@ -6,15 +6,32 @@ import ../globals import ../core/logger import ../../common/[types, utils, serialize, profile] -proc error404*(ctx: Context) {.async.} = - resp "", Http404 +# Not Found +proc error404*(request: Request) = + request.respond(404, body = "") + +# Method not allowed +proc error405*(request: Request) = + request.respond(404, body = "") + +# Utils +proc hasKey(headers: seq[(string, string)], headerName: string): bool = + for (name, value) in headers: + if name.toLower() == headerName.toLower(): + return true + return false + +proc get(headers: seq[(string, string)], headerName: string): string = + for (name, value) in headers: + if name.toLower() == headerName.toLower(): + return value + return "" #[ GET Called from agent to check for new tasks ]# -proc httpGet*(ctx: Context) {.async.} = - +proc httpGet*(request: Request) = {.cast(gcsafe).}: # Check heartbeat metadata placement @@ -24,17 +41,16 @@ proc httpGet*(ctx: Context) {.async.} = 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): - resp "", Http404 + if not request.headers.hasKey(heartbeatHeader): + request.respond(404, body = "") return - - heartbeatString = ctx.request.getHeader(heartbeatHeader)[0] + heartbeatString = request.headers.get(heartbeatHeader) of "parameter": let param = cq.profile.getString("http-get.agent.heartbeat.placement.name") - heartbeatString = ctx.getQueryParams(param) + heartbeatString = request.queryParams.get(param) if heartbeatString.len <= 0: - resp "", Http404 + request.respond(404, body = "") return of "uri": @@ -60,7 +76,7 @@ proc httpGet*(ctx: Context) {.async.} = let tasks: seq[seq[byte]] = getTasks(heartbeat) if tasks.len <= 0: - resp "", Http200 + request.respond(200, body = "") return # Create response, containing number of tasks, as well as length and content of each task @@ -73,7 +89,6 @@ proc httpGet*(ctx: Context) {.async.} = # Apply data transformation to the response var response: string - case cq.profile.getString("http-get.server.output.encoding.type", default = "none"): of "none": response = Bytes.toString(responseBytes) @@ -85,52 +100,52 @@ proc httpGet*(ctx: Context) {.async.} = let suffix = cq.profile.getString("http-get.server.output.suffix") # Add headers, as defined in the team server profile + var headers: HttpHeaders for header, value in cq.profile.getTable("http-get.server.headers"): - ctx.response.setHeader(header, value.getStringValue()) + headers.add((header, value.getStringValue())) - await ctx.respond(Http200, prefix & response & suffix, ctx.response.headers) - ctx.handled = true # Ensure that HTTP response is sent only once + request.respond(200, headers = headers, body = prefix & response & suffix) # Notify operator that agent collected tasks cq.info(fmt"{$response.len} bytes sent.") except CatchableError: - resp "", Http404 + request.respond(404, body = "") #[ POST Called from agent to register itself or post results of a task ]# -proc httpPost*(ctx: Context) {.async.} = - +proc httpPost*(request: Request) = {.cast(gcsafe).}: # Check headers # If POST data is not binary data, return 404 error code - if ctx.request.contentType != "application/octet-stream": - resp "", Http404 + if request.headers.get("Content-Type") != "application/octet-stream": + request.respond(404, body = "") return try: # Differentiate between registration and task result packet - var unpacker = Unpacker.init(ctx.request.body) + var unpacker = Unpacker.init(request.body) let header = unpacker.deserializeHeader() # Add response headers, as defined in team server profile + var headers: HttpHeaders for header, value in cq.profile.getTable("http-post.server.headers"): - ctx.response.setHeader(header, value.getStringValue()) + headers.add((header, value.getStringValue())) if cast[PacketType](header.packetType) == MSG_REGISTER: - if not register(string.toBytes(ctx.request.body)): - resp "", Http400 + if not register(string.toBytes(request.body)): + request.respond(400, body = "") return elif cast[PacketType](header.packetType) == MSG_RESULT: - handleResult(string.toBytes(ctx.request.body)) + handleResult(string.toBytes(request.body)) - resp "", Http200 + request.respond(200, body = "") except CatchableError: - resp "", Http404 + request.respond(404, body = "") return \ No newline at end of file diff --git a/src/server/core/builder.nim b/src/server/core/builder.nim index a3b2a8d..210753c 100644 --- a/src/server/core/builder.nim +++ b/src/server/core/builder.nim @@ -141,7 +141,7 @@ proc agentBuild*(cq: Conquest, listener, sleepDelay: string, sleepTechnique: str cq.error(fmt"Listener {listener.toUpperAscii} does not exist.") return false - let listener = cq.listeners[listener.toUpperAscii] + let listener = cq.listeners[listener.toUpperAscii].listener var config: seq[byte] if sleepDelay.isEmptyOrWhitespace(): diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim index 9ef5d9c..19ffcee 100644 --- a/src/server/core/listener.nim +++ b/src/server/core/listener.nim @@ -1,19 +1,14 @@ import strformat, strutils, terminal -import prologue, parsetoml +import mummy, mummy/routers +import parsetoml +import ../globals import ../utils import ../api/routes import ../db/database import ../core/logger import ../../common/[types, utils, profile] -# Utility functions -proc delListener(cq: Conquest, listenerName: string) = - cq.listeners.del(listenerName) - -proc add(cq: Conquest, listener: Listener) = - cq.listeners[listener.listenerId] = listener - #[ Listener management ]# @@ -36,105 +31,111 @@ proc listenerList*(cq: Conquest) = let listeners = cq.dbGetAllListeners() cq.drawTable(listeners) +proc serve(listener: Listener) {.thread.} = + try: + listener.server.serve(Port(listener.port), listener.address) + except Exception: + discard + proc listenerStart*(cq: Conquest, host: string, portStr: string) = # Validate arguments try: if not validatePort(portStr): - raise newException(CatchableError,fmt"[ - ] Invalid port number: {portStr}") + raise newException(CatchableError, fmt"[ - ] Invalid port number: {portStr}") let port = portStr.parseInt # Create new listener - let - name: string = generateUUID() - listenerSettings = newSettings( - appName = name, - debug = false, - address = "", # For some reason, the program crashes when the ip parameter is passed to the newSettings function - port = Port(port) # As a result, I will hardcode the listener to be served on all interfaces (0.0.0.0) by default - ) # TODO: fix this issue and start the listener on the address passed as the HOST parameter + let name: string = generateUUID() + var router: Router + router.notFoundHandler = routes.error404 + router.methodNotAllowedHandler = routes.error405 - var listener = newApp(settings = listenerSettings) - # Define API endpoints based on C2 profile # GET requests for endpoint in cq.profile.getArray("http-get.endpoints"): - listener.addRoute(endpoint.getStringValue(), routes.httpGet) + router.addRoute("GET", endpoint.getStringValue(), routes.httpGet) # POST requests - var postMethods: seq[HttpMethod] + var postMethods: seq[string] for reqMethod in cq.profile.getArray("http-post.request-methods"): - postMethods.add(parseEnum[HttpMethod](reqMethod.getStringValue())) + postMethods.add(reqMethod.getStringValue()) # Default method is POST if postMethods.len == 0: - postMethods = @[HttpPost] + postMethods = @["POST"] for endpoint in cq.profile.getArray("http-post.endpoints"): - listener.addRoute(endpoint.getStringValue(), routes.httpPost, postMethods) + for httpMethod in postMethods: + router.addRoute(httpMethod, endpoint.getStringValue(), routes.httpPost) - listener.registerErrorHandler(Http404, routes.error404) + let server = newServer(router.toHandler()) # Store listener in database - var listenerInstance = Listener( + var listener = Listener( + server: server, listenerId: name, address: host, port: port, protocol: HTTP ) - if not cq.dbStoreListener(listenerInstance): - raise newException(CatchableError, "Failed to store listener in database.") # Start serving - discard listener.runAsync() - cq.add(listenerInstance) + var thread: Thread[Listener] + createThread(thread, serve, listener) + server.waitUntilReady() + + cq.listeners[name] = (listener, thread) + if not cq.dbStoreListener(listener): + raise newException(CatchableError, "Failed to store listener in database.") + cq.success("Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on {host}:{portStr}.") except CatchableError as err: cq.error("Failed to start listener: ", err.msg) -proc restartListeners*(cq: Conquest) = - let listeners: seq[Listener] = cq.dbGetAllListeners() +proc restartListeners*(cq: Conquest) = + var listeners: seq[Listener] = cq.dbGetAllListeners() # Restart all active listeners that are stored in the database - for l in listeners: + for listener in listeners: try: - let - settings = newSettings( - appName = l.listenerId, - debug = false, - address = "", - port = Port(l.port) - ) - listener = newApp(settings = settings) - + # Create new listener + let name: string = generateUUID() + var router: Router + router.notFoundHandler = routes.error404 + router.methodNotAllowedHandler = routes.error405 + # Define API endpoints based on C2 profile # GET requests for endpoint in cq.profile.getArray("http-get.endpoints"): - listener.get(endpoint.getStringValue(), routes.httpGet) + router.addRoute("GET", endpoint.getStringValue(), routes.httpGet) # POST requests - var postMethods: seq[HttpMethod] + var postMethods: seq[string] for reqMethod in cq.profile.getArray("http-post.request-methods"): - postMethods.add(parseEnum[HttpMethod](reqMethod.getStringValue())) + postMethods.add(reqMethod.getStringValue()) # Default method is POST if postMethods.len == 0: - postMethods = @[HttpPost] + postMethods = @["POST"] for endpoint in cq.profile.getArray("http-post.endpoints"): - listener.addRoute(endpoint.getStringValue(), routes.httpPost, postMethods) - - listener.registerErrorHandler(Http404, routes.error404) + for httpMethod in postMethods: + router.addRoute(httpMethod, endpoint.getStringValue(), routes.httpPost) - discard listener.runAsync() - cq.add(l) - cq.success("Restarted listener", fgGreen, fmt" {l.listenerId} ", resetStyle, fmt"on {l.address}:{$l.port}.") - - # Delay before serving another listener to avoid crashing the application - waitFor sleepAsync(100) - + let server = newServer(router.toHandler()) + listener.server = server + + # Start serving + var thread: Thread[Listener] + createThread(thread, serve, listener) + server.waitUntilReady() + + cq.listeners[listener.listenerId] = (listener, thread) + cq.success("Restarted listener", fgGreen, fmt" {listener.listenerId} ", resetStyle, fmt"on {listener.address}:{$listener.port}.") + except CatchableError as err: cq.error("Failed to restart listener: ", err.msg) @@ -154,6 +155,14 @@ proc listenerStop*(cq: Conquest, name: string) = cq.error("Failed to stop listener: ", getCurrentExceptionMsg()) return - cq.delListener(name) + cq.listeners.del(name) cq.success("Stopped listener ", fgGreen, name.toUpperAscii, resetStyle, ".") + + # TODO: Make listener stoppable + # try: + # cq.listeners[name].listener .server.close() + # joinThread(cq.listeners[name].thread) + # except: + # cq.error("Failed to stop listener.") + \ No newline at end of file diff --git a/src/server/core/server.nim b/src/server/core/server.nim index 4f3cdc2..e8770f2 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -129,7 +129,7 @@ proc header() = proc init*(T: type Conquest, profile: Profile): Conquest = var cq = new Conquest cq.prompt = Prompt.init() - cq.listeners = initTable[string, Listener]() + cq.listeners = initTable[string, tuple[listener: Listener, thread: Thread[Listener]]]() cq.agents = initTable[string, Agent]() cq.interactAgent = nil cq.profile = profile