Removed prompt user intreface; Team server and Client are now fully separated.

This commit is contained in:
Jakob Friedl
2025-10-01 13:25:15 +02:00
parent a1990e4a18
commit c97cb4585f
12 changed files with 188 additions and 628 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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]]

View File

@@ -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

View File

@@ -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)

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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:

View File

@@ -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))
result["protocol"] = %listener.protocol