
POC pour CVE-2026-39816 qui permet aux utilisateurs NiFi sans permissions d'exécution de code d'exécuter des scripts arbitraires
Démonstration de preuve de concept pour CVE-2026-39816, un contournement de la permission EXECUTE_CODE
dans Apache NiFi 2.8.0 qui permet à un concepteur de flux ayant été
délibérément privé des privilèges d'exécution de code d'exécuter du Groovy arbitraire
dans la JVM NiFi via le processeur ExecuteGraphQuery du bundle graph.
@Restricted manquante sur TinkerpopClientService (CVE-2026-39816, CWE-95)NiFi restreint les processeurs exécutant du code derrière la permission EXECUTE_CODE
via l'annotation @Restricted(requiredPermission = RequiredPermission.EXECUTE_CODE).
Les 16 composants de script dédiés de NiFi —
ExecuteScript, InvokeScriptedProcessor, ExecuteGroovyScript,
ScriptedTransformRecord, etc. — portent tous cette annotation, donc un
concepteur de flux doit se voir accorder la permission explicite EXECUTE_CODE avant
que la couche d'autorisation ne lui permette de créer ou configurer ces
processeurs.
Le bundle graph (nifi-graph-nar + nifi-other-graph-services-nar)
fournit un troisième chemin d'exécution de code auquel cette annotation manque :
// 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); // ligne 463 — compile la chaîne de l'attaquant
compiledCode.put(s, compiled);
...
compiled.setBinding(bindings);
Object result = compiled.run(); // ligne 477 — exécute dans la JVM NiFi
Le paramètre s est la chaîne de requête de la propriété « Graph Query » du
processeur ExecuteGraphQuery, transmise sans modification. Il n'y a
aucune désinfection, aucune liste blanche et aucun bac à sable — tout Groovy valide est
compilé et exécuté avec les privilèges complets du processus NiFi.
Ni TinkerpopClientService ni les processeurs ExecuteGraphQuery /
ExecuteGraphQueryRecord qui le pilotent ne portent l'annotation
@Restricted. La couche d'autorisation de NiFi les traite donc
comme des composants ordinaires, et un concepteur de flux qui s'est vu
explicitement refuser EXECUTE_CODE peut toujours les créer.
Un serveur NiFi 2.8.0 est exploitable lorsque les deux conditions suivantes sont réunies :
Le bundle graph optionnel est installé. Plus précisément le
NAR nifi-other-graph-services-nar — c'est le bundle qui fournit
TinkerpopClientService. Les serveurs sans ce NAR ne sont pas concernés.
Au moins un utilisateur dispose de permissions de type concepteur de flux
(read/write sur un process group et sur /controller) sans
EXECUTE_CODE. C'est une configuration de politique courante : une organisation
souhaite que les utilisateurs puissent construire et modifier des pipelines, mais pas exécuter
de code arbitraire. La permission EXECUTE_CODE est précisément la
frontière de sécurité que cette politique est censée faire respecter.
Si les deux conditions sont réunies, l'utilisateur peut configurer un
TinkerpopClientService en mode « ByteCode Submission », le pointer vers n'importe quel
serveur Gremlin accessible (le serveur Gremlin n'est utilisé que pour l'initialisation
du service — l'exécution se fait localement), et créer un
processeur ExecuteGraphQuery avec du code Groovy dans la propriété « Graph Query ».
Le démarrage du processeur compile et exécute le Groovy dans la
JVM NiFi.
Exécution de code arbitraire sous le compte de service NiFi. À partir de là :
flow.json.gz, en déposant un NAR
dans le répertoire de chargement automatique, ou en modifiant un controller service.L'exploit contourne la permission EXECUTE_CODE, donc cela fonctionne même
lorsque l'opérateur a explicitement retiré les droits d'exécution de code à
l'utilisateur — exactement le scénario que la permission est censée empêcher.
Ce dépôt fournit un POC, couvrant le déclencheur le plus direct :
du Groovy contrôlé par l'attaquant placé directement dans la propriété du processeur
Graph Query. Aucune connexion en amont, aucun Expression Language,
aucun contenu FlowFile nécessaire — juste le processeur fonctionnant sur sa propre
minuterie.
Deux chemins de code supplémentaires existent (le contenu du corps du FlowFile utilisé comme
requête lorsque « Graph Query » est vide, et l'interpolation par Expression Language
des attributs FlowFile influencés par l'attaquant dans le modèle de requête).
Ils ne sont pas démontrés ici car ils atteignent la même sortie avec des préconditions
supplémentaires ; le chemin direct suffit à prouver le contournement de EXECUTE_CODE.
setup/ — Environnement Docker Compose. Démarre une instance NiFi 2.8.0
(image officielle apache/nifi:2.8.0) avec les NAR du bundle graph
chargés automatiquement, un serveur LDAP fournissant deux utilisateurs de test (admin
et flow_designer), et un serveur Gremlin vers lequel le
TinkerpopClientService doit pointer. setup.sh initialise les
politiques afin que flow_designer dispose de droits d'édition de flux mais
pas de EXECUTE_CODE, puis vérifie la configuration.
pocs/flow_designer_groovy_rce.py — Exploit autonome.
S'authentifie en tant que flow_designer, démontre que ExecuteScript
est refusé (la barrière EXECUTE_CODE fonctionne), puis crée un
+ avec une charge utile
Groovy qui lance un shell inverse bash vers un écouteur que le POC
démarre localement. Une fois la connexion établie, le POC
fait évoluer le shell vers un bash interactif complet adossé à un PTY via
d'util-linux, avec pontage stdio en mode brut et redimensionnement
de fenêtre en direct — l'opérateur obtient un vrai terminal dans le conteneur NiFi.
Prérequis : Docker, Python 3.10+ et uv.
cd setup
./setup.sh
La première exécution télécharge les NAR du bundle graph depuis Maven Central et récupère les images Docker (~2-3 min). Une fois l'installation terminée, elle affiche l'invocation POC prête à coller.
Exécutez le 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
Le POC place l'opérateur dans une session bash interactive à l'intérieur du
conteneur NiFi s'exécutant sous le compte de service nifi. Tapez exit ou
appuyez sur Ctrl-D pour vous déconnecter — le POC nettoiera alors le processeur
et le controller service qu'il a créés.
Sur Docker Linux (où host.docker.internal ne se résout pas par
défaut), passez --shell-host <ip-pont-hôte> ou ajoutez
extra_hosts: ["host.docker.internal:host-gateway"] au service nifi
dans setup/docker-compose.yml.
Arrêt :
cd setup
./teardown.sh
TinkerpopClientServiceExecuteGraphQueryscript