



服务端模板注入(Server-Side Template Injection, SSTI)漏洞一般是由于服务端接收了用户的输入后,没有进行合理的控制和处理就将其插入We b应用模板,导致模板引擎在进行目标编译渲染的过程中执行了用户输入的恶意内容。
在学习服务端模板注入漏洞之前,我们需要先了解一下什么是模板引擎。
模板引擎以网站业务逻辑层和表现层分离为目的,将模板与数据模型结合起来,生成结果文档(例如HTML),有助于将动态数据填充到网页中。
常见的模板引擎包括Smarty、Twig、Jinja2、Tornado等,不同的模板引擎在渲染语法上会有一定的差异,关于模板渲染的知识,读者可以自行学习。模板引擎一般会提供沙箱机制来防范漏洞,不允许使用没有定义或声明的模块,但是依然可以利用沙箱逃逸技术绕过。
在挖掘服务端模板注入漏洞之前,首先要对目标使用的模板引擎进行检测,模板引擎检测可以参考由国外安全研究人员James Kettles提出的检测流程,如图1-205所示,实线箭头和虚线箭头分别代表响应成功和响应失败。有时,同一个可执行的Payload可以有多个不同的响应结果,例如“{{7*'7'}}”会在Twig中返回“49”,而在Jinja2中则是“7777777”,我们在检测模板引擎时要注意辨别。
图1-205 模板引擎检测流程
模板注入漏洞涉及服务端Web应用使用模板引擎渲染用户输入请求的过程,我们以PHP的Smarty模板引擎为例介绍模板注入产生的原理。
新建smarty.php文件,代码如下所示,程序定义了一个name变量为用户输入的内容,没有启用沙箱模式,直接使用Smarty模板引擎的display()方法渲染当前页面。
我们先正常访问页面,传递一个name参数并查看返回结果,如图1-206所示。
图1-206 正常返回结果
由于程序完全信任用户的输入,没有采取任何安全措施,因此我们直接构造Payload进行测试。首先测试是否存在SSTI漏洞,输入“{7*7}”,结果如图1-207所示。
图1-207 存在SSTI漏洞
从返回的结果中发现存在SSTI漏洞,有些利用方法在新版的Smarty中已经不再适用,这里使用if标签构造Payload为“{if system(pwd)}{/if}”,结果如图1-208所示,成功利用SSTI漏洞执行了系统命令。
图1-208 利用SSTI漏洞执行命令
通过这个简单的示例,我们大致了解了服务端模板注入的原理。由于渲染的模板内容可以由用户控制,因此当攻击者输入构造好的恶意内容时,服务端模板引擎也可以正常渲染,从而形成服务端模板注入漏洞。
Flask是一个使用Python编写的轻量级Web应用框架,其模板引擎使用的是Jinja2。Jinja2是基于Python的模板引擎,官方介绍称Jinja2是一个现代的、设计者友好的、仿照Django模板的Python模板语言,它速度快,被广泛使用,并且提供了可选的沙箱模板执行环境来保证安全。Jinja2有3种常用的基本语法,分别是变量{{name}}、注释{# ...#}和控制结构{%...%}。
1.实例环境
我们写一个存在SSTI漏洞的实例,代码如下所示。
使用Python3启动后,访问页面并传入“name={{7*7}}”,测试是否存在SSTI漏洞,结果如图1-209所示,存在SSTI漏洞。
图1-209 存在SSTI漏洞
当我们想进一步利用SSTI漏洞来执行系统命令时,例如导入os(operating system)模块来执行whoami命令,会发现系统报错,如图1-210所示。
图1-210 错误信息
为什么会出现这种情况呢?这里涉及了前面提到的沙箱技术,默认情况下模板引擎会限制用户访问不安全的属性和方法,因此只能充分利用我们可以控制的地方来绕过沙箱机制。
在尝试沙箱绕过之前,我们需要先了解一下Python的魔术方法。
2.Python中的魔术方法
Magic Method(魔术方法)是Python中一些特殊方法的统称,这些特殊方法名前后都添加了两个下划线,例如:__init__。对于SSTI中常用的魔术方法及作用如表1-9所示。
表1-9 魔术方法及作用
3.沙箱绕过
在Python中,所有内容都可以用对象表示,均继承于对象,对象中的类也是可以继承的,我们可以利用继承关系来间接调用模块,从而达到我们想要的效果。
由于CTF赛题环境与本地测试环境可能不一致,并且Python2与Python3也有一定的差异,因此在构造Payload时要格外注意,比赛时要以比赛环境为主来进行构造Payload。以下构造方法基于Python3,与Python2的构造思路大致相同,Python2的构造方法读者可以自行尝试。
获取字符串的类对象,代码如下。
用__bases__[0]拿到基类,代码如下。
用__subclasses__()方法列出全部子类和其他继承于该基类的类,代码如下,部分结果如图1-211所示。
图1-211 部分子类
接下来通过索引来指定类,环境不同索引值可能会有所不同,以实际题目环境为准。
通过以下脚本遍历所有包含sys模块的类。
结果如图1-212所示。
图1-212 所有包含sys模块的类
这里我们选定warnings.catch_warnings类进行尝试,获取它的全局变量,代码如下。
从返回的结果中发现存在sys模块,如图1-213所示。
图1-213 存在sys模块
为了更直观地了解其中的原理,我们查看catch_warnings类的源码,如图1-214所示。
图1-214 catch_warnings类源码
将源码翻到顶部可以看到导入了sys模块,如图1-215所示,我们可以利用这个类间接调用sys模块,从而达到命令执行的效果。
图1-215 导入sys模块
构造最终Payload如下。
将构造好的Payload发送并查看返回结果,发现成功执行了系统命令,如图1-216所示。
图1-216 执行系统命令
当Python解释器启动后,即使用户不做任何操作,也有很多函数可以使用,这些函数就是Python的内置函数。那么内置函数是如何工作的呢?这里又涉及了Python命名空间的知识,命名空间是从名称到对象的映射,其中大部分命名空间是通过Python字典实现的。命名空间一般分为3种:内置名称、全局名称、局部名称。
Python解释器在启动时会加载内置命名空间,内置命名空间有许多名字到对象之间的映射,这些名字就是内置函数的名称,对象就是这些内置函数本身,这些内置命名空间到对象之间的映射是由Python的内置模块__builtins__完成的。
首次启动Python解释器时,我们可以输入dir()查看有哪些自动导入的模块,代码如下。
从返回结果可以看到__builtins__的模块名称,输入dir(__builtins__)查看所有内置函数名称,结果如图1-217所示。
图1-217 内置函数名称
知道了__builtins__模块与内置函数的关系后,我们可以直接使用__builtins__来调用内置函数。在Python解释器中输入__builtins__.eval("__import__('os').system('whoami')"),结果如图1-218所示,可见执行了系统命令。
图1-218 通过内置模块执行系统命令
如果通过内置函数构造可用于SSTI执行系统命令的Payload,则需要遍历含有eval内置函数的子类,我们可以写一个脚本进行遍历,代码如下。
运行结果如图1-219所示。
图1-219 遍历包含eval内置函数的子类
构造最终Payload如下。
将Payload发送至靶机,结果如图1-220所示,成功执行了系统命令。
图1-220 使用内置函数执行命令
Flask内置了非常多的函数,其中包括url_for()函数。这个函数是在helpers.py文件里定义的,通过分析helpers.py文件,发现可以利用这个函数来绕过限制,达到执行命令的目的。该函数的定义方法如图1-221所示,在代码头部发现其导入了os和sys模块,如图1-222所示。
图1-221 url_for()函数代码
图1-222 导入了os和sys模块
调用__globals__类中的os或者sys模块,构造可以执行命令的Payload,将其发送至靶机,结果如图1-223所示。
图1-223 利用Flask内置函数执行命令
4.Bypass技巧
当题目过滤了大括号“{{”或“}}”时,我们可以使用Jinja2模板引擎的“{%...%}”语句装载一个循环语句来进行绕过,Payload代码如下。
执行结果如图1-224所示。
图1-224 内置语法绕过限制
当题目过滤了一些关键字时,我们可以采用拼接字符串的方式绕过。例如在Python中可以用加号“+”进行字符串的拼接,构造Payload如下。
结果如图1-225所示。
图1-225 拼接字符串绕过
如果中括号“[]”或小数点“.”被过滤,我们可以使用Jinja2原生函数attr()来绕过,其中中括号“[]”可以用__getitem__来代替。
构造Payload如下。
上述代码等同于下列代码。
将构造好的Payload发送至靶机,结果如图1-226所示。
图1-226 绕过对中括号或小数点的过滤
Smarty是一个用于PHP的模板引擎,促进了表示层(HTML/CSS)与应用逻辑的分离。这意味着PHP代码是应用程序逻辑,与表示层分离。2021年2月,研究人员发布了该模板引擎的两个CVE漏洞,漏洞可用于绕过沙箱机制执行PHP代码,受影响的Smarty模板引擎版本小于或等于3.1.38。
1.CVE-2021-26119
漏洞产生的根本原因是Smarty从$smarty.template_object超级变量中访问实例,在smarty_internal_compile_private_special_variable.php文件的compile()函数中,返回了一个$_smarty_tpl,如图1-227所示。
图1-227 部分源码
smarty_tpl返回的是Smarty_Internal_Template类实例,在smarty_internal_template.php文件中发现Smarty对应的是smarty类,如图1-228所示。
图1-228 smarty类部分源码
而开启和关闭沙箱的方法都在smarty类中,如图1-229所示,这意味着我们可以通过调用disableSecurity()函数关闭沙箱后再调用父类的display()函数执行任意PHP代码。
图1-229 开启和关闭沙箱的方法
创建实例进行测试,如下代码创建了一个强化沙箱页面,比默认沙箱机制更安全。
构造Payload如下。
将Payload发送至靶机,结果如图1-230所示,成功关闭沙箱并且执行了命令。
图1-230 开启和关闭沙箱方法
2.CVE-2021-26120
本题Smarty_Internal_Runtime_TplFunction沙箱逃逸导致PHP代码注入,与CVE-2021-26119为同一个出题人。漏洞的产生原因是Smarty编译模板语法时,Smarty_Internal_Runtime_TplFunction类在定义tplFunctions时没有正确过滤name属性。比如发送{function name='test'}{/function}到靶机,在templates_c目录中看到编译器生成了一个.php文件,部分代码如图1-231所示,name值可以被攻击者控制并且注入到生成的代码中。
图1-231 编译器生成代码
我们可以通过注入自定义函数的方法达到命令执行的目的,构造Payload如下。
将构造好的Payload发送至靶机,结果如图1-232所示,成功执行了命令。
此时再看目录中生成的.php文件,发现生成了一个rce()函数来执行系统命令,如图1-233所示。
图1-232 漏洞利用
图1-233 注入自定义函数
1.BJDCTF-2020-Web-The mystery of ip
题目页面有一个flag链接,点击链接后跳转到一个显示自己IP的页面,如图1-234所示。
图1-234 页面显示IP
看到显示了IP,首先想到的是可以尝试伪造X-Forwarded-For信息。经过各种尝试,最终发现此处存在SSTI注入。页面是用PHP编写的,经过探测发现模板引擎使用的是Smarty。
输入“{$smarty.version}”查看Smarty模板引擎版本,结果如图1-235所示。
图1-235 查询模板引擎版本
使用低版本Smarty模板引擎,直接构造Payload命令并执行,读取flag,结果如图1-236所示。
图1-236 读取flag
2.PasecaCTF-2019-Web-Flask SSTI
在CTFHub开启题目环境,题目名称很明显就是Flask SSTI,直接测试看是否存在SSTI注入漏洞,输入{{1+1}},结果如图1-237所示,存在SSTI注入漏洞。
图1-237 存在SSTI注入漏洞
查看Config信息,如图1-238所示,flag在这里显示了,不过看起来是经过加密后的flag。
图1-238 加密后的flag
于是尝试用命令读取源码,直接构造Payload无法成功。经测试发现题目过滤了下划线“_”、小数点“.”、单引号“,”等字符。因为题目提示了unicode,所以我们可以尝试用unicode编码绕过,对小数点的过滤可以尝试用“[“”]”绕过。查看所有子类,构造Payload如下。
部分结果如图1-239所示,将结果保存下来,找到需要的类的索引值。
图1-239 查看所有基类
使用warnings.catch_warnings构造命令并执行Payload。
结果如图1-240所示,源码中找到了flag的加密脚本。
图1-240 flag加密脚本
由于使用了异或变换加密flag,因此使用相同的程序也可以解密flag,解密脚本和解密后的flag如图1-241所示。
图1-241 解密后的flag