Implemented websocket (client <-> server) traffic encryption & compression.

This commit is contained in:
Jakob Friedl
2025-10-01 21:57:26 +02:00
parent 0937840b77
commit fbe85493b2
11 changed files with 160 additions and 206 deletions

View File

@@ -2,7 +2,7 @@ import whisky
import tables, strutils, strformat, json, parsetoml, base64, os # native_dialogs
import ./utils/[appImGui, globals]
import ./views/[dockspace, sessions, listeners, eventlog, console]
import ../common/[types, utils]
import ../common/[types, utils, crypto]
import ./websocket
import sugar
@@ -39,9 +39,15 @@ proc main(ip: string = "localhost", port: int = 37573) =
let io = igGetIO()
# Create key pair
let clientKeyPair = generateKeyPair()
# Initiate WebSocket connection
let ws = newWebSocket(fmt"ws://{ip}:{$port}")
defer: ws.close()
var connection = WsConnection(
ws: newWebSocket(fmt"ws://{ip}:{$port}"),
sessionKey: default(Key)
)
defer: connection.ws.close()
# main loop
while not app.handle.windowShouldClose:
@@ -59,13 +65,17 @@ proc main(ip: string = "localhost", port: int = 37573) =
WebSocket communication with the team server
]#
# Continuously send heartbeat messages
ws.sendHeartbeat()
connection.ws.sendHeartbeat()
# Receive and parse websocket response message
let event = recvEvent(ws.receiveMessage().get())
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey)
case event.eventType:
of CLIENT_KEY_EXCHANGE:
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
connection.sendPublicKey(clientKeyPair.publicKey)
of CLIENT_PROFILE:
profile = parsetoml.parseString(event.data["profile"].getStr())
profile = parsetoml.parseString(event.data["profile"].getStr())
of CLIENT_LISTENER_ADD:
let listener = event.data.to(UIListener)
@@ -90,7 +100,7 @@ proc main(ip: string = "localhost", port: int = 37573) =
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
else:
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
consoles[agent.agentId].draw(ws)
consoles[agent.agentId].draw(connection)
consoles[agent.agentId].showConsole = false
of CLIENT_AGENT_CHECKIN:
@@ -127,7 +137,7 @@ proc main(ip: string = "localhost", port: int = 37573) =
# Draw/update UI components/views
if showSessionsTable: sessionsTable.draw(addr showSessionsTable)
if showListeners: listenersTable.draw(addr showListeners, ws)
if showListeners: listenersTable.draw(addr showListeners, connection)
if showEventlog: eventlog.draw(addr showEventlog)
# Show console windows
@@ -136,7 +146,7 @@ proc main(ip: string = "localhost", port: int = 37573) =
if console.showConsole:
# Ensure that new console windows are docked to the bottom panel by default
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
console.draw(ws)
console.draw(connection)
newConsoleTable[agentId] = console
# Update the consoles table with only those sessions that have not been closed yet

View File

@@ -170,7 +170,7 @@ proc handleHelp(component: ConsoleComponent, parsed: seq[string]) =
# Command was not found
component.addItem(LOG_ERROR, fmt"The command '{parsed[1]}' does not exist.")
proc handleAgentCommand*(component: ConsoleComponent, ws: WebSocket, input: string) =
proc handleAgentCommand*(component: ConsoleComponent, connection: WsConnection, input: string) =
# Convert user input into sequence of string arguments
let parsedArgs = parseInput(input)
@@ -186,7 +186,7 @@ proc handleAgentCommand*(component: ConsoleComponent, ws: WebSocket, input: stri
command = getCommandByName(parsedArgs[0])
task = createTask(component.agent.agentId, component.agent.listenerId, command, parsedArgs[1..^1])
ws.sendAgentTask(component.agent.agentId, task)
connection.sendAgentTask(component.agent.agentId, task)
component.addItem(LOG_INFO, fmt"Tasked agent to {command.description.toLowerAscii()} ({Uuid.toString(task.taskId)})")
except CatchableError:
@@ -219,7 +219,7 @@ proc print(item: ConsoleItem) =
igSameLine(0.0f, 0.0f)
igTextUnformatted(item.text.cstring, nil)
proc draw*(component: ConsoleComponent, ws: WebSocket) =
proc draw*(component: ConsoleComponent, connection: WsConnection) =
igBegin(fmt"[{component.agent.agentId}] {component.agent.username}@{component.agent.hostname}".cstring, addr component.showConsole, 0)
defer: igEnd()
@@ -340,7 +340,7 @@ proc draw*(component: ConsoleComponent, ws: WebSocket) =
component.addItem(LOG_COMMAND, command)
# Send command to team server
component.handleAgentCommand(ws, command)
component.handleAgentCommand(connection, command)
# Add command to console history
component.history.add(command)

View File

@@ -22,7 +22,7 @@ proc ListenersTable*(title: string): ListenersTableComponent =
result.startListenerModal = ListenerModal()
result.generatePayloadModal = AgentModal()
proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebSocket) =
proc draw*(component: ListenersTableComponent, showComponent: ptr bool, connection: WsConnection) =
igBegin(component.title, showComponent, 0)
defer: igEnd()
@@ -41,11 +41,11 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebS
let listener = component.startListenerModal.draw()
if listener != nil:
ws.sendStartListener(listener)
connection.sendStartListener(listener)
let buildInformation = component.generatePayloadModal.draw(component.listeners)
if buildInformation != nil:
ws.sendAgentBuild(buildInformation)
connection.sendAgentBuild(buildInformation)
#[
Listener table
@@ -106,7 +106,7 @@ proc draw*(component: ListenersTableComponent, showComponent: ptr bool, ws: WebS
if not ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
newListeners.add(listener)
else:
ws.sendStopListener(listener.listenerId)
connection.sendStopListener(listener.listenerId)
component.listeners = newListeners
ImGuiSelectionBasicStorage_Clear(component.selection)

View File

@@ -1,20 +1,30 @@
import whisky
import times, tables, json
import times, tables, json, base64
import ../common/[types, utils, serialize, event]
export sendHeartbeat, recvEvent
#[
Client -> Server
]#
proc sendStartListener*(ws: WebSocket, listener: UIListener) =
proc sendPublicKey*(connection: WsConnection, publicKey: Key) =
let event = Event(
eventType: CLIENT_KEY_EXCHANGE,
timestamp: now().toTime().toUnix(),
data: %*{
"publicKey": encode(Bytes.toString(publicKey))
}
)
connection.ws.sendEvent(event, connection.sessionKey)
proc sendStartListener*(connection: WsConnection, listener: UIListener) =
let event = Event(
eventType: CLIENT_LISTENER_START,
timestamp: now().toTime().toUnix(),
data: %listener
)
ws.sendEvent(event)
connection.ws.sendEvent(event, connection.sessionKey)
proc sendStopListener*(ws: WebSocket, listenerId: string) =
proc sendStopListener*(connection: WsConnection, listenerId: string) =
let event = Event(
eventType: CLIENT_LISTENER_STOP,
timestamp: now().toTime().toUnix(),
@@ -22,9 +32,9 @@ proc sendStopListener*(ws: WebSocket, listenerId: string) =
"listenerId": listenerId
}
)
ws.sendEvent(event)
connection.ws.sendEvent(event, connection.sessionKey)
proc sendAgentBuild*(ws: WebSocket, buildInformation: AgentBuildInformation) =
proc sendAgentBuild*(connection: WsConnection, buildInformation: AgentBuildInformation) =
let event = Event(
eventType: CLIENT_AGENT_BUILD,
timestamp: now().toTime().toUnix(),
@@ -36,9 +46,9 @@ proc sendAgentBuild*(ws: WebSocket, buildInformation: AgentBuildInformation) =
"modules": buildInformation.modules
}
)
ws.sendEvent(event)
connection.ws.sendEvent(event, connection.sessionKey)
proc sendAgentTask*(ws: WebSocket, agentId: string, task: Task) =
proc sendAgentTask*(connection: WsConnection, agentId: string, task: Task) =
let event = Event(
eventType: CLIENT_AGENT_TASK,
timestamp: now().toTime().toUnix(),
@@ -47,4 +57,4 @@ proc sendAgentTask*(ws: WebSocket, agentId: string, task: Task) =
"task": task
}
)
ws.sendEvent(event)
connection.ws.sendEvent(event, connection.sessionKey)

View File

@@ -3,26 +3,66 @@ when defined(server):
when defined(client):
import whisky
import times, json
import ./[types, utils, serialize]
import times, json, zippy
import ./[types, utils, serialize, crypto]
proc sendEvent*(ws: WebSocket, event: Event) =
proc sendEvent*(ws: WebSocket, event: Event, key: Key = default(Key)) =
var packer = Packer.init()
let iv = generateBytes(Iv)
var
data = string.toBytes($event.data)
packer.add(cast[uint8](event.eventType))
packer.add(cast[uint32](event.timestamp))
packer.addDataWithLengthPrefix(string.toBytes($event.data))
let data = packer.pack()
if event.eventType != CLIENT_KEY_EXCHANGE and event.eventType != CLIENT_HEARTBEAT:
# Compress data
let compressed = compress(data, BestCompression, dfGzip)
# Encrypt data
let (encData, gmac) = encrypt(key, iv, compressed)
packer.addData(iv) # 12 bytes IV
packer.addData(gmac) # 16 bytes Authentication Tag
packer.addDataWithLengthPrefix(encData)
else:
packer.addDataWithLengthPrefix(data)
let body = packer.pack()
ws.send(Bytes.toString(data), BinaryMessage)
ws.send(Bytes.toString(body), BinaryMessage)
proc recvEvent*(message: Message, key: Key = default(Key)): Event =
proc recvEvent*(message: Message): Event =
var unpacker = Unpacker.init(message.data)
let
eventType = cast[EventType](unpacker.getUint8())
timestamp = cast[int64](unpacker.getUint32())
var data: string
if eventType != CLIENT_KEY_EXCHANGE and eventType != CLIENT_HEARTBEAT:
let
iv = unpacker.getByteArray(Iv)
gmac = unpacker.getByteArray(AuthenticationTag)
encData = string.toBytes(unpacker.getDataWithLengthPrefix())
# Decrypt data
let (decData, tag) = decrypt(key, iv, encData)
if tag != gmac:
raise newException(CatchableError, "Invalid authentication tag.")
# Decompress data
data = Bytes.toString(uncompress(decData, dfGzip))
else:
data = unpacker.getDataWithLengthPrefix()
return Event(
eventType: cast[EventType](unpacker.getUint8()),
timestamp: cast[int64](unpacker.getUint32()),
data: parseJson(unpacker.getDataWithLengthPrefix())
eventType: eventType,
timestamp: timestamp,
data: parseJson(data)
)
proc sendHeartbeat*(ws: WebSocket) =

View File

@@ -93,7 +93,6 @@ proc getUint64*(unpacker: Unpacker): uint64 =
unpacker.position += 8
proc getBytes*(unpacker: Unpacker, length: int): seq[byte] =
if length <= 0:
return @[]

View File

@@ -1,8 +1,10 @@
import tables
import times
import parsetoml, json
import mummy
import system
import mummy
when defined(client):
import whisky
# Custom Binary Task structure
const
@@ -242,10 +244,11 @@ type
type
EventType* = enum
CLIENT_HEARTBEAT = 0'u8 # Basic checkin
CLIENT_KEY_EXCHANGE = 200'u8
# Sent by client
CLIENT_AGENT_BUILD = 1'u8 # Generate an agent binary for a specific listener
CLIENT_AGENT_TASK = 2'u8 # Instruct TS to send queue a command for a specific agent
CLIENT_AGENT_TASK = 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
@@ -255,8 +258,8 @@ type
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
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
@@ -271,8 +274,12 @@ type
Profile* = TomlValueRef
UIClient* = ref object
ws*: WebSocket
WsConnection* = ref object
when defined(server):
ws*: mummy.WebSocket
when defined(client):
ws*: whisky.WebSocket
sessionKey*: Key
Conquest* = ref object
dbPath*: string
@@ -281,7 +288,7 @@ type
agents*: Table[string, Agent]
keyPair*: KeyPair
profile*: Profile
client*: UIClient
client*: WsConnection
AgentCtx* = ref object
agentId*: string

View File

@@ -5,7 +5,7 @@ import parsetoml
import ../api/routes
import ../db/database
import ../core/logger
import ../../common/[types, utils, profile]
import ../../common/[types, profile]
import ../websocket
proc serve(listener: Listener) {.thread.} =

View File

@@ -1,132 +0,0 @@
import prompt, terminal, argparse, parsetoml, times, json, math
import strutils, strformat, system, tables
import ./[agent, listener, builder]
import ../globals
import ../db/database
import ../core/logger
import ../../common/[types, crypto, utils, profile, event]
import ../websocket
import mummy, mummy/routers
proc header() =
echo ""
echo "┏┏┓┏┓┏┓┓┏┏┓┏╋"
echo "┗┗┛┛┗┗┫┗┻┗ ┛┗ V0.1"
echo " ┗ @jakobfriedl"
echo "".repeat(21)
echo ""
proc init*(T: type Conquest, profile: Profile): Conquest =
var cq = new Conquest
cq.listeners = initTable[string, Listener]()
cq.threads = initTable[string, Thread[Listener]]()
cq.agents = initTable[string, Agent]()
cq.interactAgent = nil
cq.profile = profile
cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & profile.getString("private-key-file"))
cq.dbPath = CONQUEST_ROOT & "/" & profile.getString("database-file")
cq.client = nil
return cq
#[
WebSocket
]#
proc upgradeHandler(request: Request) =
{.cast(gcsafe).}:
let ws = request.upgradeToWebSocket()
cq.client = UIClient(
ws: ws
)
proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.gcsafe.} =
{.cast(gcsafe).}:
case event:
of OpenEvent:
# New client connected to team server
# Send profile, sessions and listeners to the UI client
cq.client.sendProfile(cq.profile)
for id, listener in cq.listeners:
cq.client.sendListener(listener)
for id, agent in cq.agents:
cq.client.sendAgent(agent)
cq.client.sendEventlogItem(LOG_SUCCESS_SHORT, "CQ-V1")
of MessageEvent:
# Continuously send heartbeat messages
ws.sendHeartbeat()
let event = message.recvEvent()
case event.eventType:
of CLIENT_AGENT_TASK:
let agentId = event.data["agentId"].getStr()
let task = event.data["task"].to(Task)
cq.agents[agentId].tasks.add(task)
of CLIENT_LISTENER_START:
let listener = event.data.to(UIListener)
cq.listenerStart(listener.listenerId, listener.address, listener.port, listener.protocol)
of CLIENT_LISTENER_STOP:
let listenerId = event.data["listenerId"].getStr()
cq.listenerStop(listenerId)
of CLIENT_AGENT_BUILD:
let
listenerId = event.data["listenerId"].getStr()
sleepDelay = event.data["sleepDelay"].getInt()
sleepTechnique = cast[SleepObfuscationTechnique](event.data["sleepTechnique"].getInt())
spoofStack = event.data["spoofStack"].getBool()
modules = cast[uint32](event.data["modules"].getInt())
let payload = cq.agentBuild(listenerId, sleepDelay, sleepTechnique, spoofStack, modules)
if payload.len() != 0:
cq.client.sendAgentPayload(payload)
else: discard
of ErrorEvent:
discard
of CloseEvent:
# Set the client instance to nil again to prevent debug error messages
cq.client = nil
proc startServer*(profilePath: string) =
# Ensure that the conquest root directory was passed as a compile-time define
when not defined(CONQUEST_ROOT):
quit(0)
header()
try:
# Initialize framework context
# Load and parse profile
let profile = parsetoml.parseFile(profilePath)
cq = Conquest.init(profile)
cq.info("Using profile \"", profile.getString("name"), "\" (", profilePath ,").")
except CatchableError as err:
echo err.msg
quit(0)
# Initialize database
cq.dbInit()
for agent in cq.dbGetAllAgents():
cq.agents[agent.agentId] = agent
for listener in cq.dbGetAllListeners():
cq.listeners[listener.listenerId] = listener
# Restart existing listeners
for listenerId, listener in cq.listeners:
cq.listenerStart(listenerId, listener.address, listener.port, listener.protocol)
# Start websocket server
var router: Router
router.get("/*", upgradeHandler)
# Increased websocket message length in order to support dotnet assembly execution
let server = newServer(router, websocketHandler, maxMessageLen = 1024 * 1024 * 1024)
server.serve(Port(cq.profile.getInt("team-server.port")), "0.0.0.0")

View File

@@ -1,11 +1,11 @@
import terminal, parsetoml, json, math
import terminal, parsetoml, json, math, base64
import strutils, strformat, system, tables
import ./core/[listener, builder]
import ./globals
import ./db/database
import ./core/logger
import ../common/[types, crypto, profile, event]
import ../common/[types, crypto, utils, profile, event]
import ./websocket
import mummy, mummy/routers
@@ -25,7 +25,7 @@ proc init*(T: type Conquest, profile: Profile): Conquest =
cq.profile = profile
cq.keyPair = loadKeyPair(CONQUEST_ROOT & "/" & profile.getString("private-key-file"))
cq.dbPath = CONQUEST_ROOT & "/" & profile.getString("database-file")
cq.client = nil
cq.client = nil
return cq
#[
@@ -34,7 +34,7 @@ proc init*(T: type Conquest, profile: Profile): Conquest =
proc upgradeHandler(request: Request) =
{.cast(gcsafe).}:
let ws = request.upgradeToWebSocket()
cq.client = UIClient(
cq.client = WsConnection(
ws: ws
)
@@ -43,21 +43,31 @@ proc websocketHandler(ws: WebSocket, event: WebSocketEvent, message: Message) {.
case event:
of OpenEvent:
# New client connected to team server
# Send profile, sessions and listeners to the UI client
cq.client.sendProfile(cq.profile)
for id, listener in cq.listeners:
cq.client.sendListener(listener)
for id, agent in cq.agents:
cq.client.sendAgent(agent)
cq.client.sendEventlogItem(LOG_SUCCESS_SHORT, "CQ-V1")
# Send the public key for the key exchange, all other information with be transmitted when the key exchange is completed
cq.client.sendPublicKey(cq.keyPair.publicKey)
of MessageEvent:
# Continuously send heartbeat messages
ws.sendHeartbeat()
let event = message.recvEvent()
let event = message.recvEvent(cq.client.sessionKey)
case event.eventType:
of CLIENT_KEY_EXCHANGE:
let publicKey = decode(event.data["publicKey"].getStr()).toKey()
cq.client.sessionKey = deriveSessionKey(cq.keyPair, publicKey)
# Send relevant information to the client
# - C2 profile
# - agent sessions
# - listeners
cq.client.sendProfile(cq.profile)
for id, listener in cq.listeners:
cq.client.sendListener(listener)
for id, agent in cq.agents:
cq.client.sendAgent(agent)
cq.client.sendEventlogItem(LOG_SUCCESS_SHORT, "CQ-V1")
of CLIENT_AGENT_TASK:
let agentId = event.data["agentId"].getStr()
let task = event.data["task"].to(Task)

View File

@@ -1,6 +1,5 @@
import times, json, base64, parsetoml
import ../common/[types, event]
import ../common/[types, utils, event]
export sendHeartbeat, recvEvent
proc `%`*(agent: Agent): JsonNode =
@@ -29,7 +28,18 @@ proc `%`*(listener: Listener): JsonNode =
#[
Server -> Client
]#
proc sendProfile*(client: UIClient, profile: Profile) =
proc sendPublicKey*(client: WsConnection, publicKey: Key) =
let event = Event(
eventType: CLIENT_KEY_EXCHANGE,
timestamp: now().toTime().toUnix(),
data: %*{
"publicKey": encode(Bytes.toString(publicKey))
}
)
if client != nil:
client.ws.sendEvent(event, client.sessionKey)
proc sendProfile*(client: WsConnection, profile: Profile) =
let event = Event(
eventType: CLIENT_PROFILE,
timestamp: now().toTime().toUnix(),
@@ -38,9 +48,9 @@ proc sendProfile*(client: UIClient, profile: Profile) =
}
)
if client != nil:
client.ws.sendEvent(event)
client.ws.sendEvent(event, client.sessionKey)
proc sendEventlogItem*(client: UIClient, logType: LogType, message: string) =
proc sendEventlogItem*(client: WsConnection, logType: LogType, message: string) =
let event = Event(
eventType: CLIENT_EVENTLOG_ITEM,
timestamp: now().toTime().toUnix(),
@@ -50,27 +60,27 @@ proc sendEventlogItem*(client: UIClient, logType: LogType, message: string) =
}
)
if client != nil:
client.ws.sendEvent(event)
client.ws.sendEvent(event, client.sessionKey)
proc sendAgent*(client: UIClient, agent: Agent) =
proc sendAgent*(client: WsConnection, agent: Agent) =
let event = Event(
eventType: CLIENT_AGENT_ADD,
timestamp: now().toTime().toUnix(),
data: %agent
)
if client != nil:
client.ws.sendEvent(event)
client.ws.sendEvent(event, client.sessionKey)
proc sendListener*(client: UIClient, listener: Listener) =
proc sendListener*(client: WsConnection, listener: Listener) =
let event = Event(
eventType: CLIENT_LISTENER_ADD,
timestamp: now().toTime().toUnix(),
data: %listener
)
if client != nil:
client.ws.sendEvent(event)
client.ws.sendEvent(event, client.sessionKey)
proc sendAgentCheckin*(client: UIClient, agentId: string) =
proc sendAgentCheckin*(client: WsConnection, agentId: string) =
let event = Event(
eventType: CLIENT_AGENT_CHECKIN,
timestamp: now().toTime().toUnix(),
@@ -79,9 +89,9 @@ proc sendAgentCheckin*(client: UIClient, agentId: string) =
}
)
if client != nil:
client.ws.sendEvent(event)
client.ws.sendEvent(event, client.sessionKey)
proc sendAgentPayload*(client: UIClient, bytes: seq[byte]) =
proc sendAgentPayload*(client: WsConnection, bytes: seq[byte]) =
let event = Event(
eventType: CLIENT_AGENT_PAYLOAD,
timestamp: now().toTime().toUnix(),
@@ -90,9 +100,9 @@ proc sendAgentPayload*(client: UIClient, bytes: seq[byte]) =
}
)
if client != nil:
client.ws.sendEvent(event)
client.ws.sendEvent(event, client.sessionKey)
proc sendConsoleItem*(client: UIClient, agentId: string, logType: LogType, message: string) =
proc sendConsoleItem*(client: WsConnection, agentId: string, logType: LogType, message: string) =
let event = Event(
eventType: CLIENT_CONSOLE_ITEM,
timestamp: now().toTime().toUnix(),
@@ -103,4 +113,4 @@ proc sendConsoleItem*(client: UIClient, agentId: string, logType: LogType, messa
}
)
if client != nil:
client.ws.sendEvent(event)
client.ws.sendEvent(event, client.sessionKey)