diff --git a/server/agent.nim b/server/agent.nim new file mode 100644 index 0000000..a2bde19 --- /dev/null +++ b/server/agent.nim @@ -0,0 +1,31 @@ +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 \ No newline at end of file diff --git a/server/config.nims b/server/config.nims index f177b11..4713d76 100644 --- a/server/config.nims +++ b/server/config.nims @@ -1,2 +1,2 @@ # Compiler flags ---threads:on \ No newline at end of file +# --threads:on \ No newline at end of file diff --git a/server/console.nim b/server/console.nim new file mode 100644 index 0000000..906d282 --- /dev/null +++ b/server/console.nim @@ -0,0 +1,129 @@ +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) + diff --git a/server/db/database.nim b/server/db/database.nim new file mode 100644 index 0000000..dd1147b --- /dev/null +++ b/server/db/database.nim @@ -0,0 +1,85 @@ +import tiny_sqlite, net +import ../types + +import system, terminal, strformat + +proc dbInit*(console: Console) = + + try: + let conquestDb = openDatabase(console.dbPath, mode=dbReadWrite) + + # Create tables + conquestDb.execScript(""" + CREATE TABLE listener ( + name TEXT PRIMARY KEY, + address TEXT NOT NULL, + port INTEGER NOT NULL UNIQUE, + protocol TEXT NOT NULL CHECK (protocol IN ('http')), + sleep INTEGER NOT NULL, + jitter REAL NOT NULL + ); + + """) + + console.writeLine(fgGreen, "[+] ", console.dbPath, ": Database created.") + conquestDb.close() + except SqliteError: + console.writeLine(fgGreen, "[+] ", console.dbPath, ": Database file found.") + +proc dbStore*(console: Console, listener: Listener): bool = + + try: + let conquestDb = openDatabase(console.dbPath, mode=dbReadWrite) + + conquestDb.exec(""" + INSERT INTO listener (name, address, port, protocol, sleep, jitter) + VALUES (?, ?, ?, ?, ?, ?); + """, listener.name, listener.address, listener.port, $listener.protocol, listener.sleep, listener.jitter) + + conquestDb.close() + except: + console.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg()) + return false + + return true + +proc dbGetAllListeners*(console: Console): seq[Listener] = + + var listeners: seq[Listener] = @[] + + try: + let conquestDb = openDatabase(console.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 )) + + let l = Listener( + name: name, + address: address, + port: port, + protocol: stringToProtocol(protocol), + sleep: sleep, + jitter: jitter + ) + listeners.add(l) + + conquestDb.close() + except: + console.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg()) + + return listeners + +proc dbDeleteListenerByName*(console: Console, name: string): bool = + try: + let conquestDb = openDatabase(console.dbPath, mode=dbReadWrite) + + conquestDb.exec("DELETE FROM listener WHERE name = ?", name) + + conquestDb.close() + except: + return false + + return true + +proc dbStore*(agent: Agent): bool = + discard \ No newline at end of file diff --git a/server/listener/api.nim b/server/listener/api.nim new file mode 100644 index 0000000..c5b12f3 --- /dev/null +++ b/server/listener/api.nim @@ -0,0 +1,15 @@ +import prologue + +import ../types + +proc index*(ctx: Context) {.async.} = + resp "Index" + +proc agentRegister*(ctx: Context) {.async.} = + resp "Register" + +proc addTasks*(ctx: Context) {.async.} = + + let name = ctx.getPathParams("name") + + resp name diff --git a/server/listener/listener.nim b/server/listener/listener.nim new file mode 100644 index 0000000..bbf2c8e --- /dev/null +++ b/server/listener/listener.nim @@ -0,0 +1,111 @@ +import strformat, strutils, sequtils, checksums/sha1, nanoid, terminal, sugar +import prologue + +import ./[api, utils] +import ../types +import ../db/database + + +proc listenerUsage*(console: Console) = + console.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*(console: Console) = + let listeners = console.dbGetAllListeners() + console.drawTable(listeners) + +proc listenerStart*(console: Console, host: string, portStr: string) = + + # Validate arguments + # if not validateIPv4Address(host): + # console.writeLine(fgRed, styleBright, fmt"Invalid IPv4 IP address: {ip}.") + # return + if not validatePort(portStr): + console.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.addRoute("/", api.index, @[HttpGet]) + listener.addRoute("/register", api.agentRegister, @[HttpPost]) + listener.addRoute("/{name}/tasks", api.addTasks, @[HttpGet, HttpPost]) + + # Store listener in database + let listenerInstance = newListener(name, host, port) + if not console.dbStore(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}.") + except CatchableError as err: + console.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", getCurrentExceptionMsg()) + +proc restartListeners*(console: Console) = + let listeners: seq[Listener] = console.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.addRoute("/", api.index, @[HttpGet]) + listener.addRoute("/register", api.agentRegister, @[HttpPost]) + listener.addRoute("/{name}/tasks", api.addTasks, @[HttpGet, HttpPost]) + + 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}.") + except CatchableError as err: + console.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", getCurrentExceptionMsg()) + + # Delay before starting serving another listener to avoid crashing the application + waitFor sleepAsync(10) + + console.writeLine("") + +proc listenerStop*(console: Console, name: string) = + + if not console.dbDeleteListenerByName(name.toUpperAscii): + console.writeLine(fgRed, styleBright, "[-] Failed to stop listener: ", getCurrentExceptionMsg()) + return + + dec console.listeners + console.writeLine(fgGreen, "[+] ", resetStyle, "Stopped listener ", fgGreen, fmt"{name.toUpperAscii}.") + \ No newline at end of file diff --git a/server/listener/utils.nim b/server/listener/utils.nim new file mode 100644 index 0000000..16ddf4b --- /dev/null +++ b/server/listener/utils.nim @@ -0,0 +1,63 @@ +import re, strutils + +import ../types + +proc validateIPv4Address*(ip: string): bool = + let ipv4Pattern = re"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$" + return ip.match(ipv4Pattern) + +proc validatePort*(portStr: string): bool = + try: + let port: int = portStr.parseInt + return port >= 1 and port <= 65535 + except ValueError: + return false + +# Table border characters +const topLeft = "╭" +const topMid = "┬" +const topRight= "╮" +const midLeft = "├" +const midMid = "┼" +const midRight= "┤" +const botLeft = "╰" +const botMid = "┴" +const botRight= "╯" +const hor = "─" +const vert = "│" + +# 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)) + line.add(if i < widths.len - 1: mid else: right) + return line + +# Format a row of data +proc row(cells: seq[string], widths: seq[int]): string = + var row = vert + for i, cell in cells: + row.add(" " & cell.alignLeft(widths[i] - 2) & " " & vert) + return row + +proc drawTable*(console: Console, 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)) + + 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)) + + console.writeLine(border(botLeft, botMid, botRight, widths)) + + +proc drawTable*(console: Console, agents: seq[Agent]) = + discard \ No newline at end of file diff --git a/server/server.nim b/server/server.nim index 4578e3c..eebe09b 100644 --- a/server/server.nim +++ b/server/server.nim @@ -1,11 +1,19 @@ -import ./tui +import ./console + +# Handle CTRL+C, +proc exit() {.noconv.} = + echo "Received CTRL+C. Type \"exit\" to close the application.\n" proc main() = # Initialize TUI - initUi() + # initUi() + setControlCHook(exit) + # Initialize prompt interface + initPrompt() - - -main() +#[ + Start main function +]# +main() \ No newline at end of file diff --git a/server/types.nim b/server/types.nim new file mode 100644 index 0000000..5fdb289 --- /dev/null +++ b/server/types.nim @@ -0,0 +1,88 @@ +import prompt +import prologue + +#[ + Console +]# +type + Console* = 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 + var prompt = Prompt.init() + console.prompt = prompt + console.dbPath = "db/conquest.db" + console.listeners = 0 + console.agents = 0 + console.activeListeners = @[] + + return console + +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() + +# 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() + +#[ + Agent +]# +type + Agent* = ref object + name*: string + +#[ + Listener +]# +type + Protocol* = enum + HTTP = "http" + + Listener* = ref object + name*: string + address*: string + port*: int + protocol*: Protocol + sleep*: int + jitter*: float + +proc newListener*(name: string, address: string, port: int): Listener = + var listener = new Listener + listener.name = name + listener.address = address + listener.port = port + listener.protocol = HTTP + listener.sleep = 5 # 5 seconds beaconing + listener.jitter = 0.2 # 20% Jitter + + return listener + +proc stringToProtocol*(protocol: string): Protocol = + case protocol + of "http": + return HTTP + else: discard \ No newline at end of file