SSTI注入
简述:SSTI(Server-Side Template Injection)
服务器端模板注入,攻击者能够向服务器端模板引擎注入恶意模板代码,从而在服务器端执行任意代码。
一、前置基础
首先我们要知道什么是模板。
模板 是一种用于生成动态内容的工具。
它们通常包含两个基本部分:静态内容和动态占位符。
如图:绿色 部分为 静态内容 ,而 橙色 部分则是 动态占位符

模板的主要作用是分离业务逻辑和展示逻辑,使得前端开发者和后端开发者可以并行工作,同时也使得代码更易于维护。
静态部分:由前端开发者设计,包括HTML、CSS、JavaScript等。 动态部分:由后端开发者提供数据,模板引擎将数据填充到模板中,生成最终的页面。
模板引擎的工作流程
1.解析模板:将模板文件解析成抽象语法树(AST)或类似的内部表示。
2.处理指令:执行模板中的指令(如循环、条件判断等)。
3.替换占位符:将占位符替换为实际的值。
4.输出结果:将处理后的模板输出为字符串(如HTML)。
模板注入攻击成功的关键,在于攻击者输入的内容(如3)能够被模板引擎解析并生成抽象语法树(AST),而不仅仅是作为纯文本处理,AST是连接模板语法和底层代码执行的桥梁。攻击者的输入被解析成AST只是第一步,而真正执行这个AST并使其产生效果的,是模板引擎的代码生成器或解释器。
eg.[HNCTF 2022 WEEK2]ez_SSTI
传入
1 | http://node5.anna.nssctf.cn:28434/?name={{7*7}} |
返回49,说明存在服务器端模版注入。
二、SSTI注入流程
- 信息收集:
- 确定目标使用的编程语言和模板引擎(如 Flask 使用 Jinja2)。
- 测试注入点:URL 参数、表单、评论区、HTTP 请求头等。
- 提交测试 payload:
{{7*7}}或{% print(1+1) %},观察是否被解析。
- 了解模板引擎特性:
- Jinja2:支持 Python 反射(如
__class__,__globals__)。 - Twig:支持过滤器和回调函数。
- FreeMarker:支持 Java 类调用(如
?new())。
- Jinja2:支持 Python 反射(如
3.确认漏洞:
-
请求:
http://example.com/?name={{7*7}} -
响应:
49(表明 Jinja2 SSTI 漏洞存在)。4.构造 payload:
-
读取配置:
{{ config }} -
执行命令:
{{ ''.__class__.__mro__[1].__subclasses__()[159].__init__.__globals__['os'].popen('whoami').read() }}5.触发执行
在具体说明之前,我们简要说明一下类之间的继承关系,因为在后面的攻击中用到的就是这种继承关系的不断调用最终达到一个rce的效果。
eg.
1 | class A:pass |
我们创建了4个类,其中的B类继承了A类,C、D类继承了B类,如果我们在这创建一个C的对象c,那么我们就可以通过__class__魔术方法来找到它的当前类
1 | class A:pass |
它返回了一个当前的类为C,我们还可以通过__base__这个魔术方法来找到当前类的父类:
1 | class A:pass |
找到了C类的父类B类,如果还想要找B类的父类可以接着使用__base__魔术方法:
1 | class A:pass |
当然这样一个一个递进上去的方法有一些麻烦,所以我们可以使用__mro__魔术方法来一步到位看到类的所有父类
1 | class A:pass |
以数组形式在后面加上下标就能拿到指定的类了
1 | class A:pass |
我们在拿到object类后就可以通过object类来查找python中的所有object类的子类,当然这其中会有我们能通过该类rce的子类。我们通过__subclasses__来获取当前类的所有子类
1 | class A:pass |
可以发现有很多类,前面我们也说到了python的所有类最终都是继承object类的,因此这里存在大量的类,我们最终的目的是要去进行rce
常见魔术方法:
1 | __class__ :返回类型所属的对象 |
通用 payload ( Python3 )¶
1 | # 命令执行_eval |
ps:QAQ好抽象
二编:

这里再补充一下:常见模板有Smarty、Mako、Twig、Jinja2、Eval、Flask、Tornado、Go、Django、Ruby等。
这幅图的含义是通过这些指令去判断对方用的是什么模板,下面解释一下这幅图的意思:
绿色箭头是执行成功,红色箭头是执行失败。
首先是注入${7*7}没有回显出49的情况,这种时候就是执行失败走红线,再次注入49如果还是没有回显49就代表这里没有模板注入;如果注入回显了49代表执行成功,继续往下走注入49,如果执行成功回显7777777说明是jinja2模板,如果回显是49就说明是Twig模板。
然后回到最初注入${7*7}成功回显出49的情况,这种时候是执行成功走绿线,再次注入a{comment}b,如果执行成功回显ab,就说明是Smarty模板;如果没有回显出ab,就是执行失败走红线,注入${“z”.join(“ab”)},如果执行成功回显出zab就说明是Mako模板。实际做题时也可以把指令都拿去测测看谁能对上。平时做题也可以多搜集不同模板对应的注入语句语法。
常见payload:
1.Twig模版:
1 | 先找到flag位置 |
将Payload分成两部分来理解:
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("cat /flag")}}
第一部分:设置恶意回调
{{_self.env.registerUndefinedFilterCallback("exec")}}
_self:
在Twig模板中,_self是一个特殊的变量,它指向当前的模板上下文(Twig\Template实例)。通过它可以访问到模板内部的一些方法和属性。
_self.env:
env是_self的一个属性,它指的是Twig的环境对象(Twig\Environment实例)。这个对象包含了模板引擎的所有配置、扩展、过滤器、函数等全局信息。控制了它,就相当于控制了模板引擎的核心。
registerUndefinedFilterCallback:
这是Twig\Environment对象的一个方法。
它的作用是:注册一个回调函数。当Twig在模板中遇到一个未被定义的过滤器时,就会自动调用这个注册好的回调函数,而不是直接抛错。
这行代码的执行结果是——告诉Twig引擎:“以后如果你遇到任何不认识的过滤器,别报错,去调用exec函数来处理”。
第二部分:触发命令执行
{{_self.env.getFilter("cat /flag")}}
本文参考资料:SSTI 注入 - Hello CTF
Leave a comment