Extender Development
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 instructionsThe 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 => ../../../axc2Developing 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 MyListenerImplementing 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: falseImplementing 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 MyAgentPluginImplementing 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
- Place your extender directory in
dist/extenders/ - Add its
config.yamlpath toprofile.yaml’sextenderslist - Restart the server
- 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.