Removed prompt user intreface; Team server and Client are now fully separated.
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -259,7 +259,6 @@ proc draw*(component: ConsoleComponent, ws: WebSocket) =
|
||||
#[
|
||||
Filter & Options
|
||||
]#
|
||||
|
||||
var availableSize: ImVec2
|
||||
igGetContentRegionAvail(addr availableSize)
|
||||
var labelSize: ImVec2
|
||||
@@ -293,8 +292,8 @@ 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
|
||||
|
||||
# Display console items
|
||||
for item in component.console.items:
|
||||
|
||||
# Apply filter
|
||||
|
||||
@@ -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)
|
||||
@@ -91,20 +97,33 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
||||
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()
|
||||
@@ -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]]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -26,157 +23,3 @@ proc `%`*(listener: Listener): JsonNode =
|
||||
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))
|
||||
Reference in New Issue
Block a user