Skip to content

Service API Reference

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

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.

Envelope

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

Authentication (client → server)

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

Connection hardening (teamserver/pkg/service/service.go)

  • Login throttling: 5 failed Register attempts from one IP → 5-minute lockout (service.go:28-31, 96-134, checked at 285-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-password is weak/default (teamserver.go:951-952).

Agent ownership model

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.

Agent type registration (client → server)

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

Agent messages (Head.Type = "Agent")

AgentRegister (client → server): create a session

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

AgentTask: tasking, both directions

  • 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 with Body.TasksQueue = base64 of concatenated queued job payloads. (Stale-guard: a Task: "Get" that itself contains a TasksQueue key is a no-op, service.go:461.)

AgentResponse: implant traffic proxy (both directions)

  • 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 via ClientService.Responses map[string]chan []byte.

AgentOutput (client → server): console output

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.

AgentBuild: payload builds (both directions)

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

Listener messages (Head.Type = "Listener")

ListenerAdd (client → server): register a listener type

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

ListenerStart (client → server): publish a running listener

Body.Listener must contain Name, Protocol, Host, PortBind, Error, Status, Info. Published as a normal Listener/Add event.

ListenerAddExC2 (client → server): spawn an ExternalC2 endpoint

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": "..." } } }

ListenerTransmit (client → server): deliver a response for a pending listener request

Body.RequestID, Body.Request (base64) → resolved through the pending-response channel for that RequestID.

Related server-side pieces

  • handleServiceAgent (teamserver/pkg/handlers/handlers.go:339): the magic-value dispatch that calls SendResponse. 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 to AgentService.SendTask for service agents.
  • cmd/server/dispatch.go:1063: Gate/Stageless routed to SendAgentBuildRequest for service agents.

Disconnect semantics

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

Clone this wiki locally