diff --git a/docs/bug-sheet-review.md b/docs/bug-sheet-review.md index 740187aa..9aee43db 100644 --- a/docs/bug-sheet-review.md +++ b/docs/bug-sheet-review.md @@ -144,3 +144,7 @@ ### 单资产有限核验范围和写回提示(表格第 48 行) 默认单 Agent 系统提示中的持续深挖、穷尽攻击面和赏金门槛要求,与只读连通性/指纹任务完成条件冲突。现以本轮目标限制执行深度,403/认证失败等按核验目标如实报告,达到目标后结束,仍保留明确全面评估任务的验证要求。自定义资产模板此前替换掉默认模板中 complete_asset_scan 指令,现保留用户模板内容并追加完成后的现有记录动作,不增加探测范围;已有写回指令不重复。新增前端测试验证限定核验模板、资产 ID、写回提示及无额外漏洞/弱口令探测提示。Agent 包测试、资产工具和数据库测试通过,后者覆盖实际 complete_asset_scan 回写时间、会话及漏洞数量。未运行真实模型探测,提示修复不能证明所有模型均会及时结束;历史长时间行为仍保留验证限制。 + +### HTTP 文件上传归属检查(表格第 36 行扩展路径) + +HTTP 上传端点此前只验证共享监听器凭据与任务 ID 路径,未校验任务会话归属。现上传前查询任务及其会话,拒绝其他监听器任务。回归测试验证外部监听器不能覆盖已有文件、所属监听器正常上传可用;原路径安全测试增加真实任务/会话夹具。C2 包测试通过。文件下载端点使用独立 file_id,不可直接当 task_id 校验,未以错误映射限制下载;同监听器会话隔离及下载文件归属继续核对。 diff --git a/internal/c2/http_file_ownership_test.go b/internal/c2/http_file_ownership_test.go new file mode 100644 index 00000000..85782998 --- /dev/null +++ b/internal/c2/http_file_ownership_test.go @@ -0,0 +1,67 @@ +package c2 + +import ( + "cyberstrike-ai/internal/database" + "go.uber.org/zap" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" +) + +func TestHTTPUploadEnforcesListenerOwnership(t *testing.T) { + manager, db := terminalTestManager(t) + task := &database.C2Task{ID: "test-only-file-task", SessionID: "session", TaskType: "download", Status: "sent", CreatedAt: time.Now()} + if err := db.CreateC2Task(task); err != nil { + t.Fatal(err) + } + key, err := GenerateAESKey() + if err != nil { + t.Fatal(err) + } + body, err := EncryptAESGCM(key, []byte("test-only-upload")) + if err != nil { + t.Fatal(err) + } + dir, path, err := uploadPathForTask(manager.StorageDir(), task.ID) + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("original"), 0600); err != nil { + t.Fatal(err) + } + listener := &HTTPBeaconListener{rec: &database.C2Listener{ID: "foreign-listener", EncryptionKey: key, ImplantToken: "test-only-token"}, manager: manager, cfg: &ListenerConfig{BeaconFilePath: "/file/"}, logger: zap.NewNop()} + for _, owned := range []bool{false, true} { + if owned { + listener.rec.ID = "listener" + } + upload := httptest.NewRequest(http.MethodPost, "/upload?task_id="+task.ID, strings.NewReader(body)) + upload.Header.Set("X-Implant-Token", "test-only-token") + rr := httptest.NewRecorder() + listener.handleUpload(rr, upload) + expected := http.StatusNotFound + if owned { + expected = http.StatusOK + } + if rr.Code != expected { + t.Fatalf("upload owned=%v: status=%d", owned, rr.Code) + } + saved, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + expectedContent := "original" + if owned { + expectedContent = "test-only-upload" + } + if string(saved) != expectedContent { + t.Fatalf("unexpected file mutation owned=%v: %q", owned, saved) + } + + } +} diff --git a/internal/c2/listener_http.go b/internal/c2/listener_http.go index 4f930ba3..8917bfc4 100644 --- a/internal/c2/listener_http.go +++ b/internal/c2/listener_http.go @@ -324,7 +324,7 @@ func (l *HTTPBeaconListener) handleUpload(w http.ResponseWriter, r *http.Request return } taskID := r.URL.Query().Get("task_id") - if taskID == "" { + if taskID == "" || !l.ownsTask(taskID) { l.disguisedReject(w) return } @@ -397,6 +397,15 @@ func (l *HTTPBeaconListener) handleFileServe(w http.ResponseWriter, r *http.Requ // 鉴权 / 输出辅助 // ---------------------------------------------------------------------------- +func (l *HTTPBeaconListener) ownsTask(taskID string) bool { + task, err := l.manager.DB().GetC2Task(taskID) + if err != nil || task == nil { + return false + } + session, err := l.manager.DB().GetC2Session(task.SessionID) + return err == nil && session != nil && session.ListenerID == l.rec.ID +} + // checkImplantToken 校验 X-Implant-Token header(恒定时间比较防止时序攻击) func (l *HTTPBeaconListener) checkImplantToken(r *http.Request) bool { got := r.Header.Get("X-Implant-Token") diff --git a/internal/c2/listener_http_test.go b/internal/c2/listener_http_test.go index 58f1d9d1..0e572a7d 100644 --- a/internal/c2/listener_http_test.go +++ b/internal/c2/listener_http_test.go @@ -230,8 +230,11 @@ func TestHTTPBeaconListener_HandleFileServe(t *testing.T) { } func TestHTTPBeaconListener_HandleUploadConfinesTaskID(t *testing.T) { - tmp := t.TempDir() - store := filepath.Join(tmp, "c2store") + m, db := terminalTestManager(t) + store := m.StorageDir() + if err := db.CreateC2Task(&database.C2Task{ID: "t_safe123", SessionID: "session", TaskType: "download", Status: "sent", CreatedAt: time.Now()}); err != nil { + t.Fatal(err) + } keyB64, err := GenerateAESKey() if err != nil { t.Fatal(err) @@ -239,10 +242,11 @@ func TestHTTPBeaconListener_HandleUploadConfinesTaskID(t *testing.T) { token := "test-implant-token-upload" l := &HTTPBeaconListener{ rec: &database.C2Listener{ + ID: "listener", EncryptionKey: keyB64, ImplantToken: token, }, - manager: NewManager(nil, zap.NewNop(), store), + manager: m, logger: zap.NewNop(), }