High-Level Architecture

JustC2 follows a classic C2 architecture with three tiers:

┌─────────────┐     HTTPS/WSS      ┌──────────────┐    HTTP/TCP/SMB/DNS    ┌─────────────┐
│  JustClient │ ◄──────────────────► │  JustServer  │ ◄────────────────────► │   Agents    │
│  (Operator) │    WebSocket + REST  │ (Teamserver) │    Listener Protocols  │  (Targets)  │
└─────────────┘                      └──────────────┘                        └─────────────┘
                                           │
                                     ┌─────┴─────┐
                                     │ Extenders  │
                                     │   (.so)    │
                                     └───────────┘

Component Responsibilities

ComponentLanguageRole
JustServerGoCentral teamserver — manages agents, listeners, tasks, and operator connections
JustClientC++ (Qt6)Desktop GUI for operators to interact with the teamserver
ExtendersGo (.so)Plugin modules that implement listener and agent logic
axc2GoShared type definitions used by the server and extenders

JustServer

The teamserver is the central hub. It exposes a REST API and WebSocket endpoint over TLS for operator connections, and manages listener processes that communicate with deployed agents.

Core Subsystems

  • Connector — HTTP router (Gin framework) that handles REST API requests, JWT authentication, and WebSocket upgrades for real-time operator communication
  • Server — Core business logic: agent lifecycle, task queuing and dispatch, tunnel management, download tracking
  • Database — SQLite-based persistence for agents, tasks, credentials, targets, screenshots, and downloads
  • Extender Manager — Loads Go plugin .so files at startup, manages listener and agent type registration
  • Event Bus — Publishes real-time events (new agents, task completions, downloads) to connected operators via WebSocket
  • Profile — Parses profile.yaml to configure the teamserver, HTTP server behavior, and TLS settings

Authentication Flow

Operator                    JustServer
   │                           │
   ├── POST /login ───────────►│  (username + password)
   │                           │
   │◄── access_token + ────────┤  (JWT tokens)
   │    refresh_token          │
   │                           │
   ├── GET /connect ──────────►│  (OTP-based WebSocket upgrade)
   │                           │
   │◄══ WebSocket ════════════►│  (real-time sync)
   │                           │
   ├── GET /agent/list ───────►│  (Bearer access_token)
   │◄── agent data ────────────┤
   │                           │
   ├── POST /refresh ─────────►│  (refresh_token)
   │◄── new access_token ──────┤
  1. The operator logs in with credentials via POST /login and receives JWT access and refresh tokens
  2. An OTP (one-time password) is generated to establish a WebSocket connection for real-time updates
  3. All subsequent API calls use the access token as a Bearer token
  4. When the access token expires, the refresh token is used to obtain a new one

Agent Communication Flow

Agent                     Listener (Extender)              JustServer
  │                           │                               │
  ├── beacon/check-in ───────►│                               │
  │                           ├── InternalHandler() ─────────►│
  │                           │                               ├── TsAgentCreate()
  │                           │◄── agent ID ──────────────────┤
  │◄── response ──────────────┤                               │
  │                           │                               │
  │    ... time passes ...    │                               │
  │                           │                               │
  ├── beacon/poll ───────────►│                               │
  │                           ├── GetHostedAll() ────────────►│
  │                           │◄── packed tasks ──────────────┤
  │◄── tasks ─────────────────┤                               │
  │                           │                               │
  ├── beacon/result ─────────►│                               │
  │                           ├── ProcessData() ─────────────►│
  │                           │                               ├── TsTaskUpdate()
  1. An agent checks in with its listener, which calls InternalHandler() on the teamserver
  2. On first check-in, the server creates the agent record and returns an agent ID
  3. The agent periodically polls for tasks; the server packs queued tasks via the extender’s PackTasks() function
  4. When the agent returns results, the extender’s ProcessData() function decodes and dispatches them

JustClient

The Qt6 desktop client provides a multi-tabbed interface for operators:

  • Sessions Table — Lists all active agent sessions with OS, user, hostname, and status
  • Sessions Graph — Visual network graph of agent relationships and pivots
  • Console — Per-agent interactive console for executing commands
  • Listeners — Create, start, stop, and configure listeners
  • Downloads — Track file downloads from agents
  • Tunnels — Manage SOCKS proxies and port forwarding tunnels
  • Screenshots — View captured screenshots
  • Credentials — Manage harvested credentials
  • Targets — Track discovered hosts on the network
  • Chat — Communicate with other operators
  • Terminals — Interactive terminal sessions on agents

The client connects to the server over TLS, authenticates via JWT, and maintains a WebSocket connection for real-time event delivery. It uses the Qlementine theme engine for a modern dark UI with JSON-based theme customization.

Extender System

Extenders are Go plugins compiled as shared libraries (.so files). Each extender implements either a listener or an agent type.

Listener Extenders

A listener extender implements the PluginListener interface:

type PluginListener interface {
    Create(name, config string, customData []byte) (ExtenderListener, ListenerData, []byte, error)
}

The returned ExtenderListener handles the listener lifecycle:

type ExtenderListener interface {
    Start() error
    Edit(config string) (ListenerData, []byte, error)
    Stop() error
    GetProfile() ([]byte, error)
    InternalHandler(data []byte) (string, error)
}

Agent Extenders

An agent extender implements the PluginAgent interface:

type PluginAgent interface {
    GenerateProfiles(profile BuildProfile) ([][]byte, error)
    BuildPayload(profile BuildProfile, agentProfiles [][]byte) ([]byte, string, error)
    CreateAgent(beat []byte) (AgentData, ExtenderAgent, error)
    GetExtender() ExtenderAgent
}

Configuration

Each extender has a config.yaml that declares its type, binary file, and relationships:

# Listener extender example
extender_type: "listener"
extender_file: "listener_beacon_http.so"
ax_file: "ax_config.axs"
listener_name: "BeaconHTTP"
listener_type: "external"
protocol: "http"
# Agent extender example
extender_type: "agent"
extender_file: "agent_beacon.so"
ax_file: "ax_config.axs"
agent_name: "beacon"
agent_watermark: "be4c0149"
listeners:
  - "BeaconHTTP"
  - "BeaconTCP"
  - "BeaconSMB"
  - "BeaconDNS"
multi_listeners: false

Listener Types

  • External — The listener registers HTTP/public endpoints on the teamserver’s own HTTP server. The teamserver handles TLS termination. Examples: BeaconHTTP, BeaconDNS, GopherTCP
  • Internal (Bind) — The listener opens its own port or pipe independently of the teamserver. Examples: BeaconTCP, BeaconSMB