0x0 背景介绍

WeKnora是一款基于大语言模型(LLM)的框架,专为深度文档理解与语义检索而设计。在0.2.5之前的版本中,存在一个命令注入漏洞,允许经过身份验证的用户向MCP标准输入输出设置中注入stdio_config.command/args参数,导致服务器使用这些注入值执行子进程。该漏洞已在0.2.5版本中修复。

0x1 环境搭建

1.1、Ubuntu24+Docker搭建配置

#1、创建专属项目
mkdir WeKnora214 &&  cd WeKnora214/
#2、拉取WeKnora环境
git clone https://github.com/Tencent/WeKnora.git
cd WeKnora
#2.1使用指定版本
git checkout v0.2.4
#2.2检查版本
git describe --tags
输出:v0.2.4

#3、编辑yml,原始中均为latest,需改为0.2.4版本
cat docker-compose.yml | grep wechat
    image: wechatopenai/weknora-ui:v0.2.4
    image: wechatopenai/weknora-app:v0.2.4
    image: wechatopenai/weknora-docreader:v0.2.4
#4、创建env
# ========== 基础配置 ==========
GIN_MODE=release
DISABLE_REGISTRATION=false

# ========== 数据库 ==========
DB_DRIVER=postgres
DB_USER=weknora
DB_PASSWORD=WeKnora@2026!SecurePass
DB_NAME=weknora

# ========== 向量与存储 ==========
RETRIEVE_DRIVER=postgres
STORAGE_TYPE=local
LOCAL_STORAGE_BASE_DIR=/data/files
AUTO_RECOVER_DIRTY=true

# ========== Redis 流管理 ==========
STREAM_MANAGER_TYPE=redis
REDIS_PASSWORD=Redis@2026!StreamPass
REDIS_DB=0
REDIS_PREFIX=weknora:

# ========== 安全密钥(已预生成,建议保留或自行替换)==========
JWT_SECRET=9f3b1a8c5e7d2f6a4c0b9e1d8f3a7c5b2e6d9f0a1c4b8e3d7f2a5c9b6e0d4f1a
TENANT_AES_KEY=k3x9LmQpR7vYzN2sA5tG8wE1uH4jC6bF

# ========== Ollama ==========
OLLAMA_BASE_URL=http://host.docker.internal:11434

# ========== 并发控制 ==========
CONCURRENCY_POOL_SIZE=3

# ========== 网络与端口 ==========
APP_PORT=8080
FRONTEND_PORT=80
DOCREADER_PORT=50051

# ========== 腾讯云 COS(未启用,保持默认)==========
COS_ENABLE_OLD_DOMAIN=true

# ========== 图谱(禁用)==========
ENABLE_GRAPH_RAG=false
NEO4J_ENABLE=false

# ========== MinIO(如需启用,在 docker-compose 中使用 --profile minio)==========
MINIO_ACCESS_KEY_ID=minioadmin
MINIO_SECRET_ACCESS_KEY=miniostrongpassword2026

# ========== 其他 ==========
APK_MIRROR_ARG=mirrors.tencent.com

#4、拉取镜像
docker compose --profile minio up -d
#5、查看启动情况
docker ps
#6、观察日志
docker logs -f WeKnora-app
#7、无误后web访问127.0.0.1进行注册登录

成功页面

0x2 漏洞复现

2.1、Python检查

https://github.com/Kai-One001/cve-/blob/main/WeKnora_CVE-2026-22688.py

python检查

2.2、手动复现步骤

  • 标准创建流程
  • 创建流程
    创建恶意流程
  • 访问test激活,也可以web中直接测试(用户->MCP服务)
    web测试
  • 验证存在
    创建成功

2.3、复现流量特征 (PCAP)

  • 获取TOKEN和创建流程
    登录成功+创建成功
  • 激活流程
    激活MCP
  • 验证成功
    容器成功执行

0x3 漏洞原理分析

3.1、入口文件分析

PS:照例根据公开的路由尝试再IntelliJ IDEA中查询文件,排除MD后定位到WeKnora-0.2.4\WeKnora-0.2.4\frontend\src\api\mcp-service.ts

import { get, post, put, del } from '@/utils/request'

export interface MCPService {
  id: string
  tenant_id?: number
  name: string
  description: string
  enabled: boolean
  transport_type: 'sse' | 'http-streamable' | 'stdio'
  url?: string // Optional: required for SSE/HTTP Streamable
  headers?: Record<string, string>
  auth_config?: {
    api_key?: string
    token?: string
    custom_headers?: Record<string, string>
  }
  advanced_config?: {
    timeout?: number
    retry_count?: number
    retry_delay?: number
  }
  stdio_config?: {
    command: 'uvx' | 'npx' // Command: uvx or npx
    args: string[] // Command arguments array
  }
  env_vars?: Record<string, string> // Environment variables for stdio transport
  created_at?: string
  updated_at?: string
}

export interface MCPTool {
  name: string
  description: string
  inputSchema: Record<string, any>
}

export interface MCPResource {
  uri: string
  name: string
  description?: string
  mimeType?: string
}

export interface MCPTestResult {
  success: boolean
  message?: string
  tools?: MCPTool[]
  resources?: MCPResource[]
}

// List all MCP services
export async function listMCPServices(): Promise<MCPService[]> {
  const response: any = await get('/api/v1/mcp-services')
  return response.data || []
}

// Get a single MCP service by ID
export async function getMCPService(id: string): Promise<MCPService> {
  const response: any = await get(`/api/v1/mcp-services/${id}`)
  return response.data
}

// Create a new MCP service
export async function createMCPService(data: Partial<MCPService>): Promise<MCPService> {
  const response: any = await post('/api/v1/mcp-services', data)
  return response.data
}

// Update an existing MCP service
export async function updateMCPService(id: string, data: Partial<MCPService>): Promise<MCPService> {
  const response: any = await put(`/api/v1/mcp-services/${id}`, data)
  return response.data
}

// Delete an MCP service
export async function deleteMCPService(id: string): Promise<void> {
  await del(`/api/v1/mcp-services/${id}`)
}

// Test MCP service connection
export async function testMCPService(id: string): Promise<MCPTestResult> {
  const response: any = await post(`/api/v1/mcp-services/${id}/test`, {})
  // 后端返回格式: { success: true, data: MCPTestResult }
  // response interceptor 已经返回了 data,所以 response 就是 { success: true, data: {...} }
  if (response && response.data) {
    return response.data
  }
  // 如果格式不对,尝试直接返回 response(可能是直接返回的数据)
  return response
}

// Get tools from an MCP service
export async function getMCPServiceTools(id: string): Promise<MCPTool[]> {
  const response: any = await get(`/api/v1/mcp-services/${id}/tools`)
  return response.data || []
}

// Get resources from an MCP service
export async function getMCPServiceResources(id: string): Promise<MCPResource[]> {
  const response: any = await get(`/api/v1/mcp-services/${id}/resources`)
  return response.data || []
}
  • 虽然前端TypeScript类型声明限制 command 为 'uvx' | 'npx',但后端Go服务完全未做校验,且HTTP请求是原始JSON,攻击者可直接发送请求
  • 参数是Partial,说明可部分更新updateMCPService(id: string, data: Partial<MCPService>)
  • 所有相关API接口
动作(Action) HTTP 方法 URL 示例说明
createMCPService POST /api/v1/mcp-services/ 创建新 MCP 服务(支持 stdio_config.command 任意值)
updateMCPService PUT /api/v1/mcp-services/{id} 更新现有 MCP 服务(可单独修改 command 字段)
testMCPService POST /api/v1/mcp-services/{id}/test 测试 MCP 服务连接并获取工具列表
getMCPServiceTools GET /api/v1/mcp-services/{id}/tools 获取该 MCP 服务声明的工具列表(需服务已启用且连接正常)

3.2、路由注册

	// 需要认证的API路由
	v1 := r.Group("/api/v1")
	{
		RegisterAuthRoutes(v1, params.AuthHandler)
		RegisterTenantRoutes(v1, params.TenantHandler)
		RegisterKnowledgeBaseRoutes(v1, params.KBHandler)
		RegisterKnowledgeTagRoutes(v1, params.TagHandler)
		RegisterKnowledgeRoutes(v1, params.KnowledgeHandler)
		RegisterFAQRoutes(v1, params.FAQHandler)
		RegisterChunkRoutes(v1, params.ChunkHandler)
		RegisterSessionRoutes(v1, params.SessionHandler)
		RegisterChatRoutes(v1, params.SessionHandler)
		RegisterMessageRoutes(v1, params.MessageHandler)
		RegisterModelRoutes(v1, params.ModelHandler)
		RegisterEvaluationRoutes(v1, params.EvaluationHandler)
		RegisterInitializationRoutes(v1, params.InitializationHandler)
		RegisterSystemRoutes(v1, params.SystemHandler)
		RegisterMCPServiceRoutes(v1, params.MCPServiceHandler)
		RegisterWebSearchRoutes(v1, params.WebSearchHandler)
	}

	return r
}

/ RegisterMCPServiceRoutes registers MCP service routes
func RegisterMCPServiceRoutes(r *gin.RouterGroup, handler *handler.MCPServiceHandler) {
	mcpServices := r.Group("/mcp-services")
	{
		// Create MCP service
		mcpServices.POST("", handler.CreateMCPService)
		// List MCP services
		mcpServices.GET("", handler.ListMCPServices)
		// Get MCP service by ID
		mcpServices.GET("/:id", handler.GetMCPService)
		// Update MCP service
		mcpServices.PUT("/:id", handler.UpdateMCPService)
		// Delete MCP service
		mcpServices.DELETE("/:id", handler.DeleteMCPService)
		// Test MCP service connection
		mcpServices.POST("/:id/test", handler.TestMCPService)
		// Get MCP service tools
		mcpServices.GET("/:id/tools", handler.GetMCPServiceTools)
		// Get MCP service resources
		mcpServices.GET("/:id/resources", handler.GetMCPServiceResources)
	}
}

  • 所有MCP服务操作都自动加前缀/api/v1/mcp-services/... 路由暴露
POST /api/v1/mcp-services 请求 → 被 Gin 路由分发 → 调用 handler.MCPServiceHandler.CreateMCPService 方法

在IDE查找CreateMCPService,发现有interfaces\mcp_service.go、handler\mcp_service.go、service\mcp_service.go、mcp-service.ts、McpServiceDialog.vue文件都有,于是查询了信息

文件路径 语言/类型 作用
interfaces/mcp_service.go Go (接口定义) 定义 MCPService 接口,包含 CreateMCPService 方法签名
handler/mcp_service.go Go (HTTP Handler) 实现 Gin 路由对应的业务入口,接收 HTTP 请求
service/mcp_service.go Go (业务逻辑) 实现 MCPService 接口,处理数据校验、存储等
mcp-service.ts TypeScript 前端 API 封装或类型定义
McpServiceDialog.vue Vue (前端组件) 用户在界面上创建 MCP 服务的表单

3.3、HTTP Handler 层

  • 创建服务handler/mcp_service.go
// CreateMCPService godoc
// @Summary      创建MCP服务
// @Description  创建新的MCP服务配置
// @Tags         MCP服务
// @Accept       json
// @Produce      json
// @Param        request  body      types.MCPService  true  "MCP服务配置"
// @Success      200      {object}  map[string]interface{}  "创建的MCP服务"
// @Failure      400      {object}  errors.AppError         "请求参数错误"
// @Security     Bearer
// @Router       /mcp-services [post]
func (h *MCPServiceHandler) CreateMCPService(c *gin.Context) {
	ctx := c.Request.Context()

	var service types.MCPService
	if err := c.ShouldBindJSON(&service); err != nil {//直接反序列化用户输入
		logger.Error(ctx, "Failed to parse MCP service request", err)
		c.Error(errors.NewBadRequestError(err.Error()))
		return
	}

	tenantID := c.GetUint64(types.TenantIDContextKey.String())
	if tenantID == 0 {
		logger.Error(ctx, "Tenant ID is empty")
		c.Error(errors.NewBadRequestError("Tenant ID cannot be empty"))
		return
	}
	service.TenantID = tenantID

	if err := h.mcpServiceService.CreateMCPService(ctx, &service); err != nil {
		logger.ErrorWithFields(ctx, err, map[string]interface{}{"service_name": secutils.SanitizeForLog(service.Name)})
		c.Error(errors.NewInternalServerError("Failed to create MCP service: " + err.Error()))
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"data":    service,
	})
}

  • CreateMCPService直接绑定用户输入到types.MCPService,如果types.MCPService包含 StdioConfig.Command字段 → 则用户完全控制该字段
  • 更新服务也是同样,允许任意设置command
func (h *MCPServiceHandler) UpdateMCPService(c *gin.Context) {
。。。。。
	if stdioConfig, ok := updateData["stdio_config"].(map[string]interface{}); ok {
		config := &types.MCPStdioConfig{}
		if command, ok := stdioConfig["command"].(string); ok {
			config.Command = command //这里
		}
		if args, ok := stdioConfig["args"].([]interface{}); ok {
			config.Args = make([]string, len(args))
			for i, arg := range args {
				if str, ok := arg.(string); ok {
					config.Args[i] = str
				}
			}
		}
		service.StdioConfig = config
	}
  • 不仅创建时可控,更新时也可修改command参数

  • 攻击者可先创建一个合法服务,再更新为恶意命令

  • TestMCPService会触发命令执行

result, err := h.mcpServiceService.TestMCPService(ctx, tenantID, serviceID)

结合在interfaces/mcp_service.go中的定义

TestMCPService(ctx context.Context, tenantID uint64, id string) (*types.MCPTestResult, error)

“测试连接” 对于stdio类型的MCP服务,启动子进程执行command + args,并尝试通信

3.4、接口定义层(interfaces)

//主要是定义了两类接口
type MCPServiceRepository interface {
    Create(ctx context.Context, service *types.MCPService) error
    GetByID(ctx context.Context, tenantID uint64, id string) (*types.MCPService, error)
    // ... 其他 CRUD 方法
}
  • 数据访问层DAO/Repo的接口
  • 负责与数据库交互
//MCPServiceService 接口
type MCPServiceService interface {
    CreateMCPService(ctx context.Context, service *types.MCPService) error
    GetMCPServiceByID(...)
    TestMCPService(...)
    GetMCPServiceTools(...)
    // ...
}

3.5、业务逻辑层(核心漏洞点)

  • service/mcp_service.go文件
// CreateMCPService creates a new MCP service
func (s *mcpServiceService) CreateMCPService(ctx context.Context, service *types.MCPService) error {
	// Set default advanced config if not provided
	if service.AdvancedConfig == nil {
		service.AdvancedConfig = types.GetDefaultAdvancedConfig()
	}

	// Set timestamps
	service.CreatedAt = time.Now()
	service.UpdatedAt = time.Now()

	if err := s.mcpServiceRepo.Create(ctx, service); err != nil {
		logger.GetLogger(ctx).Errorf("Failed to create MCP service: %v", err)
		return fmt.Errorf("failed to create MCP service: %w", err)
	}

	return nil
}
  • CreateMCPService —— 无任何校验,原样存储

  • 完全信任传入的 *types.MCPService

  • 未对service.StdioConfig.Command做任何检查

  • 直接调用repo.Create()存入数据库

  • TestMCPService 触发命令执行

  • mcp.NewMCPClient stdio 模式下会启动子进程

client, err := mcp.NewMCPClient(&mcp.ClientConfig{Service: service})
client.Connect(testCtx) // ← 在 stdio 模式下会 exec.Command(service.StdioConfig.Command, ...)
  • UpdateMCPService允许动态覆盖StdioConfig
if service.StdioConfig != nil {
    existing.StdioConfig = service.StdioConfig // ← 完全覆盖!
}
其它姿势:
先创建一个合法服务
再通过 PUT /mcp-services/{id} 更新 stdio_config.command 为恶意 payload
点击“测试”触发 RCE

0x4 修复建议

修复方案

  1. 升级到最新版本:建议受影响的用户升级至0.2.5或更高:WeKnora
  2. 临时防护措施:
    权限最小化:限制普通用户对MCP模块配置项的修改权限,管理员用户进行强密码处理
    启用 WAF/IPS 规则:监控异常的 system()、subprocess.run() 等调用行为,及时发现潜在攻击。
    加强输入验证:涉及stdio_config.commandargs参数,实施严格的白名单校验与转义处理

免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐