diff --git a/src/client/config.nims b/src/client/config.nims index 9ecfe8e..3093b12 100644 --- a/src/client/config.nims +++ b/src/client/config.nims @@ -1,5 +1,6 @@ switch "o", "bin/client" +switch "d", "client" switch "d", "ImGuiTextSelect" # Select compiler diff --git a/src/client/event/recv.nim b/src/client/event/recv.nim new file mode 100644 index 0000000..53a2eff --- /dev/null +++ b/src/client/event/recv.nim @@ -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) \ No newline at end of file diff --git a/src/client/event/send.nim b/src/client/event/send.nim new file mode 100644 index 0000000..3d9ab34 --- /dev/null +++ b/src/client/event/send.nim @@ -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) + \ No newline at end of file diff --git a/src/client/layout.ini b/src/client/layout.ini deleted file mode 100644 index 9e4f854..0000000 --- a/src/client/layout.ini +++ /dev/null @@ -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 - diff --git a/src/client/main.nim b/src/client/main.nim index 5f5e49a..d5064b1 100644 --- a/src/client/main.nim +++ b/src/client/main.nim @@ -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) diff --git a/src/client/views/console.nim b/src/client/views/console.nim index d638521..4303f2b 100644 --- a/src/client/views/console.nim +++ b/src/client/views/console.nim @@ -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 diff --git a/src/client/views/listeners.nim b/src/client/views/listeners.nim index 7b76c41..7f3149a 100644 --- a/src/client/views/listeners.nim +++ b/src/client/views/listeners.nim @@ -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]) diff --git a/src/client/views/modals/generatePayload.nim b/src/client/views/modals/generatePayload.nim index ce21b9c..294e291 100644 --- a/src/client/views/modals/generatePayload.nim +++ b/src/client/views/modals/generatePayload.nim @@ -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 diff --git a/src/client/views/modals/startListener.nim b/src/client/views/modals/startListener.nim index b7d02b6..877b49e 100644 --- a/src/client/views/modals/startListener.nim +++ b/src/client/views/modals/startListener.nim @@ -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), diff --git a/src/client/views/sessions.nim b/src/client/views/sessions.nim index 7afb070..7afa458 100644 --- a/src/client/views/sessions.nim +++ b/src/client/views/sessions.nim @@ -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) @@ -104,7 +34,9 @@ proc interact(component: SessionsTableComponent) = # Focus the existing console window 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]) diff --git a/src/client/websocket.nim b/src/client/websocket.nim deleted file mode 100644 index 20c3411..0000000 --- a/src/client/websocket.nim +++ /dev/null @@ -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) \ No newline at end of file diff --git a/src/common/event.nim b/src/common/event.nim new file mode 100644 index 0000000..7b0a939 --- /dev/null +++ b/src/common/event.nim @@ -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) \ No newline at end of file diff --git a/src/common/types.nim b/src/common/types.nim index 1db12b9..11ba439 100644 --- a/src/common/types.nim +++ b/src/common/types.nim @@ -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_CONNECTION = 200'u8 # Return team server profile \ No newline at end of file + 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 + + diff --git a/src/server/api/handlers.nim b/src/server/api/handlers.nim index f7c4136..8213c7c 100644 --- a/src/server/api/handlers.nim +++ b/src/server/api/handlers.nim @@ -4,6 +4,7 @@ import ../globals import ../db/database import ../protocol/packer import ../core/logger +import ../event/send import ../../common/[types, utils, serialize] #[ @@ -36,6 +37,9 @@ proc register*(registrationData: seq[byte]): bool = 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") + + cq.ws.sendAgent(agent) + cq.ws.sendEventlogItem(LOG_INFO_SHORT, fmt"Agent {agent.agentId} connected to listener {agent.listenerId}.") return true @@ -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 diff --git a/src/server/core/builder.nim b/src/server/core/builder.nim index 210753c..477f361 100644 --- a/src/server/core/builder.nim +++ b/src/server/core/builder.nim @@ -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(): diff --git a/src/server/core/listener.nim b/src/server/core/listener.nim index bcf8340..a4d2751 100644 --- a/src/server/core/listener.nim +++ b/src/server/core/listener.nim @@ -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,9 +129,11 @@ 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: cq.error("Failed to restart listener: ", err.msg) diff --git a/src/server/core/server.nim b/src/server/core/server.nim index 48913dd..9cbee81 100644 --- a/src/server/core/server.nim +++ b/src/server/core/server.nim @@ -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: diff --git a/src/server/event/recv.nim b/src/server/event/recv.nim new file mode 100644 index 0000000..73c3b4f --- /dev/null +++ b/src/server/event/recv.nim @@ -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() + diff --git a/src/server/event/send.nim b/src/server/event/send.nim new file mode 100644 index 0000000..311802d --- /dev/null +++ b/src/server/event/send.nim @@ -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) diff --git a/src/server/utils.nim b/src/server/utils.nim index 18ebe5a..3dc4e6f 100644 --- a/src/server/utils.nim +++ b/src/server/utils.nim @@ -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 diff --git a/src/server/websocket/receive.nim b/src/server/websocket/receive.nim deleted file mode 100644 index 62e297d..0000000 --- a/src/server/websocket/receive.nim +++ /dev/null @@ -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() - diff --git a/src/server/websocket/send.nim b/src/server/websocket/send.nim deleted file mode 100644 index 6badffd..0000000 --- a/src/server/websocket/send.nim +++ /dev/null @@ -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) -