Overview

JustC2 uses a Go plugin architecture where listeners and agent types are implemented as shared libraries (.so files). Each plugin is called an extender. You can develop custom extenders to add new communication protocols or agent types.

Architecture

Extenders are loaded at startup based on the extenders list in profile.yaml. Each extender directory contains:

my_custom_extender/
├── config.yaml       # Extender metadata
├── my_extender.so    # Compiled plugin binary
├── ax_config.axs     # AxScript UI configuration
└── Makefile          # Build instructions

The axc2 Module

All extenders import the justc2/axc2 module, which defines the shared interfaces and data types. Your extender’s go.mod should include:

module my_extender

go 1.26.5

require justc2/axc2 v0.0.0

replace justc2/axc2 => ../../../axc2

Developing a Listener Extender

config.yaml

extender_type: "listener"
extender_file: "my_listener.so"
ax_file: "ax_config.axs"

listener_name: "MyCustomListener"
listener_type: "external"  # or "internal"
protocol: "custom"

Implementing PluginListener

Your plugin must export a PluginListener variable that implements the interface:

package main

import "justc2/axc2"

type MyListener struct{}

func (l *MyListener) Create(name, config string, customData []byte) (
    axc2.ExtenderListener, axc2.ListenerData, []byte, error,
) {
    // Parse config, create listener instance
    listener := &MyListenerInstance{name: name}

    data := axc2.ListenerData{
        Name:     name,
        RegName:  "MyCustomListener",
        Protocol: "custom",
        Type:     "external",
        Status:   "stopped",
    }

    return listener, data, nil, nil
}

// Export as Go plugin
var PluginListener MyListener

Implementing ExtenderListener

type MyListenerInstance struct {
    name string
}

func (l *MyListenerInstance) Start() error {
    // Start the listener (open ports, register endpoints)
    return nil
}

func (l *MyListenerInstance) Edit(config string) (axc2.ListenerData, []byte, error) {
    // Handle configuration changes
    return axc2.ListenerData{}, nil, nil
}

func (l *MyListenerInstance) Stop() error {
    // Stop the listener (close ports, unregister endpoints)
    return nil
}

func (l *MyListenerInstance) GetProfile() ([]byte, error) {
    // Return the transport profile for agent generation
    return nil, nil
}

func (l *MyListenerInstance) InternalHandler(data []byte) (string, error) {
    // Process incoming agent data
    // Returns the agent ID as a string
    return "", nil
}

External vs Internal Listeners

External listeners register HTTP endpoints on the teamserver’s existing HTTP server using the connector’s RegisterEndpoint() and RegisterPublicEndpoint() methods. The teamserver handles TLS.

Internal (bind) listeners open their own network sockets independently. They manage their own connections and protocols.

Developing an Agent Extender

config.yaml

extender_type: "agent"
extender_file: "my_agent.so"
ax_file: "ax_config.axs"

agent_name: "myagent"
agent_watermark: "deadbeef"
listeners:
  - "MyCustomListener"
multi_listeners: false

Implementing PluginAgent

package main

import "justc2/axc2"

type MyAgentPlugin struct{}

func (p *MyAgentPlugin) GenerateProfiles(profile axc2.BuildProfile) ([][]byte, error) {
    // Generate transport profiles for the agent payload
    return nil, nil
}

func (p *MyAgentPlugin) BuildPayload(
    profile axc2.BuildProfile, agentProfiles [][]byte,
) ([]byte, string, error) {
    // Compile the agent payload
    // Returns: payload bytes, filename, error
    return nil, "myagent.bin", nil
}

func (p *MyAgentPlugin) CreateAgent(beat []byte) (
    axc2.AgentData, axc2.ExtenderAgent, error,
) {
    // Parse the initial agent check-in data
    // Return agent info and the extender for command handling
    return axc2.AgentData{}, &MyAgentExtender{}, nil
}

func (p *MyAgentPlugin) GetExtender() axc2.ExtenderAgent {
    return &MyAgentExtender{}
}

var PluginAgent MyAgentPlugin

Implementing ExtenderAgent

type MyAgentExtender struct{}

func (e *MyAgentExtender) CreateCommand(
    agent axc2.AgentData, args map[string]any,
) (axc2.TaskData, axc2.ConsoleMessageData, error) {
    // Create a task from a command
    return axc2.TaskData{}, axc2.ConsoleMessageData{}, nil
}

func (e *MyAgentExtender) PackTasks(
    agent axc2.AgentData, tasks []axc2.TaskData,
) ([]byte, error) {
    // Pack multiple tasks into a single binary blob for delivery
    return nil, nil
}

func (e *MyAgentExtender) ProcessData(
    agent axc2.AgentData, data []byte,
) error {
    // Process data returned by the agent
    return nil
}

func (e *MyAgentExtender) Encrypt(data, key []byte) ([]byte, error) {
    // Encrypt data for the agent
    return data, nil
}

func (e *MyAgentExtender) Decrypt(data, key []byte) ([]byte, error) {
    // Decrypt data from the agent
    return data, nil
}

func (e *MyAgentExtender) PivotPackData(
    parentId string, data []byte,
) (axc2.TaskData, error) {
    // Pack data for relay through a parent agent (pivoting)
    return axc2.TaskData{}, nil
}

func (e *MyAgentExtender) TunnelCallbacks() axc2.TunnelCallbacks {
    // Return tunnel operation callbacks
    return axc2.TunnelCallbacks{}
}

func (e *MyAgentExtender) TerminalCallbacks() axc2.TerminalCallbacks {
    // Return terminal operation callbacks
    return axc2.TerminalCallbacks{}
}

Building Extenders

Makefile

all:
    @mkdir -p dist
    @CGO_ENABLED=1 go build -buildmode=plugin -o dist/my_extender.so .
    @cp config.yaml ax_config.axs dist/

Compilation

Extenders must be compiled with the same Go version and build flags as the server. Mismatched versions will cause plugin load failures.

CGO_ENABLED=1 go build -buildmode=plugin -o my_extender.so .

Testing

  1. Place your extender directory in dist/extenders/
  2. Add its config.yaml path to profile.yaml’s extenders list
  3. Restart the server
  4. Check the startup logs for successful loading

AxScript Configuration

The ax_config.axs file defines the UI for your extender — the forms for creating listeners, configuring agents, and displaying custom data in the client. See the AxScript reference for details.