Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions AdaptixServer/extenders/beacon_listener_discord/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
all: clean
@ echo " * Building listener_beacon_discord plugin"
@ mkdir dist
@ cp config.yaml ax_config.axs ./dist/
@ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/listener_beacon_discord.so pl_main.go pl_transport.go
@ echo " done..."

clean:
@ rm -rf dist
90 changes: 90 additions & 0 deletions AdaptixServer/extenders/beacon_listener_discord/ax_config.axs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/// Beacon Discord listener

function ListenerUI(mode_create)
{
// BOT TOKEN
let labelBotToken = form.create_label("Bot Token:");
let textlineBotToken = form.create_textline();
textlineBotToken.setPlaceholder("Discord bot token (server-side only)");
textlineBotToken.setEnabled(mode_create);

// CHANNEL IDS
let labelChannelBeacon = form.create_label("Beacon Channel ID:");
let textlineChannelBeacon = form.create_textline();
textlineChannelBeacon.setPlaceholder("Channel for beacon -> server messages");
textlineChannelBeacon.setEnabled(mode_create);

let labelChannelTasks = form.create_label("Tasks Channel ID:");
let textlineChannelTasks = form.create_textline();
textlineChannelTasks.setPlaceholder("Channel for server -> beacon tasks");
textlineChannelTasks.setEnabled(mode_create);

// WEBHOOK URL
let labelWebhook = form.create_label("Webhook URL:");
let textlineWebhook = form.create_textline();
textlineWebhook.setPlaceholder("https://discord.com/api/webhooks/...");

// POLL INTERVAL
let labelPollInterval = form.create_label("Poll Interval (seconds):");
let spinPollInterval = form.create_spin();
spinPollInterval.setRange(1, 60);
spinPollInterval.setValue(5);

// CLEANUP
let checkCleanup = form.create_check("Delete messages after reading");
checkCleanup.setChecked(true);

// ENCRYPTION KEY
let labelEncryptKey = form.create_label("Encryption key:");
let textlineEncryptKey = form.create_textline(ax.random_string(64, "hex"));
textlineEncryptKey.setEnabled(mode_create);
let buttonEncryptKey = form.create_button("Generate");
buttonEncryptKey.setEnabled(mode_create);

form.connect(buttonEncryptKey, "clicked", function() { textlineEncryptKey.setText( ax.random_string(64, "hex") ); });

// LAYOUT
let layoutMain = form.create_gridlayout();
layoutMain.addWidget(labelBotToken, 0, 0, 1, 1);
layoutMain.addWidget(textlineBotToken, 0, 1, 1, 2);
layoutMain.addWidget(labelChannelBeacon, 1, 0, 1, 1);
layoutMain.addWidget(textlineChannelBeacon,1, 1, 1, 2);
layoutMain.addWidget(labelChannelTasks, 2, 0, 1, 1);
layoutMain.addWidget(textlineChannelTasks, 2, 1, 1, 2);
layoutMain.addWidget(labelWebhook, 3, 0, 1, 1);
layoutMain.addWidget(textlineWebhook, 3, 1, 1, 2);
layoutMain.addWidget(labelPollInterval, 4, 0, 1, 1);
layoutMain.addWidget(spinPollInterval, 4, 1, 1, 2);
layoutMain.addWidget(checkCleanup, 5, 0, 1, 3);
layoutMain.addWidget(labelEncryptKey, 6, 0, 1, 1);
layoutMain.addWidget(textlineEncryptKey, 6, 1, 1, 1);
layoutMain.addWidget(buttonEncryptKey, 6, 2, 1, 1);

let panelMain = form.create_panel();
panelMain.setLayout(layoutMain);

let tabs = form.create_tabs();
tabs.addTab(panelMain, "Main settings");

let layout = form.create_hlayout();
layout.addWidget(tabs);

let container = form.create_container();
container.put("bot_token", textlineBotToken);
container.put("channel_beacon", textlineChannelBeacon);
container.put("channel_tasks", textlineChannelTasks);
container.put("webhook_url", textlineWebhook);
container.put("poll_interval", spinPollInterval);
container.put("cleanup", checkCleanup);
container.put("encrypt_key", textlineEncryptKey);

let panel = form.create_panel();
panel.setLayout(layout);

return {
ui_panel: panel,
ui_container: container,
ui_height: 400,
ui_width: 650
}
}
7 changes: 7 additions & 0 deletions AdaptixServer/extenders/beacon_listener_discord/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extender_type: "listener"
extender_file: "listener_beacon_discord.so"
ax_file: "ax_config.axs"

listener_name: "BeaconDiscord"
listener_type: "external"
protocol: "discord"
5 changes: 5 additions & 0 deletions AdaptixServer/extenders/beacon_listener_discord/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module adaptix_listener_beacon_discord

go 1.25.4

require github.com/Adaptix-Framework/axc2 v1.2.0
2 changes: 2 additions & 0 deletions AdaptixServer/extenders/beacon_listener_discord/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA=
github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ=
243 changes: 243 additions & 0 deletions AdaptixServer/extenders/beacon_listener_discord/pl_main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package main

import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"regexp"

adaptix "github.com/Adaptix-Framework/axc2"
)

type Teamserver interface {
TsAgentIsExists(agentId string) bool
TsAgentCreate(agentCrc string, agentId string, beat []byte, listenerName string, ExternalIP string, Async bool) (adaptix.AgentData, error)
TsAgentProcessData(agentId string, bodyData []byte) error
TsAgentSetTick(agentId string, listenerName string) error
TsAgentGetHostedAll(agentId string, maxDataSize int) ([]byte, error)
}

type PluginListener struct{}

var (
ModuleDir string
ListenerDataDir string
Ts Teamserver
)

func InitPlugin(ts any, moduleDir string, listenerDir string) adaptix.PluginListener {
ModuleDir = moduleDir
ListenerDataDir = listenerDir
Ts = ts.(Teamserver)
return &PluginListener{}
}

func (p *PluginListener) Create(name string, config string, customData []byte) (adaptix.ExtenderListener, adaptix.ListenerData, []byte, error) {
var (
listener *Listener
listenerData adaptix.ListenerData
conf ConfigDiscord
customdData []byte
err error
)

/// START CODE HERE

if customData == nil {
if err = validConfig(config); err != nil {
return nil, listenerData, customdData, err
}

err = json.Unmarshal([]byte(config), &conf)
if err != nil {
return nil, listenerData, customdData, err
}

conf.encryptKeyBytes, err = hex.DecodeString(conf.EncryptKey)
if err != nil {
return nil, listenerData, customdData, fmt.Errorf("invalid encrypt_key hex: %v", err)
}

} else {
err = json.Unmarshal(customData, &conf)
if err != nil {
return nil, listenerData, customdData, err
}

conf.encryptKeyBytes, err = hex.DecodeString(conf.EncryptKey)
if err != nil {
return nil, listenerData, customdData, fmt.Errorf("invalid encrypt_key hex: %v", err)
}
}

transport := &TransportDiscord{
Name: name,
Config: conf,
Active: false,
}

listenerData = adaptix.ListenerData{
BindHost: "discord",
BindPort: "0",
AgentAddr: conf.WebhookUrl,
Protocol: "discord",
Status: "Stopped",
}

var buffer bytes.Buffer
err = json.NewEncoder(&buffer).Encode(transport.Config)
if err != nil {
return nil, listenerData, customdData, err
}
customdData = buffer.Bytes()

listener = &Listener{transport: transport}

/// END CODE HERE

return listener, listenerData, customdData, nil
}

func (l *Listener) Start() error {

/// START CODE HERE

return l.transport.Start(Ts)

/// END CODE HERE
}

func (l *Listener) Edit(config string) (adaptix.ListenerData, []byte, error) {
var (
listenerData adaptix.ListenerData
conf ConfigDiscord
customdData []byte
err error
)

err = json.Unmarshal([]byte(config), &conf)
if err != nil {
return listenerData, customdData, err
}

/// START CODE HERE

l.transport.Config.WebhookUrl = conf.WebhookUrl
l.transport.Config.PollInterval = conf.PollInterval
l.transport.Config.Cleanup = conf.Cleanup

listenerData = adaptix.ListenerData{
BindHost: "discord",
BindPort: "0",
AgentAddr: l.transport.Config.WebhookUrl,
Status: "Listen",
}
if !l.transport.Active {
listenerData.Status = "Closed"
}

var buffer bytes.Buffer
err = json.NewEncoder(&buffer).Encode(l.transport.Config)
if err != nil {
return listenerData, customdData, err
}
customdData = buffer.Bytes()

/// END CODE HERE

return listenerData, customdData, nil
}

func (l *Listener) Stop() error {

/// START CODE HERE

return l.transport.Stop()

/// END CODE HERE
}

func (l *Listener) GetProfile() ([]byte, error) {
var buffer bytes.Buffer

/// START CODE HERE

// Return only what the beacon needs: webhook URL, channel IDs, poll interval, encrypt key
profile := map[string]any{
"protocol": "discord",
"webhook_url": l.transport.Config.WebhookUrl,
"bot_token": l.transport.Config.BotToken,
"channel_beacon": l.transport.Config.ChannelBeacon,
"channel_tasks_id": l.transport.Config.ChannelTasks,
"poll_interval": l.transport.Config.PollInterval,
"encrypt_key": l.transport.Config.EncryptKey,
"cleanup": l.transport.Config.Cleanup,
}

err := json.NewEncoder(&buffer).Encode(profile)
if err != nil {
return nil, err
}
/// END CODE HERE

return buffer.Bytes(), nil
}

func (l *Listener) InternalHandler(data []byte) (string, error) {
var agentId = ""

/// START CODE HERE

/// END CODE HERE

return agentId, nil
}

func validConfig(config string) error {
var conf ConfigDiscord
err := json.Unmarshal([]byte(config), &conf)
if err != nil {
return err
}

if conf.BotToken == "" {
return errors.New("bot_token is required")
}

if conf.ChannelBeacon == "" {
return errors.New("channel_beacon is required")
}
matchChan, _ := regexp.MatchString("^[0-9]+$", conf.ChannelBeacon)
if !matchChan {
return errors.New("channel_beacon must be a numeric Discord channel ID")
}

if conf.ChannelTasks == "" {
return errors.New("channel_tasks is required")
}
matchChan, _ = regexp.MatchString("^[0-9]+$", conf.ChannelTasks)
if !matchChan {
return errors.New("channel_tasks must be a numeric Discord channel ID")
}

if conf.WebhookUrl == "" {
return errors.New("webhook_url is required")
}
matchWebhook, _ := regexp.MatchString("^https://discord\\.com/api/webhooks/", conf.WebhookUrl)
if !matchWebhook {
return errors.New("webhook_url must be a valid Discord webhook URL")
}

if conf.PollInterval < 1 || conf.PollInterval > 60 {
return errors.New("poll_interval must be between 1 and 60 seconds")
}

match, _ := regexp.MatchString("^[0-9a-f]{64}$", conf.EncryptKey)
if len(conf.EncryptKey) != 64 || !match {
return errors.New("encrypt_key must be 64 hex characters (32 bytes for AES-256)")
}

return nil
}
Loading