Submit #992815: hiyouga LlamaFactory <= 0.9.5 Server-Side Request Forgery (CWE-918)info

Titlehiyouga LlamaFactory <= 0.9.5 Server-Side Request Forgery (CWE-918)
Description--- tags: - bug-bounty - llm - ssrf - python - fastapi alvo: hiyouga/LlamaFactory servico: FastAPI /v1/chat/completions cve: CVE pendente — bypass da correção de CVE-2025-61784 nivel: alto status: confirmado-localmente-nao-reportado data: 2026-09-20 commit: 97b32d3133b501432141a82949d5c7bc4d94f23a --- # LlamaFactory — bypass de SSRF por redirecionamento HTTP > [!warning] > Pesquisa feita exclusivamente contra uma cópia local do código. Nenhuma instância pública do LlamaFactory foi testada. O achado ainda não foi enviado aos mantenedores. Artefatos criados: [[Artefatos/poc_redirect_ssrf.py|PoC local seguro]]. ## Como usar este documento Este documento registra cronologicamente a análise do LlamaFactory, a tentativa que falhou por dependência ausente e a prova que confirmou o acesso a loopback. Ele também contém uma versão curta adequada para submissão privada ao mantenedor. ## Índice - [[#1. Como pensar sobre a falha]] - [[#2. Escopo e pré-requisitos]] - [[#3. Seleção do snapshot]] - [[#4. Busca inicial por sinks]] - [[#5. Rastreamento da entrada até o request HTTP]] - [[#6. Análise do filtro SSRF]] - [[#7. Primeira tentativa e falha]] - [[#8. Reprodução vencedora]] - [[#9. Alcance pela API e autenticação]] - [[#10. Impacto e severidade]] - [[#11. Causa-raiz]] - [[#12. Correção recomendada]] - [[#13. Relatório para divulgação privada]] - [[#14. Burp Suite]] - [[#15. Troubleshooting]] - [[#16. Cadeia resumida]] - [[#17. Checklist]] - [[#18. Glossário]] ## 1. Como pensar sobre a falha SSRF ocorre quando um servidor aceita uma URL controlada pelo usuário e faz a requisição em nome dele. Isso é especialmente perigoso porque o servidor pode alcançar destinos invisíveis ao usuário externo, como `127.0.0.1`, painéis internos e serviços de metadata. O filtro do LlamaFactory age como um porteiro que examina apenas o primeiro endereço de uma viagem. A URL inicial é pública, então recebe autorização. Entretanto, o servidor público responde com uma ordem de redirecionamento para um endereço interno. O cliente HTTP obedece à ordem sem consultar o porteiro novamente. ```text Cliente -> POST /v1/chat/completions com URL pública -> check_ssrf_url(URL pública): aceita -> requests.get(URL pública) -> HTTP 302 Location: http://127.0.0.1:18765/ssrf-proof -> requests segue o redirect automaticamente -> serviço de loopback é alcançado ``` Pergunta ofensiva: **a validação acompanha cada salto da requisição ou somente o primeiro URL?** Hipótese: como `requests.get()` segue redirects por padrão e o código não usa `allow_redirects=False`, um destino público pode redirecionar para rede privada e contornar `check_ssrf_url()`. ## 2. Escopo e pré-requisitos ### Escopo - Repositório público: `https://github.com/hiyouga/LlamaFactory` - Cópia local: `/root/LlamaFactory` - Commit: `97b32d3133b501432141a82949d5c7bc4d94f23a` - Teste interno: `127.0.0.1:18765`, controlado pelo pesquisador - Redirecionador: `https://httpbin.org/redirect-to` - Fora do escopo: instâncias de terceiros ### Ferramentas efetivamente utilizadas | Ferramenta | Finalidade | |---|---| | `git` | Clonar e identificar exatamente o snapshot analisado | | `grep` | Procurar sinks perigosos porque `rg` não estava instalado | | `sed` e `nl` | Ler o código e preservar números de linha | | Python 3 | Executar o validador real e hospedar o serviço controlado | | `requests` | Reproduzir o mesmo comportamento HTTP usado pela aplicação | Não foi necessário um IP de atacante remoto. O teste ocorreu inteiramente no host local, usando loopback. ## 3. Seleção do snapshot ### Hipótese/Objetivo Fixar o código atual para que o resultado possa ser reproduzido mesmo depois de alterações no branch principal. ### O que fazer ```bash git clone --depth 1 https://github.com/hiyouga/LlamaFactory.git /root/LlamaFactory git -C /root/LlamaFactory rev-parse HEAD ``` ### Como o comando foi montado | Parte | Significado e motivo | |---|---| | `git clone` | Cria uma cópia local do repositório | | `--depth 1` | Baixa apenas o snapshot mais recente, suficiente para a análise inicial | | URL GitHub | Origem pública examinada | | `/root/LlamaFactory` | Caminho explícito da cópia local | | `git -C` | Executa o próximo subcomando dentro do diretório indicado | | `rev-parse HEAD` | Mostra o hash exato do commit atual | ### Saída obtida ```text Cloning into '/root/LlamaFactory'... 97b32d3133b501432141a82949d5c7bc4d94f23a ``` ### O que observar O hash torna a análise imutável e verificável. ### Decisão tomada Usar esse commit como referência em todos os achados. ## 4. Busca inicial por sinks ### Hipótese/Objetivo Aplicações LLM frequentemente processam modelos, mídia e arquivos externos. Procurar chamadas de rede, desserialização e subprocessos é um teste barato que reduz rapidamente a superfície. ### O que fazer A primeira busca tentou usar `rg`, mas a ferramenta não estava instalada. A busca foi então repetida com `grep`: ```bash grep -RInE --include='*.py' 'subprocess|Popen\(|os\.system|shell[[:space:]]*=[[:space:]]*True|torch\.load|pickle\.load|yaml\.(load|unsafe_load)|requests\.(get|post)|httpx\.|urlopen|urllib\.request|extractall|shutil\.unpack_archive' src scripts | head -n 300 ``` ### Como o comando foi montado | Parte | Significado e motivo | |---|---| | `grep` | Pesquisa texto recursivamente | | `-R` | Percorre subdiretórios | | `-I` | Ignora arquivos binários | | `-n` | Mostra número da linha | | `-E` | Habilita expressão regular estendida | | `--include='*.py'` | Limita a código Python | | `\|` | Alternância da expressão regular, equivalente a “ou” | | `src scripts` | Diretórios examinados | | `|` | Envia a saída do `grep` ao próximo programa | | `head -n 300` | Limita a visualização às primeiras 300 linhas | ### Saída obtida relevante ```text src/llamafactory/api/chat.py:131: image_stream = requests.get(image_url, stream=True).raw src/llamafactory/api/chat.py:144: video_stream = requests.get(video_url, stream=True).raw src/llamafactory/api/chat.py:157: audio_stream = requests.get(audio_url, stream=True).raw ``` ### O que observar Três entradas multimodais diferentes chegam ao mesmo cliente HTTP. Isso amplia a alcançabilidade da falha. ### Decisão tomada Rastrear a validação que precede essas três chamadas. ## 5. Rastreamento da entrada até o request HTTP ### Hipótese/Objetivo Confirmar que a URL vem do corpo controlado pelo cliente e que existe um filtro imediatamente antes do sink. ### O que fazer ```bash nl -ba src/llamafactory/api/chat.py | sed -n '115,165p' ``` ### Como o comando foi montado | Parte | Significado e motivo | |---|---| | `nl -ba` | Numera todas as linhas, inclusive linhas vazias | | arquivo | Implementação do chat multimodal | | `|` | Encaminha o texto numerado ao `sed` | | `sed -n` | Não imprime automaticamente todas as linhas | | `'115,165p'` | Imprime somente o intervalo relevante | ### Saída obtida relevante ```python image_url = input_item.image_url.url check_ssrf_url(image_url) image_stream = requests.get(image_url, stream=True).raw ``` O mesmo padrão foi obtido para `video_url` e `audio_url`. ### O que observar `input_item.*_url.url` vem da mensagem enviada à API. A função valida a string e depois `requests` inicia uma nova operação de resolução/conexão. ### Decisão tomada Inspecionar se `check_ssrf_url()` vincula a validação à conexão e se revalida redirects. ## 6. Análise do filtro SSRF ### Hipótese/Objetivo Verificar três classes comuns de bypass: múltiplos registros DNS, DNS rebinding e redirecionamentos HTTP. ### O que fazer ```bash nl -ba src/llamafactory/api/common.py | sed -n '65,105p' ``` ### Como o comando foi montado Os parâmetros de `nl`, o pipe e o intervalo de `sed` têm o mesmo significado explicado na fase anterior. O in Vendor coordination Vendor contacted via GHSA-3w98-wx27-3fwr. CVE assignment requested from VulDB; no CVE known at submission time.
Source⚠️ https://github.com/hiyouga/LlamaFactory/security/advisories/GHSA-3w98-wx27-3fwr
User
 fernando viana (UID 101484)
Submission09/21/2026 02:15 (23 hours ago)
Moderation09/22/2026 00:04 (22 hours later)
StatusDuplicate
VulDB entry398842 [hiyouga LLaMA-Factory up to 0.9.5 OpenAI-compatible API Multimodal Media URL requests.get server-side request forgery]
Points0

Interested in the pricing of exploits?

See the underground prices here!