-
Notifications
You must be signed in to change notification settings - Fork 1
Client Teamserver Protocol
Transport: JSON sent as binary WebSocket messages over WSS to wss://<host>:<port>/havoc/.
Server side: teamserver/pkg/packager/ + teamserver/cmd/server/dispatch.go.
Client side: client/src/Havoc/Packager.cc.
{
"Head": { "Event": 7, "User": "Neo", "Time": "12:00:00", "OneTime": "" },
"Body": { "SubEvent": 3, "Info": { "key": "value" } }
}-
Head.Event/Body.SubEvent: integers selecting the handler. -
Head.OneTime = "true": event is not stored in the server's event log (not replayed to new clients). -
Body.Info: a string→string map (all values are stringified on the client).
| Namespace | Type | SubEvents |
|---|---|---|
InitConnection |
0x1 |
Success=0x1, Error=0x2, OAuthRequest/Login=0x3, InitInfo=0x4 (server), Profile=0x5 (server) |
Listener |
0x2 |
Add=0x1, Edit=0x2, Remove=0x3, Mark=0x4, Error=0x5
|
Credentials |
0x3 |
Add=0x1, Edit=0x2, Remove=0x3
|
Chat |
0x4 |
NewMessage=0x1, NewListener=0x2, NewSession=0x3, NewUser=0x4, UserDisconnected=0x5
|
Gate |
0x5 |
Staged=0x1, Stageless=0x2, MSOffice=0x3
|
Module |
0x6 |
server-side only (pkg/packager/types.go:181-186): Add=0x1, Remove=0x2; not dispatched; no client-side namespace exists |
Session |
0x7 |
NewSession=0x1, Remove=0x2, Input/SendCommand=0x3, Output/ReceiveCommand=0x4, MarkAsDead/MarkAs=0x5
|
Service |
0x9 |
RegisterAgent=0x1, RegisterListener=0x2
|
Teamserver |
0x10 |
Log/Logger=0x1, Profile=0x2
|
Loot |
0x11 |
GetFile=0x1 (client→server), SendFile=0x2, Error=0x3 (server→client, OneTime) |
(The overlapping 0x6/0x7 values are server-side names vs client-side names for the same numbers.)
Note:
Credentials(0x3),Module/HostFile(0x6) andMisc(0x7: aMiscTypestruct exists atpkg/packager/types.go:4-7but is never instantiated and noMessageBoxconstant exists) are not handled by the server-sideDispatchEventswitch in this version. They are effectively dead or client-side-only constants. The server dispatchesSession,Chat,Listener,GateandLoot(0x11,GetFile).
-
Listener.Mark = 0x4: sender exists (pkg/events/listeners.go:187) but nothing on the teamserver calls it. -
InitConnection.InitInfo = 0x4: declared (pkg/packager/types.go:129,client/include/Havoc/Packager.hpp:28), never sent or dispatched on either side. -
Gate.Staged = 0x1andGate.MSOffice = 0x3:dispatch.gohandles onlyGate.Stageless, andpkg/events/gate.goonly emits Stageless. -
Teamserver.Profile = 0x2: theevents.Teamserver.Profilehelper historically emitted the wrong SubEvent (Teamserver.LogwithInfo["profile"]); that copy-paste bug is fixed and the helper now sends SubEvent0x2correctly, but it still has no callers: the login flow continues to send the profile asTeamserver.Log (0x1)withInfo["profile"](client parses it there too), so treat0x2as reserved-but-unused.
These behaviors are undocumented in the official wiki but are load-bearing for clients:
-
Login: the first packet must be
InitConnection/OAuthRequest(0x1/0x3) withHead.User+Body.Info["Password"](hex SHA3-256). The server compares against the server-side-hashed operator password; anything but SubEvent0x3on the first packet is rejected. -
Throttling: failed logins are rate-limited per source IP (
LoginThrottled/LoginFailure/LoginSuccess,teamserver.go:63-140). -
Pre-auth timeout: the first (login) read is bounded by
UnauthenticatedClientTimeout(30s,teamserver.go:38; deadline set atteamserver.go:748); the deadline is cleared after successful auth. -
Replay order: on success the server replies
InitConnection/Successviaevents.Authenticated(true)(keyMessage), then sends the profile event (Teamserver/LogwithInfo["profile"], containingTeamserverIPs+Demon), broadcastsChat.NewUser (0x4), then replaysEventsList(teamserver.go:907). All other SubEvents onInitConnectionare rejected withInitConnection/Error. -
Event logging: client-sent packages are appended to
EventsListbefore dispatch and theirHead.Timeis overwritten server-side with format02/01/2006 15:04:05(dd/mm/yyyy hh:mm:ss). Non-OneTime events only. -
Disconnects: an abnormal WebSocket closure or read error appends/broadcasts
Chat.UserDisconnected (0x5)and removes the client (teamserver.go:915-934); the same broadcast is emitted if a client handler panics (the socket is closed to prevent ghost operators,teamserver.go:711). -
Server-initiated session state: when an agent times out, the teamserver marks it dead and emits
Session/MarkAs (0x7/0x5)withMarked = "Dead"to all clients (pkg/events/demons.go:127,cmd/server/agent.go:26). This is the same SubEvent the client sends to manually mark sessions (client → server, InfoAgentID/Marked).
Login (client → server): Head.User (checked against the profile's operator names, teamserver.go:771,1009) and Info["Password"] (hex SHA3-256, teamserver.go:1022). Info["User"] is neither required nor checked; the login username is derived from Head.User. Post-auth (both directions): the server overwrites Head.User on every client→server package with the authenticated username, so operator impersonation via a spoofed Head.User is not possible; third-party clients should still send their username, but the server-side value is authoritative for attribution, echo exclusion and audit.
InitConnection/Success (server → client): the auth reply is actually events.Authenticated(true) with a single key Message: "Successful Authenticated" (pkg/events/events.go:22-34, sent at teamserver.go:899). The TeamserverIPs + Demon keys belong to InitConnection/Profile (0x5) (events.SendProfile, pkg/events/events.go:88, appended at teamserver.go:701). The Demon value is the Demon config JSON used by the payload dialog. Auth failures are InitConnection/Error with Message.
Listener Add/Edit: Name, Protocol (HTTPS/HTTP/SMB/External), Status, HostBind, HostRotation, PortBind, PortConn, UserAgent, Headers, Uris, Hosts, HostHeader, Secure, Proxy Enabled/Type/Host/Port/Username/Password, PipeName (SMB), Endpoint (External). For HTTP listeners the Add event additionally embeds the full listener config map (e.g. Methode, KillDate, WorkingHours, Cert, BehindRedir); Edit also carries Response Headers (pkg/events/listeners.go:110). Remove: Name. Error: Error, Name. Listener/Error (0x2/0x5) is server→client only; when it fires, the teamserver also rewrites/removes the corresponding earlier Listener.Add entries in EventsList so replayed state stays consistent (teamserver.go:1061-1092).
Gate (payload build): outbound AgentType, Listener, Arch, Format, Config; inbound PayloadArray (base64), FileName, Format (same value as FileName), or progress MessageType/Message.
Session NewSession: NameID, MagicValue, ExternalIP, InternalIP, Listener, Username, Hostname, DomainName, OSVersion, OSBuild, OSArch, ProcessName, ProcessPID, ProcessArch, ProcessPath, ProcessPPID, FirstCallIn, LastCallIn (UTC wall-clock time: the teamserver writes time.Now().UTC(); parse with the UTC timespec, not client-local), Elevated, PivotParent, Pivots (nested Parent/Links), PortFwds, SocksCli/SocksCliMtx/SocksSvr, Encryption (nested base64 AESKey/AESIv), BackgroundCheck, TaskedOnce, Reason, Active, SleepDelay, SleepJitter, KillDate, WorkingHours (all from events.Demons.NewDemon, pkg/events/demons.go:22-77).
Session SendCommand (client → server): every task carries TaskID, CommandLine, DemonID, plus Command (console command name) and a numeric CommandID string (from the client's Commands:: enum). Two special CommandID literals: "Teamserver" for server-side tasks (e.g. CommandExecute::Task, CommandSend.cc:457) and "Python Plugin" for Python-registered commands, which also carry a TaskMessage key (ConsoleInput.cc:2540-2546). Per-command keys (client/src/Havoc/Demon/CommandSend.cc):
| Command family | Additional Info keys |
|---|---|
| FS (dir/download/upload/cd/…) |
SubCommand, Arguments
|
| ProcList |
FromProcessManager ("true"/"false") |
| InlineExecute (BOF) |
HasCallback ("true"/"false", selects BOF output routing), FunctionName, Binary (base64), Arguments (base64), Flags ("threaded"/"non-threaded") |
| Dotnet InlineExecute |
Binary (base64), Arguments (plain, not base64) |
| Shellcode (inject/spawn/execute) |
Way ("Inject"/"Spawn"/"Execute"), Technique, Binary (base64), Arguments (base64), PID (inject only), Arch
|
| DllSpawn |
Binary (base64), Arguments (base64) |
| DllInject |
Binary, Arguments (plain), PID
|
| Token |
SubCommand, Arguments
|
| Proc |
ProcCommand, Args
|
| Exit | ExitMethod |
| Config |
ConfigKey, ConfigVal
|
| Net |
NetCommand, Param
|
| Pivot |
Command, Param
|
| Job |
Command, Param
|
| Transfer (upload/download mgmt) |
Command, FileID
|
| Socket (socks/rportfwd) |
Command, Params (plural, unlike most others) |
| Kerberos |
Command (luid/klist/purge/ptt) plus Argument1+Argument2 (klist), Argument (purge), Ticket+Luid (ptt) |
For third-party agents the client sends TaskID, CommandLine="", DemonID, Command, CommandArg (base64); CommandLine is sent as an empty string, not omitted.
Session ReceiveCommand (server → client): DemonID, CommandID (numeric command id; note the key is CommandID, not Command, on this direction), Output (base64), from events.Demons.DemonOutput/CallBack (pkg/events/demons.go:107-131); TaskID/AgentType appear only on the client→server Input path. Output is base64 only for task results; for Misc packages the Info carries MiscType/MiscData/MiscData2 string keys instead (values include FileExplorer, download, ProcessUI, screenshot, see teamserver/pkg/agent/demons.go). Command ids seen by the client (enum class Commands in client/include/Havoc/DemonCmdDispatch.h): CALLBACK=10, SLEEP=11, FS=15, INLINE_EXECUTE=20, INJECT_DLL=22, INJECT_SHELLCODE=24, INJECT_DLL_SPAWN=26, TOKEN=40, OUTPUT=90, ERROR=91, EXIT=92, CHECKIN=100, CONSOLE_MESSAGE=0x80, BOF_CALLBACK=0x81, PROC=0x1010, INLINE_EXECUTE_ASSEMBLY=0x2001, NET=2100, CONFIG=2500, SCREENSHOT=2510, PIVOT=2520, TRANSFER=2530, SOCKET=2540, KERBEROS=2550.
Session MarkAs: AgentID, Marked (Alive/Dead).
Service RegisterAgent (server → client): Agent = JSON string describing a third-party agent type (Name, Description, Version, MagicValue, Arch[], Formats[]{Name,Extension}, SupportedOS[], Commands[]{Name,Description,Help,NeedAdmin,Mitr[],Params[]{Name,IsFilePath,IsOptional},Anonymous}, BuildingConfig). RegisterListener: Listener = JSON for custom listener UIs.
Teamserver Logger: Text.
Loot GetFile (client → server): AgentID, FileName. Requests the content of a previously looted (downloaded) file. The teamserver walks data/loot/<ts>/agents/<AgentID>/Download/ (downloads preserve the remote directory layout) for the newest file with that base name. Loot SendFile (server → client): AgentID, FileName, Content (base64). Loot Error: Error. Both responses are OneTime and go only to the requesting client. Used by the Loot widget's "Get file" action. Guards: 64 MiB max file size; AgentID is path-traversal checked (dispatch.go:830-916).
The teamserver keeps every non-OneTime event in EventsList. When a client authenticates, the whole list is replayed (SendAllPackagesToNewClient), so the client UI is purely a function of the event stream. There is no separate "get state" RPC.