-
Notifications
You must be signed in to change notification settings - Fork 1
Service API Reference
Exact JSON message reference for the teamserver Service API (teamserver/pkg/service/). Enabled by the profile Service { Endpoint, Password } block. Transport: WebSocket at wss://<host>:<port>/<Endpoint> (route registered on the teamserver's TLS gin engine, so it is always WSS).
Terminology: the Service API client below is your third-party controller (not the operator client); "client → server" means controller → teamserver.
{ "Head": { "Type": "...", "RequestID": "..." }, "Body": { "Type": "...", "...": ... } }Constants (pkg/service/types.go):
Head: Register, RegisterAgent, Agent, Listener
Body: AgentRegister, AgentTask, AgentResponse, AgentOutput, AgentBuild
ListenerAdd, ListenerAddExC2, ListenerStart, ListenerShutdown*, ListenerTransmit
*ListenerShutdown is declared but not handled by the dispatcher.
→ { "Head": { "Type": "Register" }, "Body": { "Password": "<plaintext>" } }
← { "Head": { "Type": "Register" }, "Body": { "Success": true } }Password compared as hex SHA3-256 against the profile value. On socket close, everything registered by that connection is removed (see Ownership and lifecycle below).
-
Login throttling: 5 failed
Registerattempts from one IP → 5-minute lockout (service.go:28-31, 96-134, checked at285-288); probes during lockout are rejected without extending it. -
Unauthenticated timeout: a connection that never completes login is closed after 30 seconds, enforced by a watchdog (
service.go:38-41, 179, 187-201). -
Frame limit: WebSocket reads are capped at 64 MiB (
service.go:36, 175). - A startup warning is printed when the profile
service-passwordis weak/default (teamserver.go:951-952).
Only the service connection that registered an agent (via AgentRegister) may task it or inject output for it: Task: Add (service.go:432), Task: Get (service.go:469) and AgentOutput (service.go:682) all check the ownership map (types.go:69, OwnsAgent at service.go:157); requests from other connections are silently dropped.
Head.Type = "RegisterAgent", Body.Agent unmarshalled into AgentService:
{
"Name": "Talon",
"MagicValue": "0x74616c6f",
"Author": "...", "Description": "...",
"SupportedOS": ["Windows"],
"Formats": [ { "Name": "...", "Extension": "..." } ],
"Commands": [
{ "Name": "...", "Description": "...", "Help": "", "NeedAdmin": false,
"Mitr": [],
"Params": [ { "Name": "...", "IsFilePath": false, "IsOptional": false } ] }
],
"BuildingConfig": { }
}(Note: an "Anonymous" key is accepted by older docs but does not exist in the Command struct (pkg/service/agent.go:24-31 has only Name/Description/Help/NeedAdmin/Mitr/Params) and is silently ignored.)
Broadcast to operator clients as Service/AgentRegister (packager type 0x9, subevent 0x1). Re-registering a duplicate agent type name (HeadRegisterAgent) is silently ignored; no response is sent to the caller (service.go:375-378).
Body: Type, AgentHeader{ Size (dec string), MagicValue (hex string), AgentID (hex string) }, RegisterInfo (map; see Building a Third-Party Agent for keys). Handled by agent.RegisterInfoToInstance. A duplicate AgentRegister (same NameID owned by another connection, or already in ServerAgents) now receives an explicit failure: Body.Register = {"Success": false, "Error": "agent with this AgentID is already registered"} (service.go:592-609).
- server → client: operator ran a command.
{ "Head": {"Type": "Agent"}, "Body": { "Type": "AgentTask", "Task": "Add", "Agent": { ...agent.ToMap()... }, "Command": { "TaskID": "...", "DemonID": "...", "CommandID": "...", "CommandLine": "...", "...": "command params" } } } - client → server
Task: "Add": queue a raw job.Body.Agent.NameID,Body.Command= base64 job payload → added to the session's job queue. - client → server
Task: "Get": poll the queue.Body.Agent.NameID; teamserver rewrites the message withBody.TasksQueue= base64 of concatenated queued job payloads. (Stale-guard: aTask: "Get"that itself contains aTasksQueuekey is a no-op,service.go:461.)
- server → client: raw implant bytes arrived at a listener for your magic value. Blocks until you reply, with a 5-minute
responseTimeout(pkg/service/agent.go:16,146-158); after that the request fails and returns nil, and nothing is written to the implant.{ "Head": {"Type": "Agent"}, "Body": { "Type": "AgentResponse", "Agent": { ... } | null, "RandID": "abc123", "AgentHeader": { "Size": "...", "AgentID": "%08x", "MagicValue": "%x" }, "Response": "<base64 raw request body after the 12-byte header>" } } - client → server: answer with matching
RandID;Body.Response(base64) is written verbatim to the implant's HTTP connection. Routed viaClientService.Responses map[string]chan []byte.
Body.AgentID (string), Body.Callback (map). Broadcast to clients as DemonOutput with command 0x80 (HAVOC_CONSOLE_MESSAGE). If Callback.MiscType == "download", Callback.FileName + base64 Callback.Content are saved to loot (logr.DemonAddDownloadedFile) and the callback rewritten with MiscData/MiscData2.
- server → client (
AgentService.SendAgentBuildRequest):Body = { "ClientID", "Type": "AgentBuild", "Config": {...}, "Options": { "Listener", "Arch", "Format" } }. - client → server:
Body.ClientID,Body.Message={"FileName": "...", "Payload": "<base64>"}(→events.Gate.SendStageless) or{"Type": "...", "Message": "..."}(→ console message to that client).
Body.Listener = { "Name": "...", "Agent": "<agent name>", "Items": [ { ...UI form fields... } ] }. All three keys are required: a missing Name/Agent/Items (or a missing Body.Listener entirely) is silently ignored (service.go:817-866). Valid ones are shown in the client listener dialog (broadcast as Service/RegisterListener, packager 0x9/0x2).
Body.Listener must contain Name, Protocol, Host, PortBind, Error, Status, Info. Published as a normal Listener/Add event.
Request: Head.RequestID, Body.Name, Body.Endpoint. The teamserver creates a handlers.External listener (POST /<Endpoint> → parseAgentRequest) and replies with the same RequestID:
{ "Head": { "Type": "Listener", "RequestID": "..." },
"Body": { "Type": "ListenerAddExC2",
"ExC2": { "Success": true, "Error": "", "Name": "..." } } }Body.RequestID, Body.Request (base64) → resolved through the pending-response channel for that RequestID.
-
handleServiceAgent(teamserver/pkg/handlers/handlers.go:339): the magic-value dispatch that callsSendResponse. See Handlers for the two-side (Demon / third-party) dispatch model. -
cmd/server/service.go:ServiceAgent(magic)/ServiceAgentExist(magic)bridge handlers ↔ service. -
cmd/server/dispatch.go:299: operator Session/Input routed toAgentService.SendTaskfor service agents. -
cmd/server/dispatch.go:1063: Gate/Stageless routed toSendAgentBuildRequestfor service agents.
When a service connection drops, its registered agents are removed from ServerAgents and the teamserver broadcasts Session/MarkAs "Dead" events to operator clients for each of them (service.go:1148-1152, 1169-1173). AgentResponse and ListenerTransmit replies that arrive after the requester gave up (channel full / nobody waiting) are dropped non-blockingly (service.go:653-658, 1050-1055).