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 출시 전에 upstream 브랜치에서 수정했다고 밝혔습니다. 이 보고는 출시 전 소스 코드에 관한 것이며 고객에게 배포된 영향 버전에 관한 것이 아닙니다.[1]

핵심 개념 및 용어

AutoJack
AutoGen Studio 개발 빌드에서 시연된 공격 체인에 Microsoft가 붙인 이름입니다. 보고서에 따르면 영향을 받은 MCP 경로는 공개 PyPI 패키지에 포함되기 전에 수정됐습니다.[1], [2]
Loopback / localhost
같은 호스트의 서비스에 접근하는 네트워크 인터페이스입니다. 외부 네트워크 접근은 제한하지만 에이전트 브라우저 같은 로컬 프로세스를 같은 호스트의 다른 서비스에서 격리하지는 않습니다.[1]
WebSocket Origin 검증
WebSocket handshake에서 서버가 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에서 upstream 브랜치를 강화했습니다. 보고서에 따르면 영향을 받은 MCP WebSocket 경로는 PyPI 패키지로 배포되지 않았습니다.[1], [2]

2026-06-18

Microsoft가 AutoJack 연구와 완화 지침을 공개했습니다. 문서에는 통제된 로컬 시연이 설명되어 있으며 고객 대상 공격이 확인됐다는 내용은 없습니다.[1]

단계별 공격 실행 흐름

단계 1

에이전트가 신뢰할 수 없는 콘텐츠를 탐색

프롬프트, 검색된 페이지 또는 다른 신뢰할 수 없는 입력으로 AutoGen 브라우징 에이전트가 AutoGen Studio와 같은 워크스테이션에서 공격자가 제어하는 페이지를 표시합니다.[1]

단계 2

표시된 페이지가 로컬 MCP WebSocket에 접근

Microsoft가 테스트한 구성에서는 페이지 스크립트가 localhost의 Studio MCP endpoint에 연결할 수 있었습니다. Origin 허용 목록은 로컬 에이전트 브라우저가 렌더링한 코드와 제어 영역을 격리하지 못했습니다.[1], [3]

단계 3

MCP 경로가 일반 인증 미들웨어를 우회

영향을 받은 미들웨어는 WebSocket handler가 직접 인증할 것이라고 가정해 /api/mcp 경로를 건너뛰었습니다. Microsoft에 따르면 handler에는 해당 검사가 없었습니다.[1], [4]

단계 4

WebSocket 쿼리로 서버 매개변수를 지정

개발 경로는 URL의 server_params를 StdioServerParams로 디코딩했습니다. 이후 MCP stdio 전송이 command와 args를 사용해 자식 프로세스를 시작했습니다.[1], [2], [5]

단계 5

체인이 호스트 계정에 도달

로컬 Studio 프로세스가 자신의 운영체제 권한으로 설정된 자식 프로세스를 시작합니다. Microsoft는 이를 통제된 로컬 PoC로 보여 줬으며 해당 경로는 공개 PyPI 패키지에 없었다고 밝혔습니다.[1], [2]

소스 코드 비교: 취약한 구현 vs 보안 패치

취약한 예시는 호출자가 제어하는 시작 설정을 파싱하지만 의도적으로 프로세스를 실행하지 않습니다. upstream 수정은 설정을 WebSocket 쿼리에서 제거해 서버 측 일회용 세션 상태로 옮기고 MCP 경로의 인증을 다시 적용했습니다. 이 방어 패턴은 만료, 크기 제한, 동시성 잠금, 실행 파일 허용 목록도 추가합니다. 예시 이름을 승인된 MCP 바이너리로 바꾸세요. Origin 검증이나 OS 격리를 대체하지는 않습니다.[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]

엔지니어링 및 시스템 보안 강화 체크리스트

출처