Started porting over functionality to the ImGui client via websocket communication.

This commit is contained in:
Jakob Friedl
2025-09-25 19:22:17 +02:00
parent f0dbcdfc58
commit 14771a4b50
22 changed files with 455 additions and 569 deletions

View File

@@ -1,5 +1,6 @@
switch "o", "bin/client"
switch "d", "client"
switch "d", "ImGuiTextSelect"
# Select compiler

77
src/client/event/recv.nim Normal file
View File

@@ -0,0 +1,77 @@
import whisky
import times, tables
import ../views/[sessions, listeners, console, eventlog]
import ../../common/[types, utils, event]
export recvEvent
#[
Server -> Client
]#
# proc getMessageType*(message: Message): EventType =
# var unpacker = Unpacker.init(message.data)
# return cast[EventType](unpacker.getUint8())
# proc receiveAgentPayload*(message: Message): seq[byte] =
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# return string.toBytes(unpacker.getDataWithLengthPrefix())
# proc receiveAgentConnection*(message: Message, sessions: ptr SessionsTableComponent) =
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# let agent = Agent(
# agentId: Uuid.toString(unpacker.getUint32()),
# listenerId: Uuid.toString(unpacker.getUint32()),
# username: unpacker.getDataWithLengthPrefix(),
# hostname: unpacker.getDataWithLengthPrefix(),
# domain: unpacker.getDataWithLengthPrefix(),
# ip: unpacker.getDataWithLengthPrefix(),
# os: unpacker.getDataWithLengthPrefix(),
# process: unpacker.getDataWithLengthPrefix(),
# pid: int(unpacker.getUint32()),
# elevated: unpacker.getUint8() != 0,
# sleep: int(unpacker.getUint32()),
# tasks: @[],
# firstCheckin: cast[int64](unpacker.getUint32()).fromUnix().utc(),
# latestCheckin: cast[int64](unpacker.getUint32()).fromUnix().utc(),
# )
# sessions.agents.add(agent)
# proc receiveAgentCheckin*(message: Message, sessions: ptr SessionsTableComponent)=
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# let agentId = Uuid.toString(unpacker.getUint32())
# let timestamp = cast[int64](unpacker.getUint32())
# # TODO: Update checkin
# proc receiveConsoleItem*(message: Message, consoles: ptr Table[string, ConsoleComponent]) =
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# let
# agentId = Uuid.toString(unpacker.getUint32())
# logType = cast[LogType](unpacker.getUint8())
# timestamp = cast[int64](unpacker.getUint32())
# message = unpacker.getDataWithLengthPrefix()
# consoles[][agentId].addItem(logType, message, timestamp)
# proc receiveEventlogItem*(message: Message, eventlog: ptr EventlogComponent) =
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# let
# logType = cast[LogType](unpacker.getUint8())
# timestamp = cast[int64](unpacker.getUint32())
# message = unpacker.getDataWithLengthPrefix()
# eventlog[].addItem(logType, message, timestamp)

55
src/client/event/send.nim Normal file
View File

@@ -0,0 +1,55 @@
import whisky
import times, tables
import ../views/[sessions, listeners, console, eventlog]
import ../../common/[types, utils, serialize, event]
export sendHeartbeat
#[
Client -> Server
]#
proc sendStartListener*(ws: WebSocket, listener: UIListener) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_LISTENER_START))
packer.add(string.toUUid(listener.listenerId))
packer.addDataWithLengthPrefix(string.toBytes(listener.address))
packer.add(cast[uint16](listener.port))
packer.add(cast[uint8](listener.protocol))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendStopListener*(ws: WebSocket, listenerId: string) =
discard
# var packer = Packer.init()
# packer.add(cast[uint8](CLIENT_LISTENER_STOP))
# packer.add(string.toUuid(listenerId))
# let data = packer.pack()
# ws.send(Bytes.toString(data), BinaryMessage)
# proc sendAgentCommand*(ws: WebSocket, agentId: string, command: string) =
# var packer = Packer.init()
# packer.add(cast[uint8](CLIENT_AGENT_COMMAND))
# packer.add(string.toUuid(agentId))
# packer.addDataWithLengthPrefix(string.toBytes(command))
# let data = packer.pack()
# ws.send(Bytes.toString(data), BinaryMessage)
# proc sendAgentBuild*(ws: WebSocket, listenerId: string, sleepDelay: int, sleepMask: SleepObfuscationTechnique, spoofStack: bool, modules: uint32) =
# var packer = Packer.init()
# packer.add(cast[uint8](CLIENT_AGENT_BUILD))
# packer.add(string.toUuid(listenerId))
# packer.add(cast[uint32](sleepDelay))
# packer.add(cast[uint8](sleepMask))
# packer.add(cast[uint8](spoofStack))
# packer.add(modules)
# let data = packer.pack()
# ws.send(Bytes.toString(data), BinaryMessage)

View File

@@ -1,157 +0,0 @@
[Window][Sessions [Table View]]
Pos=10,43
Size=1310,279
Collapsed=0
DockId=0x00000003,0
[Window][Listeners]
Pos=10,324
Size=1888,665
Collapsed=0
DockId=0x00000006,0
[Window][Eventlog]
Pos=1322,43
Size=576,279
Collapsed=0
DockId=0x00000004,0
[Window][Dear ImGui Demo]
Pos=1322,43
Size=576,279
Collapsed=0
DockId=0x00000004,1
[Window][Dockspace]
Pos=0,0
Size=1908,999
Collapsed=0
[Window][[FACEDEAD] bob@LAPTOP-02]
Pos=10,395
Size=1888,594
Collapsed=0
DockId=0x00000006,1
[Window][[C9D8E7F6] charlie@SERVER-03]
Pos=10,324
Size=1888,665
Collapsed=0
DockId=0x00000006,1
[Window][Debug##Default]
Pos=60,60
Size=400,400
Collapsed=0
[Window][[G1H2I3J5] diana@WORKSTATION-04]
Pos=10,125
Size=784,665
Collapsed=0
DockId=0x00000006,1
[Window][[DEADBEEF] alice@DESKTOP-01]
Pos=10,324
Size=1888,665
Collapsed=0
DockId=0x00000006,1
[Window][Example: Console]
Pos=10,661
Size=2848,1024
Collapsed=0
DockId=0x00000006,2
[Window][Example: Assets Browser]
Pos=60,60
Size=800,480
Collapsed=0
[Window][Example: Documents]
Pos=186,108
Size=997,993
Collapsed=0
[Window][Example: Log]
Pos=119,266
Size=1717,576
Collapsed=0
[Window][Same title as another window##1]
Pos=274,278
Size=754,103
Collapsed=1
[Window][Same title as another window##2]
Pos=100,200
Size=754,103
Collapsed=0
DockId=0x00000009,1
[Window][###AnimatedTitle]
Pos=100,200
Size=754,103
Collapsed=0
DockId=0x00000009,0
[Window][Delete?]
Pos=696,412
Size=516,175
Collapsed=0
[Window][Stacked 1]
Pos=588,335
Size=669,457
Collapsed=0
[Window][StartListener]
Pos=753,446
Size=76,76
Collapsed=0
[Window][Start Listener]
Pos=704,387
Size=500,225
Collapsed=0
[Window][Dear ImGui Demo/0_E1BADA21]
IsChild=1
Size=1363,540
[Window][Generate Payload]
Pos=704,161
Size=500,677
Collapsed=0
[Window][Generate Payload/0_B6B17D5F]
IsChild=1
Size=217,310
[Table][0x32886A44,8]
Column 0 Weight=0.6432
Column 1 Weight=0.9647
Column 2 Weight=0.6694
Column 3 Weight=1.0960
Column 4 Weight=1.5816
Column 5 Weight=1.1551
Column 6 Weight=0.4331
Column 7 Weight=1.4570
[Table][0xB6880529,2]
RefScale=27
Column 0 Sort=0v
[Table][0x064A67CC,4]
Column 0 Weight=1.2081
Column 1 Weight=1.3299
Column 2 Weight=0.4873
Column 3 Weight=0.9746
[Docking][Data]
DockNode ID=0x00000009 Pos=100,200 Size=754,103 Selected=0x64D005CF
DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y
DockNode ID=0x00000005 Parent=0x85940918 SizeRef=1888,279 Split=X
DockNode ID=0x00000003 Parent=0x00000005 SizeRef=1310,159 CentralNode=1 Selected=0x61E02D75
DockNode ID=0x00000004 Parent=0x00000005 SizeRef=576,159 Selected=0x5E5F7166
DockNode ID=0x00000006 Parent=0x85940918 SizeRef=1888,665 Selected=0x6BE22050

View File

@@ -1,19 +1,21 @@
import whisky
import tables, strutils
import tables, strutils, json, parsetoml
import ./utils/appImGui
import ./views/[dockspace, sessions, listeners, eventlog, console]
import ../common/[types, utils]
import ./websocket
import ./event/[send, recv]
import sugar
proc main() =
var app = createApp(1024, 800, imnodes = true, title = "Conquest", docking = true)
defer: app.destroyApp()
var
profile: Profile
views: Table[string, ptr bool]
showConquest = true
showSessionsTable = true
showSessionsGraph = false
showListeners = true
showEventlog = true
consoles: Table[string, ConsoleComponent]
@@ -25,7 +27,6 @@ proc main() =
dockTopRight: ImGuiID = 0
views["Sessions [Table View]"] = addr showSessionsTable
views["Sessions [Graph View]"] = addr showSessionsGraph
views["Listeners"] = addr showListeners
views["Eventlog"] = addr showEventlog
@@ -58,12 +59,34 @@ proc main() =
ws.sendHeartbeat()
# Receive and parse websocket response message
let message = ws.receiveMessage().get()
case message.getMessageType()
of CLIENT_EVENT_LOG:
message.receiveEventlogItem(addr eventlog)
else: discard
let event = recvEvent(ws.receiveMessage().get())
case event.eventType:
of CLIENT_PROFILE:
profile = parseString(event.data["profile"].getStr())
of CLIENT_LISTENER_ADD:
let listener = event.data.to(UIListener)
dump listener.listenerId
listenersTable.listeners.add(listener)
of CLIENT_AGENT_ADD:
let agent = event.data.to(UIAgent)
dump agent.agentId
sessionsTable.agents.add(agent)
of CLIENT_AGENT_CHECKIN:
discard
of CLIENT_AGENT_PAYLOAD:
discard
of CLIENT_CONSOLE_ITEM:
consoles[event.data["agentId"].getStr()].addItem(cast[LogType](event.data["logType"].getInt()), event.data["message"].getStr(), event.timestamp)
of CLIENT_EVENTLOG_ITEM:
eventlog.addItem(cast[LogType](event.data["logType"].getInt()), event.data["message"].getStr(), event.timestamp)
else: discard
# Draw/update UI components/views
dockspace.draw(addr showConquest, views, addr dockTop, addr dockBottom, addr dockTopLeft, addr dockTopRight)

View File

@@ -7,7 +7,7 @@ import ../../common/[types]
const MAX_INPUT_LENGTH = 512
type
ConsoleComponent* = ref object of RootObj
agent*: Agent
agent*: UIAgent
showConsole*: bool
inputBuffer: array[MAX_INPUT_LENGTH, char]
console*: ConsoleItems
@@ -42,7 +42,7 @@ proc getLineAtIndex(i: csize_t, data: pointer, outLen: ptr csize_t): cstring {.c
outLen[] = line.len.csize_t
return line
proc Console*(agent: Agent): ConsoleComponent =
proc Console*(agent: UIAgent): ConsoleComponent =
result = new ConsoleComponent
result.agent = agent
result.showConsole = true

View File

@@ -3,41 +3,25 @@ import imguin/[cimgui, glfw_opengl, simple]
import ../utils/appImGui
import ../../common/[types, utils]
import ./modals/[startListener, generatePayload]
import ../websocket
import ../event/send
import whisky
type
ListenersTableComponent* = ref object of RootObj
title: string
listeners*: seq[Listener]
listeners*: seq[UIListener]
selection: ptr ImGuiSelectionBasicStorage
startListenerModal: ListenerModalComponent
generatePayloadModal: AgentModalComponent
let exampleListeners: seq[Listener] = @[
Listener(
listenerId: "L1234567",
address: "192.168.1.1",
port: 8080,
protocol: HTTP
),
Listener(
listenerId: "L7654321",
address: "10.0.0.2",
port: 443,
protocol: HTTP
)
]
proc ListenersTable*(title: string): ListenersTableComponent =
result = new ListenersTableComponent
result.title = title
result.listeners = exampleListeners
result.listeners = @[]
result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage()
result.startListenerModal = ListenerModal()
result.generatePayloadModal = AgentModal()
proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebSocket) =
igBegin(component.title, showComponent, 0)
defer: igEnd()
@@ -73,7 +57,7 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebS
ImGuiTableFlags_ScrollY.int32 or
ImGuiTableFlags_ScrollX.int32 or
ImGuiTableFlags_NoBordersInBodyUntilResize.int32 or
ImGui_TableFlags_SizingStretchProp.int32
ImGui_TableFlags_SizingStretchSame.int32
)
let cols: int32 = 4
@@ -114,7 +98,7 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebS
if igMenuItem("Stop", nil, false, true):
# Update agents table with only non-selected ones
var newListeners: seq[Listener] = @[]
var newListeners: seq[UIListener] = @[]
for i in 0 ..< component.listeners.len():
if not ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
newListeners.add(component.listeners[i])

View File

@@ -39,7 +39,7 @@ proc resetModalValues(component: AgentModalComponent) =
component.spoofStack = false
component.moduleSelection.reset()
proc draw*(component: AgentModalComponent, listeners: seq[Listener]) =
proc draw*(component: AgentModalComponent, listeners: seq[UIListener]) =
let textSpacing = igGetStyle().ItemSpacing.x

View File

@@ -25,7 +25,7 @@ proc resetModalValues(component: ListenerModalComponent) =
component.port = DEFAULT_PORT
component.protocol = 0
proc draw*(component: ListenerModalComponent): Listener =
proc draw*(component: ListenerModalComponent): UIListener =
let textSpacing = igGetStyle().ItemSpacing.x
# Center modal
@@ -76,7 +76,7 @@ proc draw*(component: ListenerModalComponent): Listener =
if igButton("Start", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)):
result = Listener(
result = UIListener(
listenerId: generateUUID(),
address: $(addr component.address[0]),
port: int(component.port),

View File

@@ -8,85 +8,14 @@ import ../../common/[types, utils]
type
SessionsTableComponent* = ref object of RootObj
title: string
agents*: seq[Agent]
agents*: seq[UIAgent]
selection: ptr ImGuiSelectionBasicStorage
consoles: ptr Table[string, ConsoleComponent]
let exampleAgents: seq[Agent] = @[
Agent(
agentId: "DEADBEEF",
listenerId: "L1234567",
username: "alice",
hostname: "DESKTOP-01",
domain: "corp.local",
ip: "192.168.1.10",
os: "Windows 10",
process: "explorer.exe",
pid: 2340,
elevated: true,
sleep: 60,
tasks: @[],
firstCheckin: now() - initDuration(hours = 2),
latestCheckin: now(),
sessionKey: [byte 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
),
Agent(
agentId: "FACEDEAD",
listenerId: "L7654321",
username: "bob",
hostname: "LAPTOP-02",
domain: "corp.local",
ip: "10.0.0.5",
os: "Windows 11",
process: "cmd.exe",
pid: 4567,
elevated: false,
sleep: 120,
tasks: @[],
firstCheckin: now() - initDuration(hours = 1, minutes = 30),
latestCheckin: now() - initDuration(minutes = 5),
sessionKey: [byte 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
),
Agent(
agentId: "C9D8E7F6",
listenerId: "L2468135",
username: "charlie",
hostname: "SERVER-03",
domain: "child.corp.local",
ip: "172.16.0.20",
os: "Windows Server 2019",
process: "powershell.exe",
pid: 7890,
elevated: true,
sleep: 30,
tasks: @[],
firstCheckin: now() - initDuration(hours = 3, minutes = 15),
latestCheckin: now() - initDuration(minutes = 10),
sessionKey: [byte 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
),
Agent(
agentId: "G1H2I3J5",
listenerId: "L1357924",
username: "diana",
hostname: "WORKSTATION-04",
domain: "external.local",
ip: "192.168.2.15",
os: "Windows 10",
process: "chrome.exe",
pid: 3210,
elevated: false,
sleep: 90,
tasks: @[],
firstCheckin: now() - initDuration(hours = 4),
latestCheckin: now() - initDuration(minutes = 2),
sessionKey: [byte 5, 4, 3, 2, 1, 0, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6]
)
]
proc SessionsTable*(title: string, consoles: ptr Table[string, ConsoleComponent]): SessionsTableComponent =
result = new SessionsTableComponent
result.title = title
result.agents = exampleAgents
result.agents = @[]
result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage()
result.consoles = consoles
@@ -97,6 +26,7 @@ proc interact(component: SessionsTableComponent) =
while ImGuiSelectionBasicStorage_GetNextSelectedItem(component.selection, addr it, addr row):
let agent = component.agents[cast[int](row)]
# Create a new console window
if not component.consoles[].hasKey(agent.agentId):
component.consoles[][agent.agentId] = Console(agent)
@@ -105,6 +35,8 @@ proc interact(component: SessionsTableComponent) =
else:
igSetWindowFocus_Str(fmt"[{agent.agentId}] {agent.username}@{agent.hostname}")
# TODO: Clear selection properly
ImGuiSelectionBasicStorage_Clear(component.selection)
proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
igBegin(component.title, showComponent, 0)
@@ -121,7 +53,7 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
ImGuiTableFlags_ScrollY.int32 or
ImGuiTableFlags_ScrollX.int32 or
ImGuiTableFlags_NoBordersInBodyUntilResize.int32 or
ImGui_TableFlags_SizingStretchProp.int32
ImGui_TableFlags_SizingStretchSame.int32
)
let cols: int32 = 8
@@ -134,7 +66,7 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
igTableSetupColumn("OS", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("Process", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("PID", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("Activity", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupColumn("Last seen", ImGuiTableColumnFlags_None.int32, 0.0f, 0)
igTableSetupScrollFreeze(0, 1)
igTableHeadersRow()
@@ -170,7 +102,17 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
if igTableSetColumnIndex(6):
igText($agent.pid)
if igTableSetColumnIndex(7):
igText(agent.latestCheckin.format("yyyy-MM-dd HH:mm:ss"))
let duration = now() - agent.latestCheckin.fromUnix().utc()
let totalSeconds = duration.inSeconds
let hours = totalSeconds div 3600
let minutes = (totalSeconds mod 3600) div 60
let seconds = totalSeconds mod 60
let dummyTime = dateTime(2000, mJan, 1, hours.int, minutes.int, seconds.int)
let timeText = dummyTime.format("HH:mm:ss")
igText(fmt"{timeText} ago")
# Handle right-click context menu
# Right-clicking the table header to hide/show columns or reset the layout is only possible when no sessions are selected
@@ -182,7 +124,7 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
if igMenuItem("Remove", nil, false, true):
# Update agents table with only non-selected ones
var newAgents: seq[Agent] = @[]
var newAgents: seq[UIAgent] = @[]
for i in 0 ..< component.agents.len():
if not ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
newAgents.add(component.agents[i])

View File

@@ -1,135 +0,0 @@
import times, tables
import ../common/[types, utils, serialize]
import views/[sessions, listeners, console, eventlog]
import whisky
#[
[ Sending Functions ]
Client -> Server
- Heartbeat
- ListenerStart
- ListenerStop
- AgentBuild
- AgentCommand
]#
proc sendHeartbeat*(ws: WebSocket) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_HEARTBEAT))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendStartListener*(ws: WebSocket, listener: Listener) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_LISTENER_START))
packer.add(string.toUUid(listener.listenerId))
packer.addDataWithLengthPrefix(string.toBytes(listener.address))
packer.add(cast[uint16](listener.port))
packer.add(cast[uint8](listener.protocol))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendStopListener*(ws: WebSocket, listenerId: string) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_LISTENER_STOP))
packer.add(string.toUuid(listenerId))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendAgentCommand*(ws: WebSocket, agentId: string, command: string) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_AGENT_COMMAND))
packer.add(string.toUuid(agentId))
packer.addDataWithLengthPrefix(string.toBytes(command))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendAgentBuild*(ws: WebSocket, listenerId: string, sleepDelay: int, sleepMask: SleepObfuscationTechnique, spoofStack: bool, modules: uint32) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_AGENT_BUILD))
packer.add(string.toUuid(listenerId))
packer.add(cast[uint32](sleepDelay))
packer.add(cast[uint8](sleepMask))
packer.add(cast[uint8](spoofStack))
packer.add(modules)
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
#[
[ Retrieval Functions ]
Server -> Client
]#
proc getMessageType*(message: Message): WsPacketType =
var unpacker = Unpacker.init(message.data)
return cast[WsPacketType](unpacker.getUint8())
proc receiveAgentPayload*(message: Message): seq[byte] =
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
return string.toBytes(unpacker.getDataWithLengthPrefix())
proc receiveAgentConnection*(message: Message, sessions: ptr SessionsTableComponent) =
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
let agent = Agent(
agentId: Uuid.toString(unpacker.getUint32()),
listenerId: Uuid.toString(unpacker.getUint32()),
username: unpacker.getDataWithLengthPrefix(),
hostname: unpacker.getDataWithLengthPrefix(),
domain: unpacker.getDataWithLengthPrefix(),
ip: unpacker.getDataWithLengthPrefix(),
os: unpacker.getDataWithLengthPrefix(),
process: unpacker.getDataWithLengthPrefix(),
pid: int(unpacker.getUint32()),
elevated: unpacker.getUint8() != 0,
sleep: int(unpacker.getUint32()),
tasks: @[],
firstCheckin: cast[int64](unpacker.getUint32()).fromUnix().utc(),
latestCheckin: cast[int64](unpacker.getUint32()).fromUnix().utc(),
)
sessions.agents.add(agent)
proc receiveAgentCheckin*(message: Message, sessions: ptr SessionsTableComponent)=
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
let agentId = Uuid.toString(unpacker.getUint32())
let timestamp = cast[int64](unpacker.getUint32())
# TODO: Update checkin
proc receiveConsoleItem*(message: Message, consoles: ptr Table[string, ConsoleComponent]) =
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
let
agentId = Uuid.toString(unpacker.getUint32())
logType = cast[LogType](unpacker.getUint8())
timestamp = cast[int64](unpacker.getUint32())
message = unpacker.getDataWithLengthPrefix()
consoles[][agentId].addItem(logType, message, timestamp)
proc receiveEventlogItem*(message: Message, eventlog: ptr EventlogComponent) =
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
let
logType = cast[LogType](unpacker.getUint8())
timestamp = cast[int64](unpacker.getUint32())
message = unpacker.getDataWithLengthPrefix()
eventlog[].addItem(logType, message, timestamp)

34
src/common/event.nim Normal file
View File

@@ -0,0 +1,34 @@
when defined(server):
import mummy
when defined(client):
import whisky
import times, json
import ./[types, utils, serialize]
proc sendEvent*(ws: WebSocket, event: Event) =
var packer = Packer.init()
packer.add(cast[uint8](event.eventType))
packer.add(cast[uint32](event.timestamp))
packer.addDataWithLengthPrefix(string.toBytes($event.data))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc recvEvent*(message: Message): Event =
var unpacker = Unpacker.init(message.data)
return Event(
eventType: cast[EventType](unpacker.getUint8()),
timestamp: cast[int64](unpacker.getUint32()),
data: parseJson(unpacker.getDataWithLengthPrefix())
)
proc sendHeartbeat*(ws: WebSocket) =
let event = Event(
eventType: CLIENT_HEARTBEAT,
timestamp: now().toTime().toUnix(),
data: %*{}
)
ws.sendEvent(event)

View File

@@ -1,7 +1,7 @@
import prompt
import tables
import times
import parsetoml
import parsetoml, json
import mummy
# Custom Binary Task structure
@@ -202,6 +202,22 @@ type
latestCheckin*: DateTime
sessionKey*: Key
# Session entry for client UI
UIAgent* = ref object
agentId*: string
listenerId*: string
username*: string
hostname*: string
domain*: string
ip*: string
os*: string
process*: string
pid*: int
elevated*: bool
sleep*: int
firstCheckin*: int64
latestCheckin*: int64
# Listener structure
type
Protocol* {.size: sizeof(uint8).} = enum
@@ -214,6 +230,12 @@ type
port*: int
protocol*: Protocol
UIListener* = ref object of RootObj
listenerId*: string
address*: string
port*: int
protocol*: Protocol
# Context structures
type
KeyPair* = object
@@ -225,7 +247,8 @@ type
Conquest* = ref object
prompt*: Prompt
dbPath*: string
listeners*: Table[string, tuple[listener: Listener, thread: Thread[Listener]]]
listeners*: Table[string, Listener]
threads*: Table[string, Thread[Listener]]
agents*: Table[string, Agent]
interactAgent*: Agent
keyPair*: KeyPair
@@ -280,19 +303,27 @@ type
Client <-> Server WebSocket communication
]#
type
WsPacketType* = enum
# Sent by client
EventType* = enum
CLIENT_HEARTBEAT = 0'u8 # Basic checkin
# Sent by client
CLIENT_AGENT_BUILD = 1'u8 # Generate an agent binary for a specific listener
CLIENT_AGENT_COMMAND = 2'u8 # Instruct TS to send queue a command for a specific agent
CLIENT_AGENT_COMMAND = 2'u8 # Instruct TS to send queue a command for a specific agent
CLIENT_LISTENER_START = 3'u8 # Start a listener on the TS
CLIENT_LISTENER_STOP = 4'u8 # Stop a listener
# Sent by team server
CLIENT_AGENT_BINARY = 100'u8 # Return the agent binary to write to the operator's client machine
CLIENT_AGENT_CONNECTION = 101'u8 # Notify new agent connection
CLIENT_AGENT_CHECKIN = 102'u8 # Update agent checkin
CLIENT_CONSOLE_LOG = 103'u8 # Add entry to a agent's console
CLIENT_EVENT_LOG = 104'u8 # Add entry to the eventlog
CLIENT_PROFILE = 100'u8 # Team server profile and configuration
CLIENT_LISTENER_ADD = 101'u8 # Add listener to listeners table
CLIENT_AGENT_ADD = 102'u8 # Add agent to sessions table
CLIENT_AGENT_CHECKIN = 103'u8 # Update agent checkin
CLIENT_AGENT_PAYLOAD = 104'u8 # Return agent payload binary
CLIENT_CONSOLE_ITEM = 105'u8 # Add entry to a agent's console
CLIENT_EVENTLOG_ITEM = 106'u8 # Add entry to the eventlog
Event* = object
eventType*: EventType
timestamp*: int64
data*: JsonNode
CLIENT_CONNECTION = 200'u8 # Return team server profile

View File

@@ -4,6 +4,7 @@ import ../globals
import ../db/database
import ../protocol/packer
import ../core/logger
import ../event/send
import ../../common/[types, utils, serialize]
#[
@@ -37,6 +38,9 @@ proc register*(registrationData: seq[byte]): bool =
cq.info("Agent ", fgYellow, styleBright, agent.agentId, resetStyle, " connected to listener ", fgGreen, styleBright, agent.listenerId, resetStyle, ": ", fgYellow, styleBright, fmt"{agent.username}@{agent.hostname}", "\n")
cq.ws.sendAgent(agent)
cq.ws.sendEventlogItem(LOG_INFO_SHORT, fmt"Agent {agent.agentId} connected to listener {agent.listenerId}.")
return true
except CatchableError as err:
@@ -69,6 +73,7 @@ proc getTasks*(heartbeat: seq[byte]): seq[seq[byte]] =
# Update the last check-in date for the accessed agent
cq.agents[agentId].latestCheckin = cast[int64](timestamp).fromUnix().local()
# cq.ws.sendAgentCheckin(agentId)
# Return tasks
for task in cq.agents[agentId].tasks.mitems: # Iterate over agents as mutable items in order to modify GMAC tag

View File

@@ -141,7 +141,7 @@ proc agentBuild*(cq: Conquest, listener, sleepDelay: string, sleepTechnique: str
cq.error(fmt"Listener {listener.toUpperAscii} does not exist.")
return false
let listener = cq.listeners[listener.toUpperAscii].listener
let listener = cq.listeners[listener.toUpperAscii]
var config: seq[byte]
if sleepDelay.isEmptyOrWhitespace():

View File

@@ -8,7 +8,7 @@ import ../api/routes
import ../db/database
import ../core/logger
import ../../common/[types, utils, profile]
import ../websocket/send
import ../event/send
#[
Listener management
@@ -79,7 +79,9 @@ proc listenerStart*(cq: Conquest, name: string, host: string, port: int, protoco
createThread(thread, serve, listener)
server.waitUntilReady()
cq.listeners[name] = (listener, thread)
cq.listeners[name] = listener
cq.threads[name] = thread
if not cq.dbStoreListener(listener):
raise newException(CatchableError, "Failed to store listener in database.")
@@ -97,7 +99,6 @@ proc restartListeners*(cq: Conquest) =
for listener in listeners:
try:
# Create new listener
let name: string = generateUUID()
var router: Router
router.notFoundHandler = routes.error404
router.methodNotAllowedHandler = routes.error405
@@ -128,7 +129,9 @@ proc restartListeners*(cq: Conquest) =
createThread(thread, serve, listener)
server.waitUntilReady()
cq.listeners[listener.listenerId] = (listener, thread)
cq.listeners[listener.listenerId] = listener
cq.threads[listener.listenerId] = thread
cq.success("Restarted listener", fgGreen, fmt" {listener.listenerId} ", resetStyle, fmt"on {listener.address}:{$listener.port}.")
except CatchableError as err:

View File

@@ -5,8 +5,8 @@ import ./[agent, listener, builder]
import ../globals
import ../db/database
import ../core/logger
import ../../common/[types, crypto, profile]
import ../websocket/[receive, send]
import ../../common/[types, crypto, utils, profile]
import ../event/[recv, send]
import mummy, mummy/routers
#[
@@ -88,10 +88,10 @@ proc handleConsoleCommand(cq: Conquest, args: string) =
of "list":
cq.listenerList()
of "start":
#cq.listenerStart(opts.listener.get.start.get.ip, opts.listener.get.start.get.port)
cq.listenerStart(generateUUID(), opts.listener.get.start.get.ip, parseInt(opts.listener.get.start.get.port), HTTP)
discard
of "stop":
#cq.listenerStop(opts.listener.get.stop.get.name)
cq.listenerStop(opts.listener.get.stop.get.name)
discard
else:
cq.listenerUsage()
@@ -133,7 +133,8 @@ proc header() =
proc init*(T: type Conquest, profile: Profile): Conquest =
var cq = new Conquest
cq.prompt = Prompt.init()
cq.listeners = initTable[string, tuple[listener: Listener, thread: Thread[Listener]]]()
cq.listeners = initTable[string, Listener]()
cq.threads = initTable[string, Thread[Listener]]()
cq.agents = initTable[string, Agent]()
cq.interactAgent = nil
cq.profile = profile
@@ -148,27 +149,36 @@ proc upgradeHandler(request: Request) =
{.cast(gcsafe).}:
let ws = request.upgradeToWebSocket()
cq.ws = ws
# Send client connection message
ws.sendEventlogItem(LOG_SUCCESS_SHORT, "CQ-V1")
proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.gcsafe.} =
{.cast(gcsafe).}:
case event:
of OpenEvent:
discard
# New client connected to team server
# Send profile, sessions and listeners to the UI client
ws.sendProfile(cq.profile)
for id, listener in cq.listeners:
ws.sendListener(listener)
for id, agent in cq.agents:
ws.sendAgent(agent)
ws.sendEventlogItem(LOG_SUCCESS_SHORT, "CQ-V1")
of MessageEvent:
# Continuously send heartbeat messages
ws.sendHeartbeat()
case message.getMessageType():
of CLIENT_AGENT_COMMAND:
discard
of CLIENT_LISTENER_START:
message.receiveStartListener()
of CLIENT_LISTENER_STOP:
message.receiveStopListener()
of CLIENT_AGENT_BUILD:
discard
else: discard
# case message.getMessageType():
# of CLIENT_AGENT_COMMAND:
# discard
# of CLIENT_LISTENER_START:
# message.receiveStartListener()
# of CLIENT_LISTENER_STOP:
# discard
# # message.receiveStopListener()
# of CLIENT_AGENT_BUILD:
# discard
# else: discard
of ErrorEvent:
discard
of CloseEvent:

40
src/server/event/recv.nim Normal file
View File

@@ -0,0 +1,40 @@
import mummy
import times, tables, json
import ./send
import ../globals
import ../core/[task, listener]
import ../../common/[types, utils, serialize, event]
#[
Client -> Server
]#
# proc getMessageType*(message: Message): EventType =
# var unpacker = Unpacker.init(message.data)
# return cast[EventType](unpacker.getUint8())
# proc receiveStartListener*(message: Message) =
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# let
# listenerId = Uuid.toString(unpacker.getUint32())
# address = unpacker.getDataWithLengthPrefix()
# port = int(unpacker.getUint16())
# protocol = cast[Protocol](unpacker.getUint8())
# cq.listenerStart(listenerId, address, port, protocol)
# proc receiveStopListener*(message: Message) =
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# let listenerId = Uuid.toString(unpacker.getUint32())
# cq.listenerStop(listenerId)
# proc receiveAgentCommand*(message: Message) =
# var unpacker = Unpacker.init(message.data)
# discard unpacker.getUint8()
# let
# agentId = Uuid.toString(unpacker.getUint32())
# command = unpacker.getDataWithLengthPrefix()

78
src/server/event/send.nim Normal file
View File

@@ -0,0 +1,78 @@
import mummy
import times, tables, json, base64, parsetoml
import ../utils
import ../../common/[types, utils, serialize, event]
export sendHeartbeat
#[
Server -> Client
]#
proc sendProfile*(ws: WebSocket, profile: Profile) =
let event = Event(
eventType: CLIENT_PROFILE,
timestamp: now().toTime().toUnix(),
data: %*{
"profile": profile.toTomlString()
}
)
ws.sendEvent(event)
proc sendEventlogItem*(ws: WebSocket, logType: LogType, message: string) =
let event = Event(
eventType: CLIENT_EVENTLOG_ITEM,
timestamp: now().toTime().toUnix(),
data: %*{
"logType": cast[uint8](logType),
"message": message
}
)
ws.sendEvent(event)
proc sendAgent*(ws: WebSocket, agent: Agent) =
let event = Event(
eventType: CLIENT_AGENT_ADD,
timestamp: now().toTime().toUnix(),
data: %agent
)
ws.sendEvent(event)
proc sendListener*(ws: WebSocket, listener: Listener) =
let event = Event(
eventType: CLIENT_LISTENER_ADD,
timestamp: now().toTime().toUnix(),
data: %listener
)
ws.sendEvent(event)
proc sendAgentCheckin*(ws: WebSocket, agentId: string) =
let event = Event(
eventType: CLIENT_AGENT_CHECKIN,
timestamp: now().toTime().toUnix(),
data: %*{
"agentId": agentId
}
)
ws.sendEvent(event)
proc sendAgentPayload*(ws: WebSocket, agentId: string, bytes: seq[byte]) =
let event = Event(
eventType: CLIENT_AGENT_PAYLOAD,
timestamp: now().toTime().toUnix(),
data: %*{
"agentId": agentId,
"payload": encode(bytes)
}
)
ws.sendEvent(event)
proc sendConsoleItem*(ws: WebSocket, agentId: string, logType: LogType, message: string) =
let event = Event(
eventType: CLIENT_CONSOLE_ITEM,
timestamp: now().toTime().toUnix(),
data: %*{
"agentId": agentId,
"logType": cast[uint8](logType),
"message": message
}
)
ws.sendEvent(event)

View File

@@ -1,15 +1,31 @@
import strutils, terminal, tables, sequtils, times, strformat, prompt
import strutils, terminal, tables, sequtils, times, strformat, prompt, json
import std/wordwrap
import ../common/types
import core/logger
proc validatePort*(portStr: string): bool =
try:
let port: int = portStr.parseInt
return port >= 1 and port <= 65535
except ValueError:
return false
proc `%`*(agent: Agent): JsonNode =
result = newJObject()
result["agentId"] = %agent.agentId
result["listenerId"] = %agent.listenerId
result["username"] = %agent.username
result["hostname"] = %agent.hostname
result["domain"] = %agent.domain
result["ip"] = %agent.ip
result["os"] = %agent.os
result["process"] = %agent.process
result["pid"] = %agent.pid
result["elevated"] = %agent.elevated
result["sleep"] = %agent.sleep
result["firstCheckin"] = %agent.firstCheckin.toTime().toUnix()
result["latestCheckin"] = %agent.latestCheckin.toTime().toUnix()
proc `%`*(listener: Listener): JsonNode =
result = newJObject()
result["listenerId"] = %listener.listenerId
result["address"] = %listener.address
result["port"] = %listener.port
result["protocol"] = %listener.protocol
# Table border characters
type

View File

@@ -1,42 +0,0 @@
import times, tables
import ../globals
import ../../common/[types, utils, serialize]
import mummy
import ./send
import ../core/[task, listener]
#[
[ Retrieval functions ]
Client -> Server
]#
proc getMessageType*(message: Message): WsPacketType =
var unpacker = Unpacker.init(message.data)
return cast[WsPacketType](unpacker.getUint8())
proc receiveStartListener*(message: Message) =
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
let
listenerId = Uuid.toString(unpacker.getUint32())
address = unpacker.getDataWithLengthPrefix()
port = int(unpacker.getUint16())
protocol = cast[Protocol](unpacker.getUint8())
cq.ws.sendEventlogItem(LOG_INFO_SHORT, "Attempting to start listener.")
cq.listenerStart(listenerId, address, port, protocol)
proc receiveStopListener*(message: Message) =
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
let listenerId = Uuid.toString(unpacker.getUint32())
cq.listenerStop(listenerId)
proc receiveAgentCommand*(message: Message) =
var unpacker = Unpacker.init(message.data)
discard unpacker.getUint8()
let
agentId = Uuid.toString(unpacker.getUint32())
command = unpacker.getDataWithLengthPrefix()

View File

@@ -1,79 +0,0 @@
import times, tables
import ../../common/[types, utils, serialize]
import mummy
#[
[ Sending functions ]
Server -> Client
]#
proc sendHeartbeat*(ws: WebSocket) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_HEARTBEAT))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendEventlogItem*(ws: WebSocket, logType: LogType, message: string, timestamp: int64 = now().toTime().toUnix()) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_EVENT_LOG))
packer.add(cast[uint8](logType))
packer.add(cast[uint32](timestamp))
packer.addDataWithLengthPrefix(string.toBytes(message))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendConsoleItem*(ws: WebSocket, agentId: string, logType: LogType, message: string, timestamp: int64 = now().toTime().toUnix()) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_CONSOLE_LOG))
packer.add(string.toUUid(agentId))
packer.add(cast[uint8](logType))
packer.add(cast[uint32](timestamp))
packer.addDataWithLengthPrefix(string.toBytes(message))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendAgentCheckin*(ws: WebSocket, agentId: string, timestamp: int64) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_AGENT_CHECKIN))
packer.add(string.toUUid(agentId))
packer.add(cast[uint32](timestamp))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendAgentPayload*(ws: WebSocket, payload: seq[byte]) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_AGENT_BINARY))
packer.addDataWithLengthPrefix(payload)
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
proc sendAgentConnection*(ws: WebSocket, agent: Agent) =
var packer = Packer.init()
packer.add(cast[uint8](CLIENT_AGENT_CONNECTION))
packer.add(string.toUuid(agent.agentId))
packer.add(string.toUuid(agent.listenerId))
packer.addDataWithLengthPrefix(string.toBytes(agent.username))
packer.addDataWithLengthPrefix(string.toBytes(agent.hostname))
packer.addDataWithLengthPrefix(string.toBytes(agent.domain))
packer.addDataWithLengthPrefix(string.toBytes(agent.ip))
packer.addDataWithLengthPrefix(string.toBytes(agent.os))
packer.addDataWithLengthPrefix(string.toBytes(agent.process))
packer.add(uint32(agent.pid))
packer.add(uint8(agent.elevated))
packer.add(uint32(agent.sleep))
packer.add(cast[uint32](agent.firstCheckin))
packer.add(cast[uint32](agent.latestCheckin))
let data = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)