diff --git a/internal/app/c2_tools.go b/internal/app/c2_tools.go index 14853da2..6cca39a3 100644 --- a/internal/app/c2_tools.go +++ b/internal/app/c2_tools.go @@ -75,6 +75,7 @@ tcp_reverse 默认仅接受 CSB1 加密 Beacon(AES-GCM + ImplantToken)才登 "bind_host": map[string]interface{}{"type": "string", "description": "绑定地址,默认 127.0.0.1;外网监听常用 0.0.0.0"}, "callback_host": map[string]interface{}{"type": "string", "description": "可选:植入端/Payload 回连主机名(公网 IP 或域名)。写入 config_json;生成 oneliner/beacon 时优先于 bind_host。update 时传入空字符串可清除"}, "bind_port": map[string]interface{}{"type": "integer", "description": fmt.Sprintf("绑定端口(create 必填)。须 ≠ %d(当前本服务 Web/API 端口,配置 server.port)", webListenPort), "minimum": 1, "maximum": 65535}, + "project_id": map[string]interface{}{"type": "string", "description": "所属项目 ID。create 省略时默认使用当前对话绑定项目;未绑定项目的对话则创建未绑定监听器"}, "profile_id": map[string]interface{}{"type": "string", "description": "Malleable Profile ID"}, "remark": map[string]interface{}{"type": "string", "description": "备注"}, "config": map[string]interface{}{"type": "object", "description": "高级配置(beacon 路径/TLS/OPSEC 等),create/update 可用。tcp_reverse 可选 allow_legacy_shell:true 允许未加密经典 shell(默认 false)"}, @@ -116,6 +117,13 @@ tcp_reverse 默认仅接受 CSB1 加密 Beacon(AES-GCM + ImplantToken)才登 cfg = &c2.ListenerConfig{} _ = json.Unmarshal(cfgBytes, cfg) } + projectID := strings.TrimSpace(getString(params, "project_id")) + if projectID == "" { + projectID = mcpEffectiveProjectFilter(ctx, m.DB()) + if projectID == database.ProjectFilterUnbound { + projectID = "" + } + } input := c2.CreateListenerInput{ Name: getString(params, "name"), Type: getString(params, "type"), @@ -123,7 +131,7 @@ tcp_reverse 默认仅接受 CSB1 加密 Beacon(AES-GCM + ImplantToken)才登 BindPort: int(getFloat64(params, "bind_port")), ProfileID: getString(params, "profile_id"), Remark: getString(params, "remark"), - ProjectID: strings.TrimSpace(mcp.MCPProjectIDFromContext(ctx)), + ProjectID: projectID, Config: cfg, CallbackHost: getString(params, "callback_host"), } diff --git a/internal/app/c2_tools_test.go b/internal/app/c2_tools_test.go new file mode 100644 index 00000000..b24d2660 --- /dev/null +++ b/internal/app/c2_tools_test.go @@ -0,0 +1,69 @@ +package app + +import ( + "context" + "path/filepath" + "testing" + + "cyberstrike-ai/internal/authctx" + "cyberstrike-ai/internal/c2" + "cyberstrike-ai/internal/database" + "cyberstrike-ai/internal/mcp" + "cyberstrike-ai/internal/mcp/builtin" + + "go.uber.org/zap" +) + +func TestC2ListenerCreateInheritsConversationProject(t *testing.T) { + db, err := database.NewDB(filepath.Join(t.TempDir(), "c2-tools.db"), zap.NewNop()) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + user, err := db.CreateRBACUser("c2-agent", "C2 Agent", "hash", true, nil) + if err != nil { + t.Fatal(err) + } + project, err := db.CreateProject(&database.Project{Name: "engagement"}) + if err != nil { + t.Fatal(err) + } + if err := db.AssignResourceToUser(user.ID, "project", project.ID); err != nil { + t.Fatal(err) + } + conversation, err := db.CreateConversation("project chat", database.ConversationCreateMeta{ProjectID: project.ID}) + if err != nil { + t.Fatal(err) + } + + principal := authctx.NewPrincipal(user.ID, user.Username, database.RBACScopeAssigned, map[string]bool{ + "c2:read": true, "c2:write": true, + }) + ctx := authctx.WithPrincipal(mcp.WithMCPConversationID(context.Background(), conversation.ID), principal) + server := mcp.NewServer(zap.NewNop()) + server.SetToolAuthorizer(mcpToolAuthorizer(db)) + registerC2Tools(server, c2.NewManager(db, zap.NewNop(), t.TempDir()), zap.NewNop(), 8080) + + result, _, err := server.CallTool(ctx, builtin.ToolC2Listener, map[string]interface{}{ + "action": "create", + "name": "tcp-reverse-2222", + "type": "tcp_reverse", + "bind_host": "0.0.0.0", + "bind_port": 2222, + }) + if err != nil || result == nil || result.IsError { + t.Fatalf("create listener result=%#v err=%v text=%q", result, err, toolResultText(result)) + } + + listeners, err := db.ListC2Listeners() + if err != nil { + t.Fatal(err) + } + if len(listeners) != 1 { + t.Fatalf("listener count=%d, want 1", len(listeners)) + } + if listeners[0].ProjectID != project.ID { + t.Fatalf("listener project_id=%q, want %q", listeners[0].ProjectID, project.ID) + } +} diff --git a/internal/app/mcp_authorization.go b/internal/app/mcp_authorization.go index d9947c23..cd933a79 100644 --- a/internal/app/mcp_authorization.go +++ b/internal/app/mcp_authorization.go @@ -244,7 +244,20 @@ func authorizeC2Action(ctx context.Context, principal authctx.Principal, db *dat return nil } if id == "" { - if action == "create" || action == "list" { + if action == "create" { + projectID := mcpAuthorizationString(args, "project_id") + if projectID == "" { + projectID = mcpEffectiveProjectFilter(ctx, db) + if projectID == database.ProjectFilterUnbound { + projectID = "" + } + } + if projectID != "" && (db == nil || !db.UserCanAccessResource(principal.UserID, principal.ScopeFor(permission), "project", projectID)) { + return fmt.Errorf("no access to project %s", projectID) + } + return nil + } + if action == "list" { return nil } return fmt.Errorf("missing resource identifier %s", argument)