
POC para CVE-2026-39816 que permite a usuarios de NiFi sin permisos de ejecución de código ejecutar scripts arbitrarios
Demostración de prueba de concepto para CVE-2026-39816, una omisión
del permiso EXECUTE_CODE en Apache NiFi 2.8.0 que permite a un
diseñador de flujos al que se le han denegado deliberadamente los
privilegios de ejecución de código ejecutar Groovy arbitrario
en la JVM de NiFi a través del procesador ExecuteGraphQuery del
graph bundle.
@Restricted faltante en TinkerpopClientService (CVE-2026-39816, CWE-95)NiFi restringe los procesadores que ejecutan código detrás del permiso
EXECUTE_CODE mediante la anotación
@Restricted(requiredPermission = RequiredPermission.EXECUTE_CODE).
Los 16 componentes de scripting dedicados en NiFi —
ExecuteScript, InvokeScriptedProcessor, ExecuteGroovyScript,
ScriptedTransformRecord, etc. — llevan esta anotación, por lo que un
diseñador de flujos debe tener el permiso explícito EXECUTE_CODE
antes de que la capa de autorización le permita crear o configurar
estos procesadores.
El graph bundle (nifi-graph-nar + nifi-other-graph-services-nar)
incluye una tercera vía de ejecución de código que carece de esta
anotación:
// nifi-extension-bundles/nifi-graph-bundle/nifi-other-graph-services/
// src/main/java/org/apache/nifi/graph/TinkerpopClientService.java:451
protected Map<String, String> bytecodeSubmission(
String s, Map<String, Object> map, GraphQueryResultCallback cb) {
...
compiled = groovyShell.parse(s); // línea 463 — compila la cadena del atacante
compiledCode.put(s, compiled);
...
compiled.setBinding(bindings);
Object result = compiled.run(); // línea 477 — ejecuta en la JVM de NiFi
El parámetro s es la cadena de consulta de la propiedad "Graph Query"
del procesador ExecuteGraphQuery, pasada sin cambios. No hay
saneamiento, ni lista blanca, ni sandbox — cualquier Groovy válido se
compila y ejecuta con todos los privilegios del proceso de NiFi.
Ni TinkerpopClientService ni los procesadores ExecuteGraphQuery /
ExecuteGraphQueryRecord que lo utilizan llevan la anotación
@Restricted. Por lo tanto, la capa de autorización de NiFi los trata
como componentes ordinarios, y un diseñador de flujos al que se le ha
denegado explícitamente EXECUTE_CODE aún puede crearlos.
Un servidor NiFi 2.8.0 es explotable cuando ambas de las siguientes condiciones son verdaderas:
El graph bundle opcional está instalado. Específicamente el
NAR nifi-other-graph-services-nar — este es el bundle que incluye
TinkerpopClientService. Los servidores sin este NAR no se ven
afectados.
Al menos un usuario tiene permisos de diseñador de flujos
(read/write en un grupo de procesos y en /controller) sin
EXECUTE_CODE. Esta es una forma de política común: una
organización quiere que los usuarios puedan construir y modificar
pipelines, pero no ejecutar código arbitrario. El permiso
EXECUTE_CODE es precisamente el límite de seguridad que esa
política pretende hacer cumplir.
Si ambas condiciones se cumplen, el usuario puede configurar un
TinkerpopClientService en modo "ByteCode Submission", apuntarlo a
cualquier servidor Gremlin alcanzable (el servidor Gremlin solo se usa
para la inicialización del servicio — la ejecución ocurre localmente),
y crear un procesador ExecuteGraphQuery con código Groovy en la
propiedad "Graph Query". Iniciar el procesador compila y ejecuta el
Groovy en la JVM de NiFi.
Ejecución de código arbitrario como la cuenta de servicio de NiFi. Desde ahí:
flow.json.gz, colocando un
NAR en el directorio de autocarga, o modificando un servicio de
control.El exploit omite el permiso EXECUTE_CODE, por lo que funciona incluso
cuando el operador ha eliminado explícitamente los derechos de
ejecución de código del usuario — exactamente el escenario que el
permiso existe para prevenir.
Este repositorio incluye un POC, que cubre el desencadenante más
directo: Groovy controlado por el atacante colocado directamente en la
propiedad del procesador Graph Query. Sin conexión ascendente, sin
Expression Language, sin necesidad de contenido FlowFile — solo el
procesador ejecutándose en su propio temporizador.
Existen dos vías de código adicionales (contenido del cuerpo del
FlowFile usado como consulta cuando Graph Query está vacío, e
interpolación de Expression Language de atributos FlowFile influenciados
por el atacante en la plantilla de consulta). No se demuestran aquí
porque alcanzan el mismo destino con condiciones previas adicionales;
la vía directa es suficiente para probar la omisión de EXECUTE_CODE.
setup/ — Entorno Docker Compose. Levanta una instancia de
NiFi 2.8.0 (imagen oficial apache/nifi:2.8.0) con los NAR del graph
bundle autocargados, un servidor LDAP que proporciona dos usuarios de
prueba (admin y flow_designer), y un servidor Gremlin para que
TinkerpopClientService apunte a él. setup.sh inicializa las
políticas para que flow_designer tenga derechos de edición de
flujos pero no EXECUTE_CODE, y luego verifica la configuración.
pocs/flow_designer_groovy_rce.py — Exploit autocontenido.
Se autentica como flow_designer, demuestra que ExecuteScript
está denegado (la barrera de EXECUTE_CODE funciona), luego crea un
+ con un payload Groovy
que lanza un reverse shell de bash a un listener que el POC inicia
localmente. Una vez establecida la conexión, el POC mejora el shell
a un bash completamente interactivo con soporte PTY mediante
util-linux , con puente stdio en modo raw y redimensionamiento
de ventana en vivo — el operador obtiene una terminal real dentro del
contenedor de NiFi.
Requisitos previos: Docker, Python 3.10+, y uv.
cd setup
./setup.sh
La primera ejecución descarga los NAR del graph bundle desde Maven Central y extrae las imágenes de Docker (~2-3 min). Cuando el setup se completa, imprime la invocación del POC lista para pegar.
Ejecutar el POC:
uv run --no-project --with requests \
pocs/flow_designer_groovy_rce.py \
--base-url https://localhost:8443 \
--username flow_designer \
--password 'flowDesigner123!' \
--gremlin-host gremlin-server
El POC lleva al operador a una sesión bash interactiva dentro del
contenedor de NiFi ejecutándose como la cuenta de servicio nifi.
Escriba exit o presione Ctrl-D para desconectarse — el POC luego
limpiará el procesador y el servicio de control que creó.
En Docker para Linux (donde host.docker.internal no se resuelve por
defecto), pase --shell-host <host-bridge-ip> o agregue
extra_hosts: ["host.docker.internal:host-gateway"] al servicio nifi
en setup/docker-compose.yml.
Desmontaje:
cd setup
./teardown.sh
TinkerpopClientServiceExecuteGraphQueryscript