Impersonated user is displayed in the client UI and persisted in the team server database.

This commit is contained in:
Jakob Friedl
2025-10-17 13:01:12 +02:00
parent 0fc8ff3caa
commit 4a1a70da4d
11 changed files with 95 additions and 25 deletions

View File

@@ -3,6 +3,6 @@
-d:release
--opt:size
--passL:"-s" # Strip symbols, such as sensitive function names
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
-d:CONFIGURATION="PLACEHOLDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPLACEHOLDER"
-d:MODULES="511"
-o:"/mnt/c/Users/jakob/Documents/Projects/conquest/bin/monarch.x64.exe"

View File

@@ -96,6 +96,9 @@ proc main(ip: string = "localhost", port: int = 37573) =
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
@@ -171,6 +174,16 @@ proc main(ip: string = "localhost", port: int = 37573) =
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:
echo event.data["agentId"].getStr()
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
else: discard

View File

@@ -9,7 +9,8 @@ type
SessionsTableComponent* = ref object of RootObj
title: string
agents*: seq[UIAgent]
agentActivity*: Table[string, int64] # Direct O(1) access to latest checkin
agentActivity*: Table[string, int64] # Direct O(1) access to latest checkin
agentImpersonation*: Table[string, string]
selection: ptr ImGuiSelectionBasicStorage
consoles: ptr Table[string, ConsoleComponent]
@@ -45,6 +46,8 @@ proc interact(component: SessionsTableComponent) =
proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
igBegin(component.title, showComponent, 0)
let textSpacing = igGetStyle().ItemSpacing.x
let tableFlags = (
ImGuiTableFlags_Resizable.int32 or
ImGuiTableFlags_Reorderable.int32 or
@@ -59,7 +62,7 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
ImGui_TableFlags_SizingStretchSame.int32
)
let cols: int32 = 12
let cols: int32 = 11
if igBeginTable("Sessions", cols, tableFlags, vec2(0.0f, 0.0f), 0.0f):
igTableSetupColumn("AgentID", ImGuiTableColumnFlags_NoReorder.int32 or ImGuiTableColumnFlags_NoHide.int32, 0.0f, 0)
@@ -68,7 +71,6 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
igTableSetupColumn("IP (External)", ImGuiTableColumnFlags_DefaultHide.int32, 0.0f, 0)
igTableSetupColumn("Username", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("Hostname", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("Domain", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("OS", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("Process", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("PID", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
@@ -104,18 +106,26 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
if igTableSetColumnIndex(3):
igText(agent.ipExternal)
if igTableSetColumnIndex(4):
if not agent.domain.isEmptyOrWhitespace():
igText(agent.domain & "\\")
igSameLine(0.0f, 0.0f)
igText(agent.username)
if component.agentImpersonation.hasKey(agent.agentId):
igSameLine(0.0f, textSpacing)
igText(fmt"[{component.agentImpersonation[agent.agentId]}]")
if igTableSetColumnIndex(5):
igText(agent.hostname)
if igTableSetColumnIndex(6):
igText(if agent.domain.isEmptyOrWhitespace(): "-" else: agent.domain)
if igTableSetColumnIndex(7):
igText(agent.os)
if igTableSetColumnIndex(8):
if igTableSetColumnIndex(7):
igText(agent.process)
if igTableSetColumnIndex(9):
if igTableSetColumnIndex(8):
igText($agent.pid)
if igTableSetColumnIndex(10):
if igTableSetColumnIndex(9):
let duration = now() - agent.firstCheckin.fromUnix().local()
let totalSeconds = duration.inSeconds
@@ -126,7 +136,7 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
let timeText = dateTime(2000, mJan, 1, hours.int, minutes.int, seconds.int).format("HH:mm:ss")
igText(fmt"{timeText} ago")
if igTableSetColumnIndex(11):
if igTableSetColumnIndex(10):
let duration = now() - component.agentActivity[agent.agentId].fromUnix().local()
let totalSeconds = duration.inSeconds

View File

@@ -55,6 +55,8 @@ type
CMD_MAKE_TOKEN = 18'u16
CMD_STEAL_TOKEN = 19'u16
CMD_REV2SELF = 20'u16
CMD_TOKEN_GET_PRIV = 21'u16
CMD_TOKEN_SET_PRIV = 22'u16
StatusType* = enum
STATUS_COMPLETED = 0'u8
@@ -195,6 +197,7 @@ type
agentId*: string
listenerId*: string
username*: string
impersonationToken*: string
hostname*: string
domain*: string
ipInternal*: string
@@ -215,6 +218,7 @@ type
agentId*: string
listenerId*: string
username*: string
impersonationToken*: string
hostname*: string
domain*: string
ipInternal*: string
@@ -275,6 +279,8 @@ type
CLIENT_BUILDLOG_ITEM = 107'u8 # Add entry to the build log
CLIENT_LOOT_ADD = 108'u8 # Add file or screenshot stored on the team server to preview on the client, only sends metadata and not the actual file content
CLIENT_LOOT_DATA = 109'u8 # Send file/screenshot bytes to the client to display as preview or to download to the client desktop
CLIENT_IMPERSONATE_TOKEN = 110'u8 # Access token impersonated
CLIENT_REVERT_TOKEN = 111'u8 # Revert to original logon session
Event* = object
eventType*: EventType

View File

@@ -16,7 +16,7 @@ let module* = Module(
description: protect("Create an access token from username and password."),
example: protect("make-token LAB\\john Password123!"),
arguments: @[
Argument(name: protect("domain\\username"), description: protect("Account domain and username."), argumentType: STRING, isRequired: true),
Argument(name: protect("domain\\username"), description: protect("Account domain and username. For impersonating local users, use .\\username."), argumentType: STRING, isRequired: true),
Argument(name: protect("password"), description: protect("Account password."), argumentType: STRING, isRequired: true),
Argument(name: protect("logonType"), description: protect("Logon type (https://learn.microsoft.com/en-us/windows-server/identity/securing-privileged-access/reference-tools-logon-types)."), argumentType: INT, isRequired: false)
],
@@ -63,6 +63,9 @@ when defined(agent):
if task.argCount == 3:
logonType = cast[DWORD](Bytes.toUint32(task.args[2].data))
# Revert current token before creating a new one
discard rev2self()
if not makeToken(userParts[1], password, userParts[0], logonType):
return createTaskResult(task, STATUS_FAILED, RESULT_STRING, string.toBytes(protect("Failed to create token.")))
return createTaskResult(task, STATUS_COMPLETED, RESULT_STRING, string.toBytes(fmt"Impersonated {username}."))

View File

@@ -99,6 +99,20 @@ proc handleResult*(resultData: seq[byte]) =
cq.client.sendConsoleItem(agentId, LOG_SUCCESS, fmt"Task {taskId} completed.")
cq.success(fmt"Task {taskId} completed.")
cq.agents[agentId].tasks = cq.agents[agentId].tasks.filterIt(it.taskId != taskResult.taskId)
# Handle additional actions or UI-events based on command type (only when command succeeded)
case cast[CommandType](taskResult.command):
of CMD_MAKE_TOKEN:
let impersonationToken: string = Bytes.toString(taskResult.data).split(" ")[1][0..^2] # Remove trailing '.' character from the domain\username string
if cq.dbUpdateTokenImpersonation(agentId, impersonationToken):
cq.agents[agentId].impersonationToken = impersonationToken
cq.client.sendImpersonateToken(agentId, impersonationToken)
of CMD_REV2SELF:
if cq.dbUpdateTokenImpersonation(agentId, ""):
cq.agents[agentId].impersonationToken.setLen(0)
cq.client.sendRevertToken(agentId)
else: discard
of STATUS_FAILED:
cq.client.sendConsoleItem(agentId, LOG_ERROR, fmt"Task {taskId} failed.")
cq.error(fmt"Task {taskId} failed.")
@@ -138,11 +152,8 @@ proc handleResult*(resultData: seq[byte]) =
host: cq.agents[agentId].hostname
)
# Store loot in database
if not cq.dbStoreLoot(lootItem):
raise newException(ValueError, fmt"Failed to store loot in database." & "\n")
# Send loot to client to display file/screenshot in the UI
discard cq.dbStoreLoot(lootItem)
cq.client.sendLoot(lootItem)
cq.output(fmt"File downloaded to {downloadPath} ({$fileData.len()} bytes).", "\n")

View File

@@ -114,6 +114,7 @@ proc deserializeNewAgent*(cq: Conquest, data: seq[byte], remoteAddress: string):
agentId: Uuid.toString(header.agentId),
listenerId: Uuid.toString(listenerId),
username: username,
impersonationToken: "",
hostname: hostname,
domain: domain,
ipInternal: ipInternal,

View File

@@ -9,6 +9,7 @@ proc `%`*(agent: Agent): JsonNode =
result["agentId"] = %agent.agentId
result["listenerId"] = %agent.listenerId
result["username"] = %agent.username
result["impersonationToken"] = %agent.impersonationToken
result["hostname"] = %agent.hostname
result["domain"] = %agent.domain
result["ipInternal"] = %agent.ipInternal
@@ -188,5 +189,28 @@ proc sendLootData*(client: WsConnection, loot: LootItem, data: string) =
"data": encode(data)
}
)
if client != nil:
client.ws.sendEvent(event, client.sessionKey)
proc sendImpersonateToken*(client: WsConnection, agentId: string, username: string) =
let event = Event(
eventType: CLIENT_IMPERSONATE_TOKEN,
timestamp: now().toTime().toUnix(),
data: %*{
"agentId": agentId,
"username": username
}
)
if client != nil:
client.ws.sendEvent(event, client.sessionKey)
proc sendRevertToken*(client: WsConnection, agentId: string) =
let event = Event(
eventType: CLIENT_REVERT_TOKEN,
timestamp: now().toTime().toUnix(),
data: %*{
"agentId": agentId
}
)
if client != nil:
client.ws.sendEvent(event, client.sessionKey)

View File

@@ -28,6 +28,7 @@ proc dbInit*(cq: Conquest) =
process TEXT NOT NULL,
pid INTEGER NOT NULL,
username TEXT NOT NULL,
impersonationToken TEXT NOT NULL,
hostname TEXT NOT NULL,
domain TEXT NOT NULL,
ipInternal TEXT NOT NULL,

View File

@@ -15,9 +15,9 @@ proc dbStoreAgent*(cq: Conquest, agent: Agent): bool =
let sessionKeyBlob = agent.sessionKey.toSeq()
conquestDb.exec("""
INSERT INTO agents (agentId, listenerId, process, pid, username, hostname, domain, ipInternal, ipExternal, os, elevated, sleep, modules, firstCheckin, latestCheckin, sessionKey)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
""", agent.agentId, agent.listenerId, agent.process, agent.pid, agent.username, agent.hostname, agent.domain, agent.ipInternal, agent.ipExternal, agent.os, agent.elevated, agent.sleep, agent.modules, agent.firstCheckin, agent.latestCheckin, sessionKeyBlob)
INSERT INTO agents (agentId, listenerId, process, pid, username, impersonationToken, hostname, domain, ipInternal, ipExternal, os, elevated, sleep, modules, firstCheckin, latestCheckin, sessionKey)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
""", agent.agentId, agent.listenerId, agent.process, agent.pid, agent.username, agent.impersonationToken, agent.hostname, agent.domain, agent.ipInternal, agent.ipExternal, agent.os, agent.elevated, agent.sleep, agent.modules, agent.firstCheckin, agent.latestCheckin, sessionKeyBlob)
conquestDb.close()
except:
@@ -32,8 +32,8 @@ proc dbGetAllAgents*(cq: Conquest): seq[Agent] =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
for row in conquestDb.iterate("SELECT agentId, listenerId, sleep, process, pid, username, hostname, domain, ipInternal, ipExternal, os, elevated, modules, firstCheckin, latestCheckin, sessionKey FROM agents;"):
let (agentId, listenerId, sleep, process, pid, username, hostname, domain, ipInternal, ipExternal, os, elevated, modules, firstCheckin, latestCheckin, sessionKeyBlob) = row.unpack((string, string, int, string, int, string, string, string, string, string, string, bool, uint32, int64, int64, seq[byte]))
for row in conquestDb.iterate("SELECT agentId, listenerId, sleep, process, pid, username, impersonationToken, hostname, domain, ipInternal, ipExternal, os, elevated, modules, firstCheckin, latestCheckin, sessionKey FROM agents;"):
let (agentId, listenerId, sleep, process, pid, username, impersonationToken, hostname, domain, ipInternal, ipExternal, os, elevated, modules, firstCheckin, latestCheckin, sessionKeyBlob) = row.unpack((string, string, int, string, int, string, string, string, string, string, string, string, bool, uint32, int64, int64, seq[byte]))
# Convert session key blob back to array
var sessionKey: Key
@@ -49,6 +49,7 @@ proc dbGetAllAgents*(cq: Conquest): seq[Agent] =
sleep: sleep,
pid: pid,
username: username,
impersonationToken: impersonationToken,
hostname: hostname,
domain: domain,
ipInternal: ipInternal,
@@ -128,11 +129,11 @@ proc dbDeleteAgentByName*(cq: Conquest, agentId: string): bool =
return true
proc dbAgentExists*(cq: Conquest, agentName: string): bool =
proc dbAgentExists*(cq: Conquest, agentId: string): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
let res = conquestDb.one("SELECT 1 FROM agents WHERE agentId = ? LIMIT 1", agentName)
let res = conquestDb.one("SELECT 1 FROM agents WHERE agentId = ? LIMIT 1", agentId)
conquestDb.close()
@@ -141,11 +142,11 @@ proc dbAgentExists*(cq: Conquest, agentName: string): bool =
cq.error(getCurrentExceptionMsg())
return false
proc dbUpdateSleep*(cq: Conquest, agentName: string, delay: int): bool =
proc dbUpdateTokenImpersonation*(cq: Conquest, agentId: string, impersonationToken: string): bool =
try:
let conquestDb = openDatabase(cq.dbPath, mode=dbReadWrite)
conquestDb.exec("UPDATE agents SET sleep = ? WHERE agentId = ?", delay, agentName)
conquestDb.exec("UPDATE agents SET impersonationToken = ? WHERE agentId = ?", impersonationToken, agentId)
conquestDb.close()
return true

View File

@@ -1,4 +1,4 @@
import strformat, strutils, system, terminal, tiny_sqlite
import strformat, system, terminal, tiny_sqlite
import ../core/logger
import ../../common/types