diff --git a/.gitignore b/.gitignore index 2f4aef4..0fc0fda 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,14 @@ *.db *.key !data/*/.gitkeep -# Ignore binaries +# Ignore binaries bin/* !bin/.gitkeep *.exe +# Ignore log files +*.log + .vscode/ # Nim diff --git a/src/common/types.nim b/src/common/types.nim index e8cc7b6..4854c0a 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -165,7 +165,7 @@ type port*: int protocol*: Protocol -# Server context structures +# Context structures type KeyPair* = object privateKey*: Key diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index 0b71f76..5923def 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -1,5 +1,6 @@ import terminal, strformat, strutils, sequtils, tables, times, system +import ../core/logger import ../[utils, globals] import ../db/database import ../protocol/packer @@ -26,6 +27,10 @@ proc register*(registrationData: seq[byte]): bool = cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.agentId} into database.", "\n") return false + if not cq.makeAgentLogDirectory(agent.agentId): + cq.writeLine(fgRed, styleBright, "[-] Failed to create log") + return false + cq.agents[agent.agentId] = agent let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss") @@ -77,23 +82,23 @@ proc handleResult*(resultData: seq[byte]) = listenerId = Uuid.toString(taskResult.listenerId) let date: string = now().format("dd-MM-yyyy HH:mm:ss") - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$resultData.len} bytes received.") + cq.info(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"{$resultData.len} bytes received.") case cast[StatusType](taskResult.status): of STATUS_COMPLETED: - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {taskId} completed.") + cq.success(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {taskId} completed.") of STATUS_FAILED: - cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {taskId} failed.") + cq.error(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {taskId} failed.") of STATUS_IN_PROGRESS: discard case cast[ResultType](taskResult.resultType): of RESULT_STRING: if int(taskResult.length) > 0: - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, "Output:") + cq.info(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, "Output:") # Split result string on newline to keep formatting for line in Bytes.toString(taskResult.data).split("\n"): - cq.writeLine(line) + cq.output(line) of RESULT_BINARY: # Write binary data to a file diff --git a/src/server/core/agent.nim b/src/server/core/agent.nim index 59c3e6d..cc164fd 100644 --- a/src/server/core/agent.nim +++ b/src/server/core/agent.nim @@ -1,6 +1,6 @@ import terminal, strformat, strutils, tables, times, system, parsetoml -import ./task +import ./[task, logger] import ../utils import ../db/database import ../../common/types @@ -118,6 +118,8 @@ proc agentInteract*(cq: Conquest, name: string) = 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.agentId, resetStyle, ". Type 'help' to list available commands.\n") cq.interactAgent = agent + + cq.log(fmt"Started interacting with agent {agent.agentId}.") while command.replace(" ", "") != "back": command = cq.readLine() diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim index 1a42bc4..108a715 100644 --- a/src/server/core/listener.nim +++ b/src/server/core/listener.nim @@ -40,58 +40,58 @@ proc listenerList*(cq: Conquest) = 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 = generateUUID() - 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 based on C2 profile - # GET requests - for endpoint in cq.profile.getArray("http-get.endpoints"): - listener.addRoute(endpoint.getStringValue(), routes.httpGet) - - # POST requests - var postMethods: seq[HttpMethod] - for reqMethod in cq.profile.getArray("http-post.request-methods"): - postMethods.add(parseEnum[HttpMethod](reqMethod.getStringValue())) - - # Default method is POST - if postMethods.len == 0: - postMethods = @[HttpPost] - - for endpoint in cq.profile.getArray("http-post.endpoints"): - listener.addRoute(endpoint.getStringValue(), routes.httpPost, postMethods) - - listener.registerErrorHandler(Http404, routes.error404) - - # Store listener in database - var listenerInstance = Listener( - listenerId: name, - address: host, - port: port, - protocol: HTTP - ) - if not cq.dbStoreListener(listenerInstance): - return - - # Start serving try: + if not validatePort(portStr): + raise newException(CatchableError,fmt"[-] Invalid port number: {portStr}") + + let port = portStr.parseInt + + # Create new listener + let + name: string = generateUUID() + 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 based on C2 profile + # GET requests + for endpoint in cq.profile.getArray("http-get.endpoints"): + listener.addRoute(endpoint.getStringValue(), routes.httpGet) + + # POST requests + var postMethods: seq[HttpMethod] + for reqMethod in cq.profile.getArray("http-post.request-methods"): + postMethods.add(parseEnum[HttpMethod](reqMethod.getStringValue())) + + # Default method is POST + if postMethods.len == 0: + postMethods = @[HttpPost] + + for endpoint in cq.profile.getArray("http-post.endpoints"): + listener.addRoute(endpoint.getStringValue(), routes.httpPost, postMethods) + + listener.registerErrorHandler(Http404, routes.error404) + + # Store listener in database + var listenerInstance = Listener( + listenerId: name, + address: host, + port: port, + protocol: HTTP + ) + if not cq.dbStoreListener(listenerInstance): + raise newException(CatchableError, "Failed to store listener in database.") + + # Start serving discard listener.runAsync() cq.add(listenerInstance) cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on {host}:{portStr}.") + except CatchableError as err: cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg) diff --git a/src/server/core/logger.nim b/src/server/core/logger.nim new file mode 100644 index 0000000..c1ca725 --- /dev/null +++ b/src/server/core/logger.nim @@ -0,0 +1,57 @@ +import terminal, times, strformat, strutils +import std/[dirs, paths] +import ../../common/[types, profile] + +proc makeAgentLogDirectory*(cq: Conquest, agentId: string): bool = + try: + let cqDir = cq.profile.getString("conquest_directory") + createDir(cast[Path](fmt"{cqDir}/data/logs/{agentId}")) + return true + except OSError: + return false + +proc log*(cq: Conquest, logEntry: string) = + if cq.interactAgent == nil: + return + + let + date = now().format("dd-MM-yyyy") + timestamp = now().format("dd-MM-yyyy HH:mm:ss") + cqDir = cq.profile.getString("conquest_directory") + agentLogPath = fmt"{cqDir}/data/logs/{cq.interactAgent.agentId}/{date}.log" + + # Write log entry to file + let file = open(agentLogPath, fmAppend) + file.writeLine(fmt"[{timestamp}] {logEntry}") + file.flushFile() + +proc extractStrings(args: string): string = + if not args.startsWith("("): + return args + + # Remove styling arguments, such as fgRed, styleBright, resetStyle, etc. by extracting only arguments that are quoted + var message: string + for str in args[1..^2].split(", "): + if str.startsWith("\""): + message &= str + return message.replace("\"", "") + +template info*(cq: Conquest, args: varargs[untyped]) = + cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, args) + cq.log("[*] " & extractStrings($(args))) + +template error*(cq: Conquest, args: varargs[untyped]) = + cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, args) + cq.log("[-] " & extractStrings($(args))) + +template warn*(cq: Conquest, args: varargs[untyped]) = + cq.writeLine(fgYellow, "[!] ", resetStyle, args) + cq.log("[!] " & extractStrings($(args))) + +template success*(cq: Conquest, args: varargs[untyped]) = + cq.writeLine(fgGreen, "[+] ", resetStyle, args) + cq.log("[+] " & extractStrings($(args))) + +template output*(cq: Conquest, args: varargs[untyped]) = + cq.writeLine(args) + cq.log("[>] " & extractStrings($(args))) diff --git a/src/server/core/server.nim b/src/server/core/server.nim index d48d0bf..e401547 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -1,7 +1,7 @@ import prompt, terminal, argparse, parsetoml import strutils, strformat, times, system, tables -import ./[agent, listener, builder] +import ./[agent, listener, builder, logger] import ../[globals, utils] import ../db/database import ../../common/[types, utils, crypto, profile] @@ -150,8 +150,8 @@ proc startServer*(profilePath: string) = try: # Load and parse profile let profile = parseFile(profilePath) - styledEcho(fgGreen, styleBright, "[+] Using profile \"", profile.getString("name"), "\" (", profilePath ,").") - styledEcho(fgGreen, styleBright, "[+] ", profile.getString("private_key_file"), ": Private key found.") + styledEcho(fgBlack, styleBright, "[*] ", "Using profile \"", profile.getString("name"), "\" (", profilePath ,").") + styledEcho(fgBlack, styleBright, "[*] ", "Using private key \"", profile.getString("private_key_file"), "\".") # Initialize framework context cq = Conquest.init(profile) diff --git a/src/server/core/task.nim b/src/server/core/task.nim index 96e3b55..54b3185 100644 --- a/src/server/core/task.nim +++ b/src/server/core/task.nim @@ -1,5 +1,6 @@ import times, strformat, terminal, tables, sequtils, strutils +import ./logger import ../utils import ../protocol/parser import ../../modules/manager @@ -56,6 +57,7 @@ proc handleAgentCommand*(cq: Conquest, input: string) = let date: string = now().format("dd-MM-yyyy HH:mm:ss") cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.agentId}] ", resetStyle, styleBright, input) + cq.log(fmt"Agent command received: {input}") # Convert user input into sequence of string arguments let parsedArgs = parseInput(input) @@ -78,8 +80,8 @@ proc handleAgentCommand*(cq: Conquest, input: string) = # Add task to queue cq.interactAgent.tasks.add(task) - cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}") + cq.info(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, fmt"Tasked agent to {command.description.toLowerAscii()}") except CatchableError: - cq.writeLine(fgRed, styleBright, fmt"[-] {getCurrentExceptionMsg()}" & "\n") + cq.error(getCurrentExceptionMsg() & "\n") return \ No newline at end of file diff --git a/src/server/db/database.nim b/src/server/db/database.nim index e74648a..b48f96c 100644 --- a/src/server/db/database.nim +++ b/src/server/db/database.nim @@ -41,7 +41,7 @@ proc dbInit*(cq: Conquest) = """) - cq.writeLine(fgGreen, styleBright, "[+] ", cq.dbPath, ": Database created.") + cq.writeLine(fgBlack, styleBright, "[*] Using new database: \"", cq.dbPath, "\".\n") conquestDb.close() except SqliteError as err: - cq.writeLine(fgGreen, styleBright, "[+] ", cq.dbPath, ": Database file found.") + cq.writeLine(fgBlack, styleBright, "[*] Using existing database: \"", cq.dbPath, "\".\n") diff --git a/src/server/utils.nim b/src/server/utils.nim index 7e16fd8..1154cb3 100644 --- a/src/server/utils.nim +++ b/src/server/utils.nim @@ -2,12 +2,7 @@ import strutils, terminal, tables, sequtils, times, strformat, prompt import std/wordwrap import ../common/types - -# Utility functions -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])) +import core/logger proc validatePort*(portStr: string): bool = try: @@ -19,17 +14,18 @@ proc validatePort*(portStr: string): bool = # Function templates and overwrites 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) = +proc setIndicator*(cq: Conquest, indicator: string) = cq.prompt.setIndicator(indicator) -template showPrompt*(cq: Conquest) = +proc showPrompt*(cq: Conquest) = cq.prompt.showPrompt() -template hidePrompt*(cq: Conquest) = +proc hidePrompt*(cq: Conquest) = cq.prompt.hidePrompt() -template setStatusBar*(cq: Conquest, statusBar: seq[StatusBarItem]) = +proc setStatusBar*(cq: Conquest, statusBar: seq[StatusBarItem]) = cq.prompt.setStatusBar(statusBar) -template clear*(cq: Conquest) = +proc clear*(cq: Conquest) = cq.prompt.clear() # Overwrite withOutput function to handle function arguments