Implemented initial version of logging system. Log formatting and content needs to be reworked.

This commit is contained in:
Jakob Friedl
2025-08-20 12:55:09 +02:00
parent 24208f3b4b
commit f69adc53a2
10 changed files with 139 additions and 74 deletions

5
.gitignore vendored
View File

@@ -3,11 +3,14 @@
*.db *.db
*.key *.key
!data/*/.gitkeep !data/*/.gitkeep
# Ignore binaries # Ignore binaries
bin/* bin/*
!bin/.gitkeep !bin/.gitkeep
*.exe *.exe
# Ignore log files
*.log
.vscode/ .vscode/
# Nim # Nim

View File

@@ -165,7 +165,7 @@ type
port*: int port*: int
protocol*: Protocol protocol*: Protocol
# Server context structures # Context structures
type type
KeyPair* = object KeyPair* = object
privateKey*: Key privateKey*: Key

View File

@@ -1,5 +1,6 @@
import terminal, strformat, strutils, sequtils, tables, times, system import terminal, strformat, strutils, sequtils, tables, times, system
import ../core/logger
import ../[utils, globals] import ../[utils, globals]
import ../db/database import ../db/database
import ../protocol/packer 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") cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.agentId} into database.", "\n")
return false return false
if not cq.makeAgentLogDirectory(agent.agentId):
cq.writeLine(fgRed, styleBright, "[-] Failed to create log")
return false
cq.agents[agent.agentId] = agent cq.agents[agent.agentId] = agent
let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss") 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) listenerId = Uuid.toString(taskResult.listenerId)
let date: string = now().format("dd-MM-yyyy HH:mm:ss") 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): case cast[StatusType](taskResult.status):
of STATUS_COMPLETED: 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: 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: of STATUS_IN_PROGRESS:
discard discard
case cast[ResultType](taskResult.resultType): case cast[ResultType](taskResult.resultType):
of RESULT_STRING: of RESULT_STRING:
if int(taskResult.length) > 0: 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 # Split result string on newline to keep formatting
for line in Bytes.toString(taskResult.data).split("\n"): for line in Bytes.toString(taskResult.data).split("\n"):
cq.writeLine(line) cq.output(line)
of RESULT_BINARY: of RESULT_BINARY:
# Write binary data to a file # Write binary data to a file

View File

@@ -1,6 +1,6 @@
import terminal, strformat, strutils, tables, times, system, parsetoml import terminal, strformat, strutils, tables, times, system, parsetoml
import ./task import ./[task, logger]
import ../utils import ../utils
import ../db/database import ../db/database
import ../../common/types 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.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.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.interactAgent = agent
cq.log(fmt"Started interacting with agent {agent.agentId}.")
while command.replace(" ", "") != "back": while command.replace(" ", "") != "back":
command = cq.readLine() command = cq.readLine()

View File

@@ -40,58 +40,58 @@ proc listenerList*(cq: Conquest) =
proc listenerStart*(cq: Conquest, host: string, portStr: string) = proc listenerStart*(cq: Conquest, host: string, portStr: string) =
# Validate arguments # 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: 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() discard listener.runAsync()
cq.add(listenerInstance) cq.add(listenerInstance)
cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on {host}:{portStr}.") cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on {host}:{portStr}.")
except CatchableError as err: except CatchableError as err:
cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg) cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg)

View File

@@ -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)))

View File

@@ -1,7 +1,7 @@
import prompt, terminal, argparse, parsetoml import prompt, terminal, argparse, parsetoml
import strutils, strformat, times, system, tables import strutils, strformat, times, system, tables
import ./[agent, listener, builder] import ./[agent, listener, builder, logger]
import ../[globals, utils] import ../[globals, utils]
import ../db/database import ../db/database
import ../../common/[types, utils, crypto, profile] import ../../common/[types, utils, crypto, profile]
@@ -150,8 +150,8 @@ proc startServer*(profilePath: string) =
try: try:
# Load and parse profile # Load and parse profile
let profile = parseFile(profilePath) let profile = parseFile(profilePath)
styledEcho(fgGreen, styleBright, "[+] Using profile \"", profile.getString("name"), "\" (", profilePath ,").") styledEcho(fgBlack, styleBright, "[*] ", "Using profile \"", profile.getString("name"), "\" (", profilePath ,").")
styledEcho(fgGreen, styleBright, "[+] ", profile.getString("private_key_file"), ": Private key found.") styledEcho(fgBlack, styleBright, "[*] ", "Using private key \"", profile.getString("private_key_file"), "\".")
# Initialize framework context # Initialize framework context
cq = Conquest.init(profile) cq = Conquest.init(profile)

View File

@@ -1,5 +1,6 @@
import times, strformat, terminal, tables, sequtils, strutils import times, strformat, terminal, tables, sequtils, strutils
import ./logger
import ../utils import ../utils
import ../protocol/parser import ../protocol/parser
import ../../modules/manager import ../../modules/manager
@@ -56,6 +57,7 @@ proc handleAgentCommand*(cq: Conquest, input: string) =
let date: string = now().format("dd-MM-yyyy HH:mm:ss") 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.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 # Convert user input into sequence of string arguments
let parsedArgs = parseInput(input) let parsedArgs = parseInput(input)
@@ -78,8 +80,8 @@ proc handleAgentCommand*(cq: Conquest, input: string) =
# Add task to queue # Add task to queue
cq.interactAgent.tasks.add(task) 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: except CatchableError:
cq.writeLine(fgRed, styleBright, fmt"[-] {getCurrentExceptionMsg()}" & "\n") cq.error(getCurrentExceptionMsg() & "\n")
return return

View File

@@ -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() conquestDb.close()
except SqliteError as err: except SqliteError as err:
cq.writeLine(fgGreen, styleBright, "[+] ", cq.dbPath, ": Database file found.") cq.writeLine(fgBlack, styleBright, "[*] Using existing database: \"", cq.dbPath, "\".\n")

View File

@@ -2,12 +2,7 @@ import strutils, terminal, tables, sequtils, times, strformat, prompt
import std/wordwrap import std/wordwrap
import ../common/types import ../common/types
import core/logger
# 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]))
proc validatePort*(portStr: string): bool = proc validatePort*(portStr: string): bool =
try: try:
@@ -19,17 +14,18 @@ proc validatePort*(portStr: string): bool =
# Function templates and overwrites # Function templates and overwrites
template writeLine*(cq: Conquest, args: varargs[untyped]) = template writeLine*(cq: Conquest, args: varargs[untyped]) =
cq.prompt.writeLine(args) cq.prompt.writeLine(args)
proc readLine*(cq: Conquest): string = proc readLine*(cq: Conquest): string =
return cq.prompt.readLine() return cq.prompt.readLine()
template setIndicator*(cq: Conquest, indicator: string) = proc setIndicator*(cq: Conquest, indicator: string) =
cq.prompt.setIndicator(indicator) cq.prompt.setIndicator(indicator)
template showPrompt*(cq: Conquest) = proc showPrompt*(cq: Conquest) =
cq.prompt.showPrompt() cq.prompt.showPrompt()
template hidePrompt*(cq: Conquest) = proc hidePrompt*(cq: Conquest) =
cq.prompt.hidePrompt() cq.prompt.hidePrompt()
template setStatusBar*(cq: Conquest, statusBar: seq[StatusBarItem]) = proc setStatusBar*(cq: Conquest, statusBar: seq[StatusBarItem]) =
cq.prompt.setStatusBar(statusBar) cq.prompt.setStatusBar(statusBar)
template clear*(cq: Conquest) = proc clear*(cq: Conquest) =
cq.prompt.clear() cq.prompt.clear()
# Overwrite withOutput function to handle function arguments # Overwrite withOutput function to handle function arguments