Started work on agent registration

This commit is contained in:
Jakob Friedl
2025-05-12 21:53:37 +02:00
parent 8528a3107c
commit 3038ad6f0e
11 changed files with 391 additions and 271 deletions

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Jakob Friedl
Copyright (cq) 2025 Jakob Friedl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,31 +0,0 @@
import ./types
proc agentUsage*(console: Console) =
console.writeLine("""Manage, build and interact with agents.
Usage:
agent [options] COMMAND
Commands:
list List all agents.
build Build an agent to connect to an active listener.
interact Interact with an active listener.
Options:
-h, --help""")
proc agentList*(console: Console, args: varargs[string]) =
discard
proc agentBuild*(console: Console, args: varargs[string]) =
discard
proc agentInteract*(console: Console, args: varargs[string]) =
console.setIndicator("[AGENT] (username@hostname)> ")
console.setStatusBar(@[("mode", "interact"), ("listeners", "X"), ("agents", "4")])
var command: string = console.readLine()
discard

59
server/agent/agent.nim Normal file
View File

@@ -0,0 +1,59 @@
import terminal, strformat, times
import ../[types, globals]
#[
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.
build Build an agent to connect to an active listener.
interact Interact with an active agent.
Options:
-h, --help""")
proc agentList*(cq: Conquest, args: varargs[string]) =
discard
proc agentBuild*(cq: Conquest, args: varargs[string]) =
discard
# Switch to interact mode
proc agentInteract*(cq: Conquest, args: varargs[string]) =
cq.setIndicator("[AGENT] (username@hostname)> ")
cq.setStatusBar(@[("mode", "interact"), ("listeners", "X"), ("agents", "4")])
var command: string = cq.readLine()
discard
#[
Agent API
Functions relevant for dealing with the agent API, such as registering new agents, querying tasks and posting results
]#
proc notifyAgentRegister*(agent: Agent) =
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
# The following line is required to be able to use the `cq` global variable for console output
{.cast(gcsafe).}:
cq.writeLine(fgYellow, styleBright, fmt"[{date}] Agent {agent.name} connected.", "\n")
#[
Agent interaction mode
When interacting with a agent, the following functions are called:
- addTask, to add a new tasks to the agents task queue
- getTaskResult, get the result for the task from the agent
]#

View File

@@ -1,129 +0,0 @@
import prompt, terminal
import argparse
import strutils, strformat, times, system, unicode
import ./[types, agent]
import listener/listener
import db/database
#[
Argument parsing
]#
var parser = newParser:
help("Console Command & Control")
command("listener"):
help("Manage, start and stop listeners.")
command("list"):
help("List all active listeners.")
command("start"):
help("Starts a new HTTP listener.")
option("-h", "-host", default=some("0.0.0.0"), help="IPv4 address to listen on.", required=false)
option("-p", "-port", help="Port to listen on.", required=true)
# flag("--dns", help="Use the DNS protocol for C2 communication.")
command("stop"):
help("Stop an active listener.")
option("-n", "-name", help="Name of the listener to stop.", required=true)
command("agent"):
help("Manage, build and interact with agents.")
command("list"):
help("List all agents.")
command("build"):
help("Build an agent to connect to an active listener.")
command("interact"):
help("Interact with an active listener.")
command("help"):
nohelpflag()
command("exit"):
nohelpflag()
proc handleConsoleCommand*(console: Console, args: varargs[string]) =
# Return if no command (or just whitespace) is entered
if args[0].replace(" ", "").len == 0: return
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
console.writeLine(fgCyan, fmt"[{date}] ", resetStyle, styleBright, args[0])
try:
let opts = parser.parse(args[0].split(" ").filterIt(it.len > 0))
case opts.command
of "exit": # Exit program
echo "\n"
quit(0)
of "help": # Display help menu
console.writeLine(parser.help())
of "listener":
case opts.listener.get.command
of "list":
console.listenerList()
of "start":
console.listenerStart(opts.listener.get.start.get.host, opts.listener.get.start.get.port)
of "stop":
console.listenerStop(opts.listener.get.stop.get.name)
else:
console.listenerUsage()
of "agent":
case opts.agent.get.command
of "list":
console.agentList()
of "build":
console.agentBuild()
of "interact":
console.agentInteract()
else:
console.listenerUsage()
# Handle help flag
except ShortCircuit as err:
if err.flag == "argparse_help":
console.writeLine(err.help)
# Handle invalid arguments
except UsageError:
console.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
console.writeLine("")
proc header(console: Console) =
console.writeLine("")
console.writeLine("┏┏┓┏┓┏┓┓┏┏┓┏╋")
console.writeLine("┗┗┛┛┗┗┫┗┻┗ ┛┗ 0.1")
console.writeLine(" ┗ @jakobfriedl")
console.writeLine("".repeat(21))
console.writeLine("")
proc initPrompt*() =
var console = newConsole()
# Print header
console.header()
# Initialize database
console.dbInit()
console.restartListeners()
# Main loop
while true:
console.setIndicator("[conquest]> ")
console.setStatusBar(@[("mode", "manage"), ("listeners", $console.listeners), ("agents", $console.agents)])
console.showPrompt()
var command: string = console.readLine()
console.withOutput(handleConsoleCommand, command)

View File

@@ -3,10 +3,10 @@ import ../types
import system, terminal, strformat
proc dbInit*(console: Console) =
proc dbInit*(cq: Conquest) =
try:
let conquestDb = openDatabase(console.dbPath, mode=dbReadWrite)
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
# Create tables
conquestDb.execScript("""
@@ -21,15 +21,15 @@ proc dbInit*(console: Console) =
""")
console.writeLine(fgGreen, "[+] ", console.dbPath, ": Database created.")
cq.writeLine(fgGreen, "[+] ", cq.dbPath, ": Database created.")
conquestDb.close()
except SqliteError:
console.writeLine(fgGreen, "[+] ", console.dbPath, ": Database file found.")
cq.writeLine(fgGreen, "[+] ", cq.dbPath, ": Database file found.")
proc dbStore*(console: Console, listener: Listener): bool =
proc dbStoreListener*(cq: Conquest, listener: Listener): bool =
try:
let conquestDb = openDatabase(console.dbPath, mode=dbReadWrite)
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("""
INSERT INTO listener (name, address, port, protocol, sleep, jitter)
@@ -38,17 +38,17 @@ proc dbStore*(console: Console, listener: Listener): bool =
conquestDb.close()
except:
console.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return false
return true
proc dbGetAllListeners*(console: Console): seq[Listener] =
proc dbGetAllListeners*(cq: Conquest): seq[Listener] =
var listeners: seq[Listener] = @[]
try:
let conquestDb = openDatabase(console.dbPath, mode=dbReadWrite)
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT name, address, port, protocol, sleep, jitter FROM listener;"):
let (name, address, port, protocol, sleep, jitter) = row.unpack((string, string, int, string, int, float ))
@@ -65,13 +65,13 @@ proc dbGetAllListeners*(console: Console): seq[Listener] =
conquestDb.close()
except:
console.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
return listeners
proc dbDeleteListenerByName*(console: Console, name: string): bool =
proc dbDeleteListenerByName*(cq: Conquest, name: string): bool =
try:
let conquestDb = openDatabase(console.dbPath, mode=dbReadWrite)
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("DELETE FROM listener WHERE name = ?", name)
@@ -81,5 +81,5 @@ proc dbDeleteListenerByName*(console: Console, name: string): bool =
return true
proc dbStore*(agent: Agent): bool =
proc dbStoreAgent*(agent: Agent): bool =
discard

3
server/globals.nim Normal file
View File

@@ -0,0 +1,3 @@
import ./types
var cq*: Conquest

View File

@@ -1,15 +1,62 @@
import prologue
import prologue, nanoid
import terminal, sequtils, strutils
import ../types
import ../[types]
import ../agent/agent
import ./utils
proc index*(ctx: Context) {.async.} =
resp "Index"
proc agentRegister*(ctx: Context) {.async.} =
resp "Register"
#[
POST /{listener-uuid}/register
Called from agent to register itself to the conquest server
]#
proc register*(ctx: Context) {.async.} =
proc addTasks*(ctx: Context) {.async.} =
# Check headers
doAssert(ctx.request.getHeader("CONTENT-TYPE") == @["application/json"])
doAssert(ctx.request.getHeader("USER-AGENT") == @["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"])
# Handle POST data, the register data should look like the following
#[
{
"username": "username",
"hostname":"hostname",
"ip": "ip-address",
"os": "operating-system"
"pid": 1234
"elevated": false
}
]#
let name = ctx.getPathParams("name")
let
postData: JsonNode = %ctx.request.body()
name = generate(alphabet=join(toSeq('A'..'Z'), ""), size=8)
let agent = new Agent
agent.name = name
notifyAgentRegister(agent)
resp agent.name
#[
GET /{listener-uuid}/{agent-uuid}/tasks
Called from agent to check for new tasks
]#
proc getTasks*(ctx: Context) {.async.} =
stdout.writeLine(ctx.getPathParams("listener"))
let name = ctx.getPathParams("agent")
resp name
#[
POST /{listener-uuid}/{agent-uuid}/results
Called from agent to post results of a task
]#
proc postResults*(ctx: Context) {.async.} =
let name = ctx.getPathParams("agent")
resp name

View File

@@ -1,13 +1,12 @@
import strformat, strutils, sequtils, checksums/sha1, nanoid, terminal, sugar
import strformat, strutils, sequtils, checksums/sha1, nanoid, terminal
import prologue
import ./[api, utils]
import ../types
import ../db/database
proc listenerUsage*(console: Console) =
console.writeLine("""Manage, start and stop listeners.
proc listenerUsage*(cq: Conquest) =
cq.writeLine("""Manage, start and stop listeners.
Usage:
listener [options] COMMAND
@@ -21,18 +20,18 @@ Commands:
Options:
-h, --help""")
proc listenerList*(console: Console) =
let listeners = console.dbGetAllListeners()
console.drawTable(listeners)
proc listenerList*(cq: Conquest) =
let listeners = cq.dbGetAllListeners()
cq.drawTable(listeners)
proc listenerStart*(console: Console, host: string, portStr: string) =
proc listenerStart*(cq: Conquest, host: string, portStr: string) =
# Validate arguments
# if not validateIPv4Address(host):
# console.writeLine(fgRed, styleBright, fmt"Invalid IPv4 IP address: {ip}.")
# cq.writeLine(fgRed, styleBright, fmt"Invalid IPv4 IP address: {ip}.")
# return
if not validatePort(portStr):
console.writeLine(fgRed, styleBright, fmt"[-] Invalid port number: {portStr}")
cq.writeLine(fgRed, styleBright, fmt"[-] Invalid port number: {portStr}")
return
let port = portStr.parseInt
@@ -50,26 +49,25 @@ proc listenerStart*(console: Console, host: string, portStr: string) =
var listener = newApp(settings = listenerSettings)
# Define API endpoints
listener.addRoute("/", api.index, @[HttpGet])
listener.addRoute("/register", api.agentRegister, @[HttpPost])
listener.addRoute("/{name}/tasks", api.addTasks, @[HttpGet, HttpPost])
listener.post("{listener}/register", api.register)
listener.get("{listener}/{agent}/tasks", api.getTasks)
listener.post("{listener}/{agent}/results", api.postResults)
# Store listener in database
let listenerInstance = newListener(name, host, port)
if not console.dbStore(listenerInstance):
if not cq.dbStoreListener(listenerInstance):
return
# Start serving
try:
discard listener.runAsync()
console.activeListeners.add(listener)
inc console.listeners
console.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on port {portStr}.")
inc cq.listeners
cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on port {portStr}.")
except CatchableError as err:
console.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", getCurrentExceptionMsg())
cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", getCurrentExceptionMsg())
proc restartListeners*(console: Console) =
let listeners: seq[Listener] = console.dbGetAllListeners()
proc restartListeners*(cq: Conquest) =
let listeners: seq[Listener] = cq.dbGetAllListeners()
# Restart all active listeners that are stored in the database
for l in listeners:
@@ -83,29 +81,28 @@ proc restartListeners*(console: Console) =
listener = newApp(settings = settings)
# Define API endpoints
listener.addRoute("/", api.index, @[HttpGet])
listener.addRoute("/register", api.agentRegister, @[HttpPost])
listener.addRoute("/{name}/tasks", api.addTasks, @[HttpGet, HttpPost])
listener.post("{listener}/register", api.register)
listener.get("{listener}/{agent}/tasks", api.getTasks)
listener.post("{listener}/{agent}/results", api.postResults)
try:
discard listener.runAsync()
console.activeListeners.add(listener)
inc console.listeners
console.writeLine(fgGreen, "[+] ", resetStyle, "Restarted listener", fgGreen, fmt" {l.name} ", resetStyle, fmt"on port {$l.port}.")
inc cq.listeners
cq.writeLine(fgGreen, "[+] ", resetStyle, "Restarted listener", fgGreen, fmt" {l.name} ", resetStyle, fmt"on port {$l.port}.")
except CatchableError as err:
console.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", getCurrentExceptionMsg())
cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", getCurrentExceptionMsg())
# Delay before starting serving another listener to avoid crashing the application
waitFor sleepAsync(10)
console.writeLine("")
cq.writeLine("")
proc listenerStop*(console: Console, name: string) =
proc listenerStop*(cq: Conquest, name: string) =
if not console.dbDeleteListenerByName(name.toUpperAscii):
console.writeLine(fgRed, styleBright, "[-] Failed to stop listener: ", getCurrentExceptionMsg())
if not cq.dbDeleteListenerByName(name.toUpperAscii):
cq.writeLine(fgRed, styleBright, "[-] Failed to stop listener: ", getCurrentExceptionMsg())
return
dec console.listeners
console.writeLine(fgGreen, "[+] ", resetStyle, "Stopped listener ", fgGreen, fmt"{name.toUpperAscii}.")
dec cq.listeners
cq.writeLine(fgGreen, "[+] ", resetStyle, "Stopped listener ", fgGreen, name.toUpperAscii, resetStyle, ".")

View File

@@ -41,23 +41,23 @@ proc row(cells: seq[string], widths: seq[int]): string =
row.add(" " & cell.alignLeft(widths[i] - 2) & " " & vert)
return row
proc drawTable*(console: Console, listeners: seq[Listener]) =
proc drawTable*(cq: Conquest, listeners: seq[Listener]) =
# Column headers and widths
let headers = @["Name", "Address", "Port", "Protocol", "Agents"]
let widths = @[10, 15, 7, 10, 8]
console.writeLine(border(topLeft, topMid, topRight, widths))
console.writeLine(row(headers, widths))
console.writeLine(border(midLeft, midMid, midRight, widths))
cq.writeLine(border(topLeft, topMid, topRight, widths))
cq.writeLine(row(headers, widths))
cq.writeLine(border(midLeft, midMid, midRight, widths))
for l in listeners:
# TODO: Add number of agents connected to the listener
let row = @[l.name, l.address, $l.port, $l.protocol, "X"]
console.writeLine(row(row, widths))
cq.writeLine(row(row, widths))
console.writeLine(border(botLeft, botMid, botRight, widths))
cq.writeLine(border(botLeft, botMid, botRight, widths))
proc drawTable*(console: Console, agents: seq[Agent]) =
proc drawTable*(cq: Conquest, agents: seq[Agent]) =
discard

View File

@@ -1,19 +1,141 @@
import ./console
import prompt, terminal
import argparse
import strutils, strformat, times, system, unicode
# Handle CTRL+C,
proc exit() {.noconv.} =
echo "Received CTRL+C. Type \"exit\" to close the application.\n"
proc main() =
# Initialize TUI
# initUi()
setControlCHook(exit)
# Initialize prompt interface
initPrompt()
import ./[types, globals]
import agent/agent, listener/listener, db/database
#[
Start main function
Argument parsing
]#
var parser = newParser:
help("Conquest Command & Control")
command("listener"):
help("Manage, start and stop listeners.")
command("list"):
help("List all active listeners.")
command("start"):
help("Starts a new HTTP listener.")
option("-h", "-host", default=some("0.0.0.0"), help="IPv4 address to listen on.", required=false)
option("-p", "-port", help="Port to listen on.", required=true)
# flag("--dns", help="Use the DNS protocol for C2 communication.")
command("stop"):
help("Stop an active listener.")
option("-n", "-name", help="Name of the listener to stop.", required=true)
command("agent"):
help("Manage, build and interact with agents.")
command("list"):
help("List all agents.")
command("build"):
help("Build an agent to connect to an active listener.")
command("interact"):
help("Interact with an active agent.")
command("help"):
nohelpflag()
command("exit"):
nohelpflag()
proc handleConsoleCommand*(cq: Conquest, args: varargs[string]) =
# Return if no command (or just whitespace) is entered
if args[0].replace(" ", "").len == 0: return
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
cq.writeLine(fgCyan, fmt"[{date}] ", resetStyle, styleBright, args[0])
try:
let opts = parser.parse(args[0].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.host, 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()
of "build":
cq.agentBuild()
of "interact":
cq.agentInteract()
else:
cq.agentUsage()
# Handle help flag
except ShortCircuit as err:
if err.flag == "argparse_help":
cq.writeLine(err.help)
# Handle invalid arguments
except UsageError:
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
]#
main()
proc main() =
# Handle CTRL+C,
proc exit() {.noconv.} =
echo "Received CTRL+C. Type \"exit\" to close the application.\n"
setControlCHook(exit)
# Initialize framework
cq = initConquest()
# Print header
cq.header()
# Initialize database
cq.dbInit()
cq.restartListeners()
# Main loop
while true:
cq.setIndicator("[conquest]> ")
cq.setStatusBar(@[("mode", "manage"), ("listeners", $cq.listeners), ("agents", $cq.agents)])
cq.showPrompt()
var command: string = cq.readLine()
cq.withOutput(handleConsoleCommand, command)
when isMainModule:
main()

View File

@@ -2,58 +2,110 @@ import prompt
import prologue
#[
Console
Conquest
]#
type
Console* = ref object
Conquest* = ref object
prompt*: Prompt
listeners*: int
agents*: int
dbPath*: string
activeListeners*: seq[Prologue]
Command* = object
cmd*: string
execute*: proc(console: Console, args: varargs[string])
proc newConsole*(): Console =
var console = new Console
proc initConquest*(): Conquest =
var cq = new Conquest
var prompt = Prompt.init()
console.prompt = prompt
console.dbPath = "db/conquest.db"
console.listeners = 0
console.agents = 0
console.activeListeners = @[]
cq.prompt = prompt
cq.dbPath = "db/conquest.db"
cq.listeners = 0
cq.agents = 0
return console
return cq
template writeLine*(console: Console, args: varargs[untyped]) =
console.prompt.writeLine(args)
proc readLine*(console: Console): string =
return console.prompt.readLine()
template setIndicator*(console: Console, indicator: string) =
console.prompt.setIndicator(indicator)
template showPrompt*(console: Console) =
console.prompt.showPrompt()
template hidePrompt*(console: Console) =
console.prompt.hidePrompt()
template setStatusBar*(console: Console, statusBar: seq[StatusBarItem]) =
console.prompt.setStatusBar(statusBar)
template clear*(console: Console) =
console.prompt.clear()
template writeLine*(cq: Conquest, args: varargs[untyped]) =
cq.prompt.writeLine(args)
proc readLine*(cq: Conquest): string =
return cq.prompt.readLine()
template setIndicator*(cq: Conquest, indicator: string) =
cq.prompt.setIndicator(indicator)
template showPrompt*(cq: Conquest) =
cq.prompt.showPrompt()
template hidePrompt*(cq: Conquest) =
cq.prompt.hidePrompt()
template setStatusBar*(cq: Conquest, statusBar: seq[StatusBarItem]) =
cq.prompt.setStatusBar(statusBar)
template clear*(cq: Conquest) =
cq.prompt.clear()
# Overwrite withOutput function to handle function arguments
proc withOutput*(console: Console, outputFunction: proc(console: Console, args: varargs[string]), args: varargs[string]) =
console.hidePrompt()
outputFunction(console, args)
console.showPrompt()
proc withOutput*(cq: Conquest, outputFunction: proc(cq: Conquest, args: varargs[string]), args: varargs[string]) =
cq.hidePrompt()
outputFunction(cq, args)
cq.showPrompt()
#[
Agent
]#
type
TaskCommand* = enum
ExecuteShell = "shell"
ExecuteBof = "bof"
ExecuteAssembly = "dotnet"
ExecutePe = "pe"
TaskStatus* = enum
Created = "created"
Completed = "completed"
Pending = "pending"
Failed = "failed"
Cancelled = "cancelled"
TaskResult* = string
Task* = ref object
id*: int
agent*: string
command*: TaskCommand
args*: seq[string]
result*: TaskResult
status*: TaskStatus
AgentRegistrationData* = object
username*: string
hostname*: string
ip*: string
os*: string
pid*: int
elevated*: bool
Agent* = ref object
name*: string
listener*: string
sleep*: int
jitter*: float
pid*: int
username*: string
hostname*: string
ip*: string
os*: string
elevated*: bool
tasks*: seq[Task]
proc newAgent*(name, listener, username, hostname, ip, os: string, pid: int, elevated: bool): Agent =
var agent = new Agent
agent.name = name
agent.listener = listener
agent.pid = pid
agent.username = username
agent.hostname = hostname
agent.ip = ip
agent.os = os
agent.elevated = elevated
agent.sleep = 10
agent.jitter = 0.2
agent.tasks = @[]
return agent
#[
Listener