mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-07 11:28:40 +02:00
Add files via upload
This commit is contained in:
@@ -291,16 +291,13 @@ func (l *HTTPBeaconListener) handleResult(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
var report TaskResultReport
|
||||
plaintext, decErr := DecryptAESGCM(l.rec.EncryptionKey, string(body))
|
||||
if decErr == nil {
|
||||
if err := json.Unmarshal(plaintext, &report); err != nil {
|
||||
l.disguisedReject(w)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal(body, &report); err != nil {
|
||||
l.disguisedReject(w)
|
||||
return
|
||||
}
|
||||
if decErr != nil {
|
||||
l.disguisedReject(w)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(plaintext, &report); err != nil {
|
||||
l.disguisedReject(w)
|
||||
return
|
||||
}
|
||||
if err := l.manager.IngestTaskResult(report); err != nil {
|
||||
http.Error(w, "ingest result failed", http.StatusInternalServerError)
|
||||
@@ -341,12 +338,15 @@ func (l *HTTPBeaconListener) handleUpload(w http.ResponseWriter, r *http.Request
|
||||
l.disguisedReject(w)
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(l.manager.StorageDir(), "uploads")
|
||||
dir, dst, err := uploadPathForTask(l.manager.StorageDir(), taskID)
|
||||
if err != nil {
|
||||
l.disguisedReject(w)
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
http.Error(w, "mkdir failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dst := filepath.Join(dir, taskID+".bin")
|
||||
if err := os.WriteFile(dst, plaintext, 0o644); err != nil {
|
||||
http.Error(w, "save failed", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -227,3 +228,87 @@ func TestHTTPBeaconListener_HandleFileServe(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBeaconListener_HandleUploadConfinesTaskID(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
store := filepath.Join(tmp, "c2store")
|
||||
keyB64, err := GenerateAESKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
token := "test-implant-token-upload"
|
||||
l := &HTTPBeaconListener{
|
||||
rec: &database.C2Listener{
|
||||
EncryptionKey: keyB64,
|
||||
ImplantToken: token,
|
||||
},
|
||||
manager: NewManager(nil, zap.NewNop(), store),
|
||||
logger: zap.NewNop(),
|
||||
}
|
||||
|
||||
encrypted, err := EncryptAESGCM(keyB64, []byte("safe upload"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/upload?task_id=t_safe123", strings.NewReader(encrypted))
|
||||
req.Header.Set("X-Implant-Token", token)
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
l.handleUpload(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%q", rr.Code, rr.Body.String())
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(store, "uploads", "t_safe123.bin"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "safe upload" {
|
||||
t.Fatalf("content=%q", got)
|
||||
}
|
||||
|
||||
evilBody, err := EncryptAESGCM(keyB64, []byte("owned"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
evilReq := httptest.NewRequest(http.MethodPost, "/upload?task_id=..%2Fowned", strings.NewReader(evilBody))
|
||||
evilReq.Header.Set("X-Implant-Token", token)
|
||||
evilRR := httptest.NewRecorder()
|
||||
|
||||
l.handleUpload(evilRR, evilReq)
|
||||
|
||||
if evilRR.Code != http.StatusNotFound {
|
||||
t.Fatalf("status=%d body=%q", evilRR.Code, evilRR.Body.String())
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(store, "owned.bin")); !os.IsNotExist(err) {
|
||||
t.Fatalf("outside file exists or stat failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBeaconListener_HandleResultRejectsPlaintextJSON(t *testing.T) {
|
||||
keyB64, err := GenerateAESKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l := &HTTPBeaconListener{
|
||||
rec: &database.C2Listener{
|
||||
EncryptionKey: keyB64,
|
||||
ImplantToken: "test-implant-token-result",
|
||||
},
|
||||
logger: zap.NewNop(),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/result", strings.NewReader(`{"task_id":"t_test","success":true}`))
|
||||
req.Header.Set("X-Implant-Token", "test-implant-token-result")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
l.handleResult(rr, req)
|
||||
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("status=%d body=%q", rr.Code, rr.Body.String())
|
||||
}
|
||||
if !strings.Contains(rr.Body.String(), "404 Not Found") {
|
||||
t.Fatalf("expected disguised 404 body, got %q", rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
+62
-5
@@ -42,6 +42,11 @@ type Manager struct {
|
||||
// MCPToolC2Task 与 MCP builtin、c2_task 工具名一致,供 HITL 白名单与 Agent 侧对齐。
|
||||
const MCPToolC2Task = "c2_task"
|
||||
|
||||
var (
|
||||
resultBlobSuffixPattern = regexp.MustCompile(`^\.[A-Za-z0-9][A-Za-z0-9_-]{0,31}$`)
|
||||
uploadTaskIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$`)
|
||||
)
|
||||
|
||||
// HITLBridge 把"危险任务"桥到现有 internal/handler/hitl 审批流的接口。
|
||||
// internal/app 实例化时传入;空实现表示禁用 HITL 拦截(开发期方便)。
|
||||
type HITLBridge interface {
|
||||
@@ -736,18 +741,24 @@ func (m *Manager) IngestTaskResult(report TaskResultReport) error {
|
||||
}
|
||||
|
||||
func (m *Manager) saveResultBlob(taskID, b64Content, suffix string) (string, error) {
|
||||
suffix = strings.TrimSpace(suffix)
|
||||
if suffix == "" {
|
||||
suffix = ".bin"
|
||||
taskID = strings.TrimSpace(taskID)
|
||||
if taskID == "" || taskID == "." || taskID == ".." ||
|
||||
strings.ContainsAny(taskID, `/\`) {
|
||||
return "", fmt.Errorf("invalid task_id")
|
||||
}
|
||||
if !strings.HasPrefix(suffix, ".") {
|
||||
suffix = "." + suffix
|
||||
|
||||
suffix, err := normalizeResultBlobSuffix(suffix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := filepath.Join(m.storageDir, "results")
|
||||
if err := osMkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := filepath.Join(dir, taskID+suffix)
|
||||
if err := ensurePathInDir(dir, path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := base64Decode(b64Content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -758,6 +769,52 @@ func (m *Manager) saveResultBlob(taskID, b64Content, suffix string) (string, err
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func uploadPathForTask(storageDir, taskID string) (dir, path string, err error) {
|
||||
taskID = strings.TrimSpace(taskID)
|
||||
if !uploadTaskIDPattern.MatchString(taskID) {
|
||||
return "", "", fmt.Errorf("invalid task_id")
|
||||
}
|
||||
dir = filepath.Join(storageDir, "uploads")
|
||||
path = filepath.Join(dir, taskID+".bin")
|
||||
if err := ensurePathInDir(dir, path); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return dir, path, nil
|
||||
}
|
||||
|
||||
func normalizeResultBlobSuffix(suffix string) (string, error) {
|
||||
suffix = strings.TrimSpace(suffix)
|
||||
if suffix == "" {
|
||||
return ".bin", nil
|
||||
}
|
||||
if !strings.HasPrefix(suffix, ".") {
|
||||
suffix = "." + suffix
|
||||
}
|
||||
if !resultBlobSuffixPattern.MatchString(suffix) {
|
||||
return "", fmt.Errorf("invalid blob suffix")
|
||||
}
|
||||
return suffix, nil
|
||||
}
|
||||
|
||||
func ensurePathInDir(dir, path string) error {
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(absDir, absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." || filepath.IsAbs(rel) {
|
||||
return fmt.Errorf("path escapes result directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 事件总线辅助
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package c2
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestManagerSaveResultBlobConfinesPath(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
mgr := NewManager(nil, zap.NewNop(), filepath.Join(tmp, "c2store"))
|
||||
content := base64.StdEncoding.EncodeToString([]byte("result bytes"))
|
||||
|
||||
got, err := mgr.saveResultBlob("t_safe123", content, "txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := filepath.Join(tmp, "c2store", "results", "t_safe123.txt")
|
||||
if got != want {
|
||||
t.Fatalf("path=%q want %q", got, want)
|
||||
}
|
||||
raw, err := os.ReadFile(want)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(raw) != "result bytes" {
|
||||
t.Fatalf("content=%q", raw)
|
||||
}
|
||||
|
||||
outside := filepath.Join(tmp, "owned")
|
||||
if _, err := mgr.saveResultBlob("t_safe123", content, "./../../owned"); err == nil {
|
||||
t.Fatal("expected traversal suffix to be rejected")
|
||||
}
|
||||
if _, err := os.Stat(outside); !os.IsNotExist(err) {
|
||||
t.Fatalf("outside file exists or stat failed unexpectedly: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadPathForTaskConfinesPath(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
store := filepath.Join(tmp, "c2store")
|
||||
|
||||
dir, got, err := uploadPathForTask(store, "t_safe123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := filepath.Join(store, "uploads"); dir != want {
|
||||
t.Fatalf("dir=%q want %q", dir, want)
|
||||
}
|
||||
if want := filepath.Join(store, "uploads", "t_safe123.bin"); got != want {
|
||||
t.Fatalf("path=%q want %q", got, want)
|
||||
}
|
||||
|
||||
for _, taskID := range []string{"", ".", "..", "../owned", `..\owned`, "sub/owned", "sub\\owned", "task.with.dot", "-leading"} {
|
||||
if _, _, err := uploadPathForTask(store, taskID); err == nil {
|
||||
t.Fatalf("task_id %q unexpectedly accepted", taskID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeResultBlobSuffix(t *testing.T) {
|
||||
for _, suffix := range []string{"", "png", ".jpg", ".7z", ".safe_name-1"} {
|
||||
if _, err := normalizeResultBlobSuffix(suffix); err != nil {
|
||||
t.Fatalf("suffix %q rejected: %v", suffix, err)
|
||||
}
|
||||
}
|
||||
for _, suffix := range []string{".", "..", "../x", "./../../x", "/tmp/x", `..\x`, ".name.with.dot", ".toolong012345678901234567890123456789"} {
|
||||
if _, err := normalizeResultBlobSuffix(suffix); err == nil {
|
||||
t.Fatalf("suffix %q unexpectedly accepted", suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,11 +209,13 @@ func (l *TCPReverseListener) handleTCPBeaconSession(conn net.Conn, br *bufio.Rea
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dir := filepath.Join(l.manager.StorageDir(), "uploads")
|
||||
dir, dst, err := uploadPathForTask(l.manager.StorageDir(), up.TaskID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return
|
||||
}
|
||||
dst := filepath.Join(dir, up.TaskID+".bin")
|
||||
if err := os.WriteFile(dst, plainFile, 0o644); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user