FlawOpen

flawopen.com/Incidents/microsoft-autogen-studio-autojack-agent-rce-june-2026

● Microsoft AutoGen project · CWE-1385 / CWE-306 / CWE-78 · 2026-06-18 · 高危
安全研究 · FlawOpen

AutoJack:AutoGen Studio 的 RCE 研究链

Microsoft在AutoGen Studio开发构建中发现通过本地MCP触发的RCE研究链。该问题在发布前修复,未进入PyPI软件包。[1], [2]

💡 通俗易懂的原理解析 (ELI5)

想象一个研究实验室:助手负责阅读访客留下的便条。因为助手在同一栋楼工作,它可以进入控制室。前台对这间房免除了常规身份检查,访客的便条还能指定启动哪种工具。AutoJack 把这些捷径串在一起:浏览代理显示了不可信内容,内容触达 AutoGen Studio 本机的 MCP 控制平面,调用者提供的设置可能导致子进程启动。应为控制平面启用身份验证,把启动设置移出 WebSocket URL,并将助手与开发者工作站隔离。Microsoft 称该问题在 PyPI 发布前已于上游分支修复。该研究针对的是发布前的源代码,而非受影响的客户版本。[1]

核心概念与专有名词

AutoJack
Microsoft 对 AutoGen Studio 开发构建中演示的攻击链的命名。报告称,受影响的 MCP 路由在进入已发布的 PyPI 包之前就已修复。[1], [2]
Loopback / localhost
用于访问本机服务的网络接口。它限制外部网络访问,但不会将本机进程(例如代理的浏览器)与同一主机上的其他服务隔离。[1]
WebSocket Origin 验证
服务器在 WebSocket 握手期间检查 Origin 标头。Microsoft 发现,在其测试的代理流程中,这项检查本身无法阻止渲染的网页触达本地 MCP 服务。[1], [3]
StdioServerParams
AutoGen 的 MCP stdio 配置,包含启动服务器进程所用的程序和参数。报告中的路由会从 WebSocket URL 参数解码这些设置。[1], [2], [5]
Confused deputy
攻击者诱使有权限的组件替不可信方使用自身权限。此处,浏览代理和本地 Studio 服务把网页内容连接到了主机进程启动操作。[1]

框架映射

事件演进时间线

2026-06-18 之前

Microsoft 向 MSRC 报告该行为;AutoGen 维护者通过提交 b047730 加固了上游分支。报告称受影响的 MCP WebSocket 路由尚未进入 PyPI 发布包。[1], [2]

2026-06-18

Microsoft 发布 AutoJack 研究和缓解建议。报告描述的是受控的本地演示,并非针对客户的已确认攻击。[1]

攻击执行流程分解

步骤 1

代理被要求浏览不可信内容

提示、检索到的网页或其他不可信输入使 AutoGen 浏览代理在运行 AutoGen Studio 的同一台工作站上显示攻击者控制的页面。[1]

步骤 2

网页触达本地 MCP WebSocket

在 Microsoft 测试的配置中,页面脚本可以连接到 localhost 上的 Studio MCP 端点。Origin 允许列表没有隔离本地代理浏览器渲染的代码与控制平面。[1], [3]

步骤 3

MCP 路由绕过常规身份验证中间件

受影响的中间件跳过 /api/mcp 路径,假设 WebSocket 处理程序会自行验证身份。Microsoft 报告称,该处理程序没有执行此检查。[1], [4]

步骤 4

WebSocket 查询参数控制服务器配置

开发路由从 URL 解码 server_params 并构造 StdioServerParams。随后 MCP stdio 传输会使用 command 和 args 启动子进程。[1], [2], [5]

步骤 5

攻击链抵达主机账户

本地 Studio 进程以自身操作系统权限启动配置的子进程。Microsoft 在受控本地 PoC 中演示了该过程,并称受影响路由没有包含在 PyPI 发布包中。[1], [2]

源代码对比:漏洞与安全实现

易受攻击的示例会解析调用者控制的启动设置,但特意不启动进程。上游修复将设置移出 WebSocket 查询,存入服务器端的一次性会话状态,并恢复 MCP 路由的身份验证。此防御模式还增加了过期时间、大小限制、并发锁和可执行程序允许列表;请将示例名称替换为明确批准的 MCP 二进制文件。它不能替代 Origin 验证或操作系统隔离。[1], [2]

存在漏洞的实现
import base64
import json
from dataclasses import dataclass


@dataclass(frozen=True)
class StdioServerParams:
    command: str
    args: tuple[str, ...]


def params_from_websocket_query(query: dict[str, str]) -> StdioServerParams:
    # 开发路由信任 URL 中提供的配置。
    encoded = query["server_params"]
    decoded = base64.b64decode(encoded)
    return StdioServerParams(**json.loads(decoded))
加固后的安全修复
from dataclasses import dataclass
from threading import Lock
from time import monotonic
from uuid import uuid4


@dataclass(frozen=True)
class StdioServerParams:
    command: str
    args: tuple[str, ...]


SESSION_TTL = 30.0
MAX_PENDING = 256
ALLOWED_COMMANDS = {"approved-mcp-server"}
pending: dict[str, tuple[float, StdioServerParams]] = {}
pending_lock = Lock()


def register_params(params: StdioServerParams, authenticated: bool) -> str:
    # authenticated 必须来自经过验证的中间件,不能来自请求数据。
    if not authenticated:
        raise PermissionError("MCP control-plane authentication required")
    if params.command not in ALLOWED_COMMANDS:
        raise ValueError("unapproved MCP server executable")
    now = monotonic()
    with pending_lock:
        for key, (expires, _) in list(pending.items()):
            if expires <= now:
                pending.pop(key, None)
        if len(pending) >= MAX_PENDING:
            raise RuntimeError("pending MCP session limit reached")
        session_id = str(uuid4())
        pending[session_id] = (now + SESSION_TTL, params)
        return session_id


def consume_params(session_id: str, authenticated: bool) -> StdioServerParams:
    if not authenticated:
        raise PermissionError("MCP control-plane authentication required")
    with pending_lock:
        entry = pending.pop(session_id, None)
    if entry is None or entry[0] <= monotonic():
        raise ValueError("unknown or expired MCP session")
    return entry[1]

工程与系统安全加固清单

参考来源