ssti(Server-Side Template Injection)即服务器端模版注入。
首先,在介绍ssti漏洞之前,需要了解web前端框架(MVC)中对模版文件的调用和渲染,Web框架比如Flask(以使用 Python 编写的轻量级 Web 应用框架),函数调用模版引擎(模板引擎是一种将动态数据与静态模板结合生成最终输出的工具。)对模版文件(多为html文件和html file文件)进行渲染和调用。还有就是关于SSTI的题大都出在python上,但是这种攻击方式并不是只存在于Python 这门语言里面,凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言。在正常情况下,模板引擎被设计用于安全地将预定义的模板与数据进行组合,生成最终的输出。但是,SSTI 漏洞允许攻击者在应用程序的上下文中执行任意的服务器端代码。当攻击者能够通过用户输入或其他外部来源插入恶意的模板代码时,就会产生一系列问题。
当绕过服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,而模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,就会导致敏感信息泄露、代码执行、GetShell 等问题 。
一句话介绍ssti漏洞成因就是:如果在构建模板时未正确处理用户输入,就可能导致SSTI漏洞的产生。
由上方介绍可知,ssti最开始以及最重要的一步应该是判断模版类型。常见模板有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模板。
实际上,这里就是通过对各个语法的不同,通过回显,来判断模版的类型。因为在不同的语法中,包裹的的内容,不同,语法的应用不同,对语法中内容的处理也不同。
常用的类:
__class__用来查看变量所属的类
__bases__用来查看类的基类,就是父类(或者__base__)
__mro__:显示类和基类
__subclasses__():查看当前类的子类
__getitem__:对数组字典的内容提取
__init__ : 初始化类,返回的类型是function
__globals__:查看全局变量,有哪些可用的函数方法等,然后再搜索popen,eval等
__builtins__:提供对Python的所有"内置"标识符的直接访问,即先加载内嵌函数再调用
__import__ : 动态加载类和函数,用于导入模块,经常用于导入os模块(例如__import__('os').popen('ls').read())
url_for :flask的方法,可以用于得到__builtins__
lipsum :flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
config:当前application的所有配置。
popen():执行一个 shell 以运行命令来开启一个进程
基本的类就这些,但是很多网站都存在很多过滤,到时候绕过又需要花费一些功夫。
接下来就是实战
[HNCTF 2022 WEEK2]ez_SSTI
进入页面,已经给了提示,且经过测试,存在ssti
太贴心了,还给了一个判断模版的超链接
通过和图里面的方法,不断试错。
发现模版是jinja2(这里的参数是看好像很多的文章介绍这个都是以name为参数,所以选择name)
然后呢,执行方式选择eval执行命令,构造payload,返回出所有的子类,一般要用到的子类是os._wrap_close,当然在不同的情况下所处的位置不同
搜索了一下,定位了大概位置,这里呢,有两种构造payload的方法,第一种呢是通过全局变量—__globals__,然后再搜索
payload:
{{config.__class__.__init__.__globals__['os'].popen('cat flag').read() }}
就可以直接得到flag
还有一个是在类的下面找,这个有点麻烦,因为要找到子类的位置
这里通过查看器发现是第138项
构造payload
?name={{''.__class__.__bases__[0].__subclasses__()[138].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat f*").read()')}}
就得到了flag
纯小白,如有错误欢迎指导