Skip to content

Client Teamserver Protocol

laptop tester edited this page Sep 6, 2026 · 2 revisions

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.

Envelope

{
  "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).

Event / SubEvent constants

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) and Misc (0x7: a MiscType struct exists at pkg/packager/types.go:4-7 but is never instantiated and no MessageBox constant exists) are not handled by the server-side DispatchEvent switch in this version. They are effectively dead or client-side-only constants. The server dispatches Session, Chat, Listener, Gate and Loot (0x11, GetFile).

Dead / unused constants (declared on both sides, never used)

  • 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 = 0x1 and Gate.MSOffice = 0x3: dispatch.go handles only Gate.Stageless, and pkg/events/gate.go only emits Stageless.
  • Teamserver.Profile = 0x2: the events.Teamserver.Profile helper historically emitted the wrong SubEvent (Teamserver.Log with Info["profile"]); that copy-paste bug is fixed and the helper now sends SubEvent 0x2 correctly, but it still has no callers: the login flow continues to send the profile as Teamserver.Log (0x1) with Info["profile"] (client parses it there too), so treat 0x2 as reserved-but-unused.

Connection lifecycle (server-side behavior, teamserver/cmd/server/teamserver.go)

These behaviors are undocumented in the official wiki but are load-bearing for clients:

  1. Login: the first packet must be InitConnection/OAuthRequest (0x1/0x3) with Head.User + Body.Info["Password"] (hex SHA3-256). The server compares against the server-side-hashed operator password; anything but SubEvent 0x3 on the first packet is rejected.
  2. Throttling: failed logins are rate-limited per source IP (LoginThrottled / LoginFailure / LoginSuccess, teamserver.go:63-140).
  3. Pre-auth timeout: the first (login) read is bounded by UnauthenticatedClientTimeout (30s, teamserver.go:38; deadline set at teamserver.go:748); the deadline is cleared after successful auth.
  4. Replay order: on success the server replies InitConnection/Success via events.Authenticated(true) (key Message), then sends the profile event (Teamserver/Log with Info["profile"], containing TeamserverIPs + Demon), broadcasts Chat.NewUser (0x4), then replays EventsList (teamserver.go:907). All other SubEvents on InitConnection are rejected with InitConnection/Error.
  5. Event logging: client-sent packages are appended to EventsList before dispatch and their Head.Time is overwritten server-side with format 02/01/2006 15:04:05 (dd/mm/yyyy hh:mm:ss). Non-OneTime events only.
  6. 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).
  7. Server-initiated session state: when an agent times out, the teamserver marks it dead and emits Session/MarkAs (0x7/0x5) with Marked = "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, Info AgentID/Marked).

Key Info fields

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).

State reconstruction

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.

Clone this wiki locally