From c97cb4585f2bb4c9b0e6e4e38deaae82238c6e5d Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:25:15 +0200 Subject: [PATCH] Removed prompt user intreface; Team server and Client are now fully separated. --- conquest.nimble | 2 - data/profile.toml | 8 ++ src/client/main.nim | 6 +- src/client/views/console.nim | 7 +- src/client/views/sessions.nim | 43 ++++-- src/common/types.nim | 2 - src/server/core/agent.nim | 108 +------------- src/server/core/listener.nim | 78 +--------- src/server/core/logger.nim | 4 +- src/server/core/server.nim | 264 ---------------------------------- src/server/main.nim | 133 ++++++++++++++++- src/server/utils.nim | 161 +-------------------- 12 files changed, 188 insertions(+), 628 deletions(-) delete mode 100644 src/server/core/server.nim diff --git a/conquest.nimble b/conquest.nimble index ee610dc..ad5ab25 100644 --- a/conquest.nimble +++ b/conquest.nimble @@ -20,8 +20,6 @@ task client, "Build conquest client binary": requires "nim >= 2.2.4" -requires "prompt >= 0.0.1" -requires "argparse >= 4.0.2" requires "parsetoml >= 0.7.2" requires "nimcrypto >= 0.6.4" requires "tiny_sqlite >= 0.2.0" diff --git a/data/profile.toml b/data/profile.toml index 454f516..bca89e5 100644 --- a/data/profile.toml +++ b/data/profile.toml @@ -2,10 +2,18 @@ name = "cq-default-profile" + # Important file paths and locations private-key-file = "data/keys/conquest-server_x25519_private.key" database-file = "data/conquest.db" +# Team server settings (WebSocket server port, users, ...) +[team-server] +port = 37573 + +[server.users] + + # General agent settings [agent] sleep = 5 diff --git a/src/client/main.nim b/src/client/main.nim index 1b449ca..c9e6f37 100644 --- a/src/client/main.nim +++ b/src/client/main.nim @@ -7,7 +7,7 @@ import ./websocket import sugar -proc main() = +proc main(ip: string = "localhost", port: int = 37573) = var app = createApp(1024, 800, imnodes = true, title = "Conquest", docking = true) defer: app.destroyApp() @@ -40,7 +40,7 @@ proc main() = let io = igGetIO() # Initiate WebSocket connection - let ws = newWebSocket("ws://localhost:12345") + let ws = newWebSocket(fmt"ws://{ip}:{$port}") defer: ws.close() # main loop @@ -152,4 +152,4 @@ proc main() = app.handle.setWindowShouldClose(true) when isMainModule: - main() + import cligen; dispatch main diff --git a/src/client/views/console.nim b/src/client/views/console.nim index 34461ab..44dfabb 100644 --- a/src/client/views/console.nim +++ b/src/client/views/console.nim @@ -259,7 +259,6 @@ proc draw*(component: ConsoleComponent, ws: WebSocket) = #[ Filter & Options ]# - var availableSize: ImVec2 igGetContentRegionAvail(addr availableSize) var labelSize: ImVec2 @@ -293,17 +292,17 @@ proc draw*(component: ConsoleComponent, ws: WebSocket) = let childWindowFlags = ImGuiChildFlags_NavFlattened.int32 or ImGui_ChildFlags_Borders.int32 or ImGui_ChildFlags_AlwaysUseWindowPadding.int32 or ImGuiChildFlags_FrameStyle.int32 if igBeginChild_Str("##Console", vec2(-1.0f, -footerHeight), childWindowFlags, ImGuiWindowFlags_HorizontalScrollbar.int32): + # Display console items - for item in component.console.items: # Apply filter if component.filter.ImGuiTextFilter_IsActive(): if not component.filter.ImGuiTextFilter_PassFilter(item.getText(), nil): continue + + item.print() - item.print() - component.textSelect.textselect_update() # Auto-scroll to bottom diff --git a/src/client/views/sessions.nim b/src/client/views/sessions.nim index b69cf2e..2390fbe 100644 --- a/src/client/views/sessions.nim +++ b/src/client/views/sessions.nim @@ -1,4 +1,4 @@ -import times, tables, strformat, strutils +import times, tables, strformat, strutils, algorithm import imguin/[cimgui, glfw_opengl, simple] import ./console @@ -21,6 +21,9 @@ proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent] result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage() result.consoles = consoles +proc cmp(x, y: UIAgent): int = + return cmp(x.firstCheckin, y.firstCheckin) + proc interact(component: SessionsTableComponent) = # Open a new console for each selected agent session var it: pointer = nil @@ -41,7 +44,6 @@ proc interact(component: SessionsTableComponent) = proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = igBegin(component.title, showComponent, 0) - defer: igEnd() let tableFlags = ( ImGuiTableFlags_Resizable.int32 or @@ -57,10 +59,11 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = ImGui_TableFlags_SizingStretchSame.int32 ) - let cols: int32 = 9 + let cols: int32 = 11 if igBeginTable("Sessions", cols, tableFlags, vec2(0.0f, 0.0f), 0.0f): igTableSetupColumn("AgentID", ImGuiTableColumnFlags_NoReorder.int32 or ImGuiTableColumnFlags_NoHide.int32, 0.0f, 0) + igTableSetupColumn("ListenerID", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0) igTableSetupColumn("Address", ImGuiTableColumnFlags_None.int32, 0.0f, 0) igTableSetupColumn("Username", ImGuiTableColumnFlags_None.int32, 0.0f, 0) igTableSetupColumn("Hostname", ImGuiTableColumnFlags_None.int32, 0.0f, 0) @@ -68,6 +71,7 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = igTableSetupColumn("OS", ImGuiTableColumnFlags_None.int32, 0.0f, 0) igTableSetupColumn("Process", ImGuiTableColumnFlags_None.int32, 0.0f, 0) igTableSetupColumn("PID", ImGuiTableColumnFlags_None.int32, 0.0f, 0) + igTableSetupColumn("First seen", ImGuiTableColumnFlags_None.int32, 0.0f, 0) igTableSetupColumn("Last seen", ImGuiTableColumnFlags_None.int32, 0.0f, 0) igTableSetupScrollFreeze(0, 1) @@ -76,6 +80,8 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = var multiSelectIO = igBeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape.int32 or ImGuiMultiSelectFlags_BoxSelect1d.int32, component.selection[].Size, int32(component.agents.len())) ImGuiSelectionBasicStorage_ApplyRequests(component.selection, multiSelectIO) + # Sort sessions table based on first checkin + component.agents.sort(cmp) for row, agent in component.agents: igTableNextRow(ImGuiTableRowFlags_None.int32, 0.0f) @@ -89,22 +95,35 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = # Interact with session on double-click if igIsMouseDoubleClicked_Nil(ImGui_MouseButton_Left.int32): component.interact() - + if igTableSetColumnIndex(1): - igText(agent.ip) + igText(agent.listenerId) if igTableSetColumnIndex(2): - igText(agent.username) + igText(agent.ip) if igTableSetColumnIndex(3): - igText(agent.hostname) + igText(agent.username) if igTableSetColumnIndex(4): - igText(if agent.domain.isEmptyOrWhitespace(): "-" else: agent.domain) + igText(agent.hostname) if igTableSetColumnIndex(5): - igText(agent.os) + igText(if agent.domain.isEmptyOrWhitespace(): "-" else: agent.domain) if igTableSetColumnIndex(6): - igText(agent.process) + igText(agent.os) if igTableSetColumnIndex(7): - igText($agent.pid) + igText(agent.process) if igTableSetColumnIndex(8): + igText($agent.pid) + if igTableSetColumnIndex(9): + let duration = now() - agent.firstCheckin.fromUnix().utc() + let totalSeconds = duration.inSeconds + + let hours = totalSeconds div 3600 + let minutes = (totalSeconds mod 3600) div 60 + let seconds = totalSeconds mod 60 + + let timeText = dateTime(2000, mJan, 1, hours.int, minutes.int, seconds.int).format("HH:mm:ss") + igText(fmt"{timeText} ago") + + if igTableSetColumnIndex(10): let duration = now() - component.agentActivity[agent.agentId].fromUnix().utc() let totalSeconds = duration.inSeconds @@ -148,3 +167,5 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = igSetScrollHereY(1.0f) igEndTable() + + igEnd() \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index 905e37f..66ae7db 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -1,4 +1,3 @@ -import prompt import tables import times import parsetoml, json @@ -276,7 +275,6 @@ type ws*: WebSocket Conquest* = ref object - prompt*: Prompt dbPath*: string listeners*: Table[string, Listener] threads*: Table[string, Thread[Listener]] diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index ca9ce48..da50556 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -1,4 +1,4 @@ -import terminal, strformat, strutils, tables, times, system, parsetoml, prompt +import terminal, strformat, strutils, tables, times, system, parsetoml import ../utils import ../core/logger @@ -6,83 +6,6 @@ import ../db/database import ../../common/types import ../websocket -# Utility functions -proc addMultiple*(cq: Conquest, agents: seq[Agent]) = - for a in agents: - cq.agents[a.agentId] = a - -proc delAgent*(cq: Conquest, agentName: string) = - cq.agents.del(agentName) - -proc getAgentsAsSeq*(cq: Conquest): seq[Agent] = - var agents: seq[Agent] = @[] - for agent in cq.agents.values: - agents.add(agent) - return agents - -#[ - Agent management -]# -proc agentUsage*(cq: Conquest) = - cq.output("""Manage, build and interact with agents. - -Usage: - agent [options] COMMAND - -Commands: - - list List all agents. - info Display details for a specific agent. - kill Terminate the connection of an active listener and remove it from the interface. - interact Interact with an active agent. - build Generate a new agent to connect to an active listener. - -Options: - -h, --help""") - -# List agents -proc agentList*(cq: Conquest, listener: string) = - - # If no argument is passed via -n, list all agents, otherwise only display agents connected to a specific listener - if listener == "": - cq.drawTable(cq.dbGetAllAgents()) - - else: - # Check if listener exists - if not cq.dbListenerExists(listener.toUpperAscii): - cq.error(fmt"Listener {listener.toUpperAscii} does not exist.") - return - - cq.drawTable(cq.dbGetAllAgentsByListener(listener.toUpperAscii)) - - -# Display agent properties and details -proc agentInfo*(cq: Conquest, name: string) = - # Check if agent supplied via -n parameter exists in database - if not cq.dbAgentExists(name.toUpperAscii): - cq.error(fmt"Agent {name.toUpperAscii} does not exist.") - return - - let agent = cq.agents[name.toUpperAscii] - - # TODO: Improve formatting - cq.output(fmt""" -Agent name (UUID): {agent.agentId} -Connected to listener: {agent.listenerId} -────────────────────────────────────────── -Username: {agent.username} -Hostname: {agent.hostname} -Domain: {agent.domain} -IP-Address: {agent.ip} -Operating system: {agent.os} -────────────────────────────────────────── -Process name: {agent.process} -Process ID: {$agent.pid} -Process elevated: {$agent.elevated} -First checkin: {agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss")} -Latest checkin: {agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss")} -""") - # Terminate agent and remove it from the database proc agentKill*(cq: Conquest, name: string) = @@ -100,32 +23,5 @@ proc agentKill*(cq: Conquest, name: string) = cq.error("Failed to terminate agent: ", getCurrentExceptionMsg()) return - cq.delAgent(name) + cq.agents.del(name) cq.success("Terminated agent ", fgYellow, styleBright, name.toUpperAscii, resetStyle, ".") - -# Switch to interact mode -proc agentInteract*(cq: Conquest, name: string) = - - discard - # Verify that agent exists - # if not cq.dbAgentExists(name.toUpperAscii): - # cq.error(fmt"Agent {name.toUpperAscii} does not exist.") - # return - - # let agent = cq.agents[name.toUpperAscii] - # var command: string = "" - - # # Change prompt indicator to show agent interaction - # cq.interactAgent = agent - # cq.prompt.setIndicator(fmt"[{agent.agentId}]> ") - # cq.prompt.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")]) - - # cq.info("Started interacting with agent ", fgYellow, styleBright, agent.agentId, resetStyle, ". Type 'help' to list available commands.\n") - - # while command.replace(" ", "") != "back": - # command = cq.prompt.readLine() - # cq.handleAgentCommand(name, command) - - # # Reset interactAgent field after interaction with agent is ended using 'back' command - # cq.interactAgent = nil - diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim index 200e0a5..de30689 100644 --- a/src/server/core/listener.nim +++ b/src/server/core/listener.nim @@ -10,28 +10,6 @@ import ../core/logger import ../../common/[types, utils, profile] import ../websocket -#[ - Listener management -]# -proc listenerUsage*(cq: Conquest) = - cq.output("""Manage, start and stop listeners. - -Usage: - listener [options] COMMAND - -Commands: - - list List all active listeners. - start Starts a new HTTP listener. - stop Stop an active listener. - -Options: - -h, --help""") - -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) @@ -82,8 +60,9 @@ proc listenerStart*(cq: Conquest, name: string, host: string, port: int, protoco cq.listeners[name] = listener cq.threads[name] = thread - if not cq.dbStoreListener(listener): - raise newException(CatchableError, "Failed to store listener in database.") + if not cq.dbListenerExists(name.toUpperAscii): + 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}:{$port}.") cq.client.sendListener(listener) @@ -93,55 +72,6 @@ proc listenerStart*(cq: Conquest, name: string, host: string, port: int, protoco cq.error("Failed to start listener: ", err.msg) cq.client.sendEventlogItem(LOG_ERROR_SHORT, fmt"Failed to start listener: {err.msg}.") -proc restartListeners*(cq: Conquest) = - var listeners: seq[Listener] = cq.dbGetAllListeners() - - # Restart all active listeners that are stored in the database - for listener in listeners: - try: - # Create new listener - 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"): - router.addRoute("GET", endpoint.getStringValue(), routes.httpGet) - - # POST requests - var postMethods: seq[string] - for reqMethod in cq.profile.getArray("http-post.request-methods"): - postMethods.add(reqMethod.getStringValue()) - - # Default method is POST - if postMethods.len == 0: - postMethods = @["POST"] - - for endpoint in cq.profile.getArray("http-post.endpoints"): - for httpMethod in postMethods: - router.addRoute(httpMethod, endpoint.getStringValue(), routes.httpPost) - - 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 - cq.threads[listener.listenerId] = thread - - cq.client.sendEventlogItem(LOG_SUCCESS_SHORT, fmt"Restarted listener {listener.listenerId} on {listener.address}:{$listener.port}.") - 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) - - cq.output() - - # Remove listener from database, preventing automatic startup on server restart proc listenerStop*(cq: Conquest, name: string) = @@ -158,7 +88,7 @@ proc listenerStop*(cq: Conquest, name: string) = cq.listeners.del(name) cq.success("Stopped listener ", fgGreen, name.toUpperAscii, resetStyle, ".") - # TODO: Make listener stoppable + # TODO: Shutdown listener without server restart. Since the listener is removed from the DB, agents connecting to it after it has been shutdown are not accepted # try: # cq.listeners[name].listener .server.close() # joinThread(cq.listeners[name].thread) diff --git a/src/server/core/logger.nim b/src/server/core/logger.nim index ede28bc..a136464 100644 --- a/src/server/core/logger.nim +++ b/src/server/core/logger.nim @@ -1,4 +1,4 @@ -import times, strformat, strutils, prompt, terminal +import times, strformat, strutils, terminal import std/[dirs, paths] import ../globals @@ -37,7 +37,7 @@ proc getTimestamp*(): string = # Function templates and overwrites template writeLine*(cq: Conquest, args: varargs[untyped] = "") = - cq.prompt.writeLine(args) + stdout.styledWriteLine(args) if cq.interactAgent != nil: cq.log(extractStrings($(args))) diff --git a/src/server/core/server.nim b/src/server/core/server.nim deleted file mode 100644 index dc16753..0000000 --- a/src/server/core/server.nim +++ /dev/null @@ -1,264 +0,0 @@ -import prompt, terminal, argparse, parsetoml, times, json, math -import strutils, strformat, system, tables - -import ./[agent, listener, builder] -import ../globals -import ../db/database -import ../core/logger -import ../../common/[types, crypto, utils, profile, event] -import ../websocket -import mummy, mummy/routers - -#[ - Argument parsing -]# -var parser = newParser: - help("Conquest Command & Control") - nohelpflag() - - command("listener"): - help("Manage, start and stop listeners.") - - command("list"): - help("List all active listeners.") - - command("start"): - help("Starts a new HTTP listener.") - option("-i", "--ip", default=some("127.0.0.1"), help="IPv4 address to listen on.", required=false) - option("-p", "--port", help="Port to listen on.", required=true) - - command("stop"): - help("Stop an active listener.") - option("-n", "--name", help="Name of the listener.", required=true) - - command("agent"): - help("Manage, build and interact with agents.") - - command("list"): - help("List all agents.") - option("-l", "--listener", help="Name of the listener.") - - command("info"): - help("Display details for a specific agent.") - option("-n", "--name", help="Name of the agent.", required=true) - - command("kill"): - help("Terminate the connection of an active listener and remove it from the interface.") - option("-n", "--name", help="Name of the agent.", required=true) - # flag("--self-delete", help="Remove agent executable from target system.") - - command("interact"): - help("Interact with an active agent.") - option("-n", "--name", help="Name of the agent.", required=true) - - command("build"): - help("Generate a new agent to connect to an active listener.") - option("-l", "--listener", help="Name of the listener.", required=true) - option("-s", "--sleep", help="Sleep delay in seconds.") - option("--sleepmask", help="Sleep obfuscation technique.", default=some("none"), choices = @["ekko", "zilean", "foliage", "none"]) - flag("--spoof-stack", help="Use stack duplication to spoof the call stack. Supported by EKKO and ZILEAN techniques.") - - command("help"): - nohelpflag() - - command("exit"): - nohelpflag() - -proc handleConsoleCommand(cq: Conquest, args: string) = - - # Return if no command (or just whitespace) is entered - if args.replace(" ", "").len == 0: return - - cq.input(args) - - try: - let opts = parser.parse(args.split(" ").filterIt(it.len > 0)) - - case opts.command - - of "exit": # Exit program - echo "\n" - quit(0) - - of "help": # Display help menu - cq.output(parser.help()) - - of "listener": - case opts.listener.get.command - of "list": - cq.listenerList() - of "start": - cq.listenerStart(generateUUID(), opts.listener.get.start.get.ip, parseInt(opts.listener.get.start.get.port), HTTP) - discard - of "stop": - cq.listenerStop(opts.listener.get.stop.get.name) - discard - else: - cq.listenerUsage() - - of "agent": - case opts.agent.get.command - of "list": - cq.agentList(opts.agent.get.list.get.listener) - of "info": - cq.agentInfo(opts.agent.get.info.get.name) - of "kill": - cq.agentKill(opts.agent.get.kill.get.name) - of "interact": - cq.agentInteract(opts.agent.get.interact.get.name) - of "build": - discard - # cq.agentBuild(opts.agent.get.build.get.listener, opts.agent.get.build.get.sleep, opts.agent.get.build.get.sleepmask, opts.agent.get.build.get.spoof_stack) - else: - cq.agentUsage() - - # Handle help flag - except ShortCircuit as err: - if err.flag == "argparse_help": - cq.output(err.help) - - # Handle invalid arguments - except CatchableError: - cq.error(getCurrentExceptionMsg()) - - cq.output() - -proc header() = - echo "" - echo "┏┏┓┏┓┏┓┓┏┏┓┏╋" - echo "┗┗┛┛┗┗┫┗┻┗ ┛┗ V0.1" - echo " ┗ @jakobfriedl" - echo "─".repeat(21) - echo "" - -proc init*(T: type Conquest, profile: Profile): Conquest = - var cq = new Conquest - cq.prompt = Prompt.init() - cq.listeners = initTable[string, Listener]() - cq.threads = initTable[string, Thread[Listener]]() - cq.agents = initTable[string, Agent]() - cq.interactAgent = nil - cq.profile = profile - cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & profile.getString("private-key-file")) - cq.dbPath = CONQUEST_ROOT & "/" & profile.getString("database-file") - cq.client = nil - return cq - -#[ - WebSocket -]# -proc upgradeHandler(request: Request) = - {.cast(gcsafe).}: - let ws = request.upgradeToWebSocket() - cq.client = UIClient( - ws: ws - ) - -proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.gcsafe.} = - {.cast(gcsafe).}: - case event: - of OpenEvent: - # New client connected to team server - # Send profile, sessions and listeners to the UI client - cq.client.sendProfile(cq.profile) - for id, listener in cq.listeners: - cq.client.sendListener(listener) - for id, agent in cq.agents: - cq.client.sendAgent(agent) - cq.client.sendEventlogItem(LOG_SUCCESS_SHORT, "CQ-V1") - - of MessageEvent: - # Continuously send heartbeat messages - ws.sendHeartbeat() - - let event = message.recvEvent() - - case event.eventType: - of CLIENT_AGENT_TASK: - let agentId = event.data["agentId"].getStr() - let task = event.data["task"].to(Task) - cq.agents[agentId].tasks.add(task) - - of CLIENT_LISTENER_START: - let listener = event.data.to(UIListener) - cq.listenerStart(listener.listenerId, listener.address, listener.port, listener.protocol) - - of CLIENT_LISTENER_STOP: - let listenerId = event.data["listenerId"].getStr() - cq.listenerStop(listenerId) - - of CLIENT_AGENT_BUILD: - let - listenerId = event.data["listenerId"].getStr() - sleepDelay = event.data["sleepDelay"].getInt() - sleepTechnique = cast[SleepObfuscationTechnique](event.data["sleepTechnique"].getInt()) - spoofStack = event.data["spoofStack"].getBool() - modules = cast[uint32](event.data["modules"].getInt()) - - let payload = cq.agentBuild(listenerId, sleepDelay, sleepTechnique, spoofStack, modules) - if payload.len() != 0: - cq.client.sendAgentPayload(payload) - - else: discard - - of ErrorEvent: - discard - of CloseEvent: - # Set the client instance to nil again to prevent debug error messages - cq.client = nil - -proc serve(server: Server) {.thread.} = - try: - server.serve(Port(12345), "127.0.0.1") - except Exception: - discard - -proc startServer*(profilePath: string) = - - # Ensure that the conquest root directory was passed as a compile-time define - when not defined(CONQUEST_ROOT): - quit(0) - - # Handle CTRL+C, - proc exit() {.noconv.} = - echo "Received CTRL+C. Type \"exit\" to close the application.\n" - setControlCHook(exit) - - header() - - try: - # Initialize framework context - # Load and parse profile - let profile = parsetoml.parseFile(profilePath) - cq = Conquest.init(profile) - - cq.info("Using profile \"", profile.getString("name"), "\" (", profilePath ,").") - - except CatchableError as err: - echo err.msg - quit(0) - - # Initialize database - cq.dbInit() - cq.restartListeners() - cq.addMultiple(cq.dbGetAllAgents()) - - # Start websocket server - var router: Router - router.get("/*", upgradeHandler) - - # Increased websocket message length in order to support dotnet assembly execution - let server = newServer(router, websocketHandler, maxMessageLen = 1024 * 1024 * 1024) - - var thread: Thread[Server] - createThread(thread, serve, server) - - # Main loop - while true: - - cq.prompt.setIndicator("[conquest]> ") - cq.prompt.setStatusBar(@[("[mode]", "manage"), ("[listeners]", $len(cq.listeners)), ("[agents]", $len(cq.agents))]) - cq.prompt.showPrompt() - - var command: string = cq.prompt.readLine() - cq.handleConsoleCommand(command) diff --git a/src/server/main.nim b/src/server/main.nim index 040f26f..b1fcfcd 100644 --- a/src/server/main.nim +++ b/src/server/main.nim @@ -1,4 +1,135 @@ -import core/server +import terminal, parsetoml, times, json, math +import strutils, strformat, system, tables + +import ./core/[agent, listener, builder] +import ./globals +import ./db/database +import ./core/logger +import ../common/[types, crypto, utils, profile, event] +import ./websocket +import mummy, mummy/routers + +proc header() = + echo "" + echo "┏┏┓┏┓┏┓┓┏┏┓┏╋" + echo "┗┗┛┛┗┗┫┗┻┗ ┛┗ V0.1" + echo " ┗ @jakobfriedl" + echo "─".repeat(21) + echo "" + +proc init*(T: type Conquest, profile: Profile): Conquest = + var cq = new Conquest + cq.listeners = initTable[string, Listener]() + cq.threads = initTable[string, Thread[Listener]]() + cq.agents = initTable[string, Agent]() + cq.interactAgent = nil + cq.profile = profile + cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & profile.getString("private-key-file")) + cq.dbPath = CONQUEST_ROOT & "/" & profile.getString("database-file") + cq.client = nil + return cq + +#[ + WebSocket +]# +proc upgradeHandler(request: Request) = + {.cast(gcsafe).}: + let ws = request.upgradeToWebSocket() + cq.client = UIClient( + ws: ws + ) + +proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.gcsafe.} = + {.cast(gcsafe).}: + case event: + of OpenEvent: + # New client connected to team server + # Send profile, sessions and listeners to the UI client + cq.client.sendProfile(cq.profile) + for id, listener in cq.listeners: + cq.client.sendListener(listener) + for id, agent in cq.agents: + cq.client.sendAgent(agent) + cq.client.sendEventlogItem(LOG_SUCCESS_SHORT, "CQ-V1") + + of MessageEvent: + # Continuously send heartbeat messages + ws.sendHeartbeat() + + let event = message.recvEvent() + + case event.eventType: + of CLIENT_AGENT_TASK: + let agentId = event.data["agentId"].getStr() + let task = event.data["task"].to(Task) + cq.agents[agentId].tasks.add(task) + + of CLIENT_LISTENER_START: + let listener = event.data.to(UIListener) + cq.listenerStart(listener.listenerId, listener.address, listener.port, listener.protocol) + + of CLIENT_LISTENER_STOP: + let listenerId = event.data["listenerId"].getStr() + cq.listenerStop(listenerId) + + of CLIENT_AGENT_BUILD: + let + listenerId = event.data["listenerId"].getStr() + sleepDelay = event.data["sleepDelay"].getInt() + sleepTechnique = cast[SleepObfuscationTechnique](event.data["sleepTechnique"].getInt()) + spoofStack = event.data["spoofStack"].getBool() + modules = cast[uint32](event.data["modules"].getInt()) + + let payload = cq.agentBuild(listenerId, sleepDelay, sleepTechnique, spoofStack, modules) + if payload.len() != 0: + cq.client.sendAgentPayload(payload) + + else: discard + + of ErrorEvent: + discard + of CloseEvent: + # Set the client instance to nil again to prevent debug error messages + cq.client = nil + +proc startServer*(profilePath: string) = + + # Ensure that the conquest root directory was passed as a compile-time define + when not defined(CONQUEST_ROOT): + quit(0) + + header() + + try: + # Initialize framework context + # Load and parse profile + let profile = parsetoml.parseFile(profilePath) + cq = Conquest.init(profile) + + cq.info("Using profile \"", profile.getString("name"), "\" (", profilePath ,").") + + except CatchableError as err: + echo err.msg + quit(0) + + # Initialize database + cq.dbInit() + for agent in cq.dbGetAllAgents(): + cq.agents[agent.agentId] = agent + for listener in cq.dbGetAllListeners(): + cq.listeners[listener.listenerId] = listener + + # Restart existing listeners + for listenerId, listener in cq.listeners: + cq.listenerStart(listenerId, listener.address, listener.port, listener.protocol) + + # Start websocket server + var router: Router + router.get("/*", upgradeHandler) + + # Increased websocket message length in order to support dotnet assembly execution + let server = newServer(router, websocketHandler, maxMessageLen = 1024 * 1024 * 1024) + server.serve(Port(cq.profile.getInt("team-server.port")), "0.0.0.0") # Conquest framework entry point when isMainModule: diff --git a/src/server/utils.nim b/src/server/utils.nim index 3dc4e6f..4a00d1d 100644 --- a/src/server/utils.nim +++ b/src/server/utils.nim @@ -1,8 +1,5 @@ -import strutils, terminal, tables, sequtils, times, strformat, prompt, json -import std/wordwrap - +import times, json import ../common/types -import core/logger proc `%`*(agent: Agent): JsonNode = result = newJObject() @@ -25,158 +22,4 @@ proc `%`*(listener: Listener): JsonNode = result["listenerId"] = %listener.listenerId result["address"] = %listener.address result["port"] = %listener.port - result["protocol"] = %listener.protocol - -# Table border characters -type - Cell = object - text: string - fg: ForegroundColor = fgWhite - bg: BackgroundColor = bgDefault - style: Style - -const topLeft = "╭" -const topMid = "┬" -const topRight= "╮" -const midLeft = "├" -const midMid = "┼" -const midRight= "┤" -const botLeft = "╰" -const botMid = "┴" -const botRight= "╯" -const hor = "─" -const vert = "│" - -# Wrap cell content -proc wrapCell(text: string, width: int): seq[string] = - result = text.wrapWords(width).splitLines() - -# Format border -proc border(left, mid, right: string, widths: seq[int]): string = - var line = left - for i, w in widths: - line.add(hor.repeat(w + 2)) - line.add(if i < widths.len - 1: mid else: right) - return line - -# Format a row of data -proc formatRow(cells: seq[Cell], widths: seq[int]): seq[seq[Cell]] = - var wrappedCols: seq[seq[Cell]] - var maxLines = 1 - - for i, cell in cells: - let wrappedLines = wrapCell(cell.text, widths[i]) - wrappedCols.add(wrappedLines.mapIt(Cell(text: it, fg: cell.fg, bg: cell.bg, style: cell.style))) - maxLines = max(maxLines, wrappedLines.len) - - for line in 0 ..< maxLines: - var lineRow: seq[Cell] = @[] - for i, col in wrappedCols: - let lineText = if line < col.len: col[line].text else: "" - let base = cells[i] - lineRow.add(Cell(text: " " & lineText.alignLeft(widths[i]) & " ", fg: base.fg, bg: base.bg, style: base.style)) - result.add(lineRow) - -proc writeRow(cq: Conquest, row: seq[Cell]) = - stdout.write(vert) - for cell in row: - stdout.styledWrite(cell.fg, cell.bg, cell.style, cell.text, resetStyle, vert) - stdout.write("\n") - -proc drawTable*(cq: Conquest, listeners: seq[Listener]) = - - # Column headers and widths - let headers = @["UUID", "Address", "Port", "Protocol", "Agents"] - let widths = @[8, 15, 5, 8, 6] - let headerCells = headers.mapIt(Cell(text: it, fg: fgWhite, bg: bgDefault)) - - cq.output(border(topLeft, topMid, topRight, widths)) - for line in formatRow(headerCells, widths): - cq.prompt.hidePrompt() - cq.writeRow(line) - cq.prompt.showPrompt() - cq.output(border(midLeft, midMid, midRight, widths)) - - for l in listeners: - # Get number of agents connected to the listener - let connectedAgents = cq.agents.values.countIt(it.listenerId == l.listenerId) - - let rowCells = @[ - Cell(text: l.listenerId, fg: fgGreen), - Cell(text: l.address), - Cell(text: $l.port), - Cell(text: $l.protocol), - Cell(text: $connectedAgents) - ] - - for line in formatRow(rowCells, widths): - cq.prompt.hidePrompt() - cq.writeRow(line) - cq.prompt.showPrompt() - - cq.output(border(botLeft, botMid, botRight, widths)) - -# Calculate time since latest checking in format: Xd Xh Xm Xs -proc timeSince*(agent: Agent, timestamp: DateTime): Cell = - - let - now = now() - duration = now - timestamp - totalSeconds = int(duration.inSeconds) - - let - days = totalSeconds div 86400 - hours = (totalSeconds mod 86400) div 3600 - minutes = (totalSeconds mod 3600) div 60 - seconds = totalSeconds mod 60 - - var text = "" - - if days > 0: - text &= fmt"{days}d " - if hours > 0 or days > 0: - text &= fmt"{hours}h " - if minutes > 0 or hours > 0 or days > 0: - text &= fmt"{minutes}m " - text &= fmt"{seconds}s" - - return Cell( - text: text.strip(), - # When the agent is 'dead', meaning that the latest checkin occured - # more than the agents sleep configuration, dim the text style - style: if totalSeconds > agent.sleep: styleDim else: styleBright - ) - -proc drawTable*(cq: Conquest, agents: seq[Agent]) = - - let headers: seq[string] = @["UUID", "Address", "Username", "Hostname", "Operating System", "Process", "PID", "Activity"] - let widths = @[8, 15, 15, 15, 16, 13, 5, 8] - let headerCells = headers.mapIt(Cell(text: it, fg: fgWhite, bg: bgDefault)) - - cq.output(border(topLeft, topMid, topRight, widths)) - for line in formatRow(headerCells, widths): - cq.prompt.hidePrompt() - cq.writeRow(line) - cq.prompt.showPrompt() - cq.output(border(midLeft, midMid, midRight, widths)) - - for a in agents: - - var cells = @[ - Cell(text: a.agentId, fg: fgYellow, style: styleBright), - Cell(text: a.ip), - Cell(text: a.username), - Cell(text: a.hostname), - Cell(text: a.os), - Cell(text: a.process, fg: if a.elevated: fgRed else: fgWhite), - Cell(text: $a.pid, fg: if a.elevated: fgRed else: fgWhite), - a.timeSince(cq.agents[a.agentId].latestCheckin) - ] - - # Highlight agents running within elevated processes - for line in formatRow(cells, widths): - cq.prompt.hidePrompt() - cq.writeRow(line) - cq.prompt.showPrompt() - - cq.output(border(botLeft, botMid, botRight, widths)) \ No newline at end of file + result["protocol"] = %listener.protocol \ No newline at end of file