找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3068

积分

0

好友

446

主题
发表于 11 小时前 | 查看: 0| 回复: 0

这周浏览技术文章时,看到一个关于 SpringBoot + Vue 实现的后台管理系统的分享,阅读量还挺高。出于好奇,我便下载了源码进行分析,结果发现其中存在的安全漏洞还真不少。尤其是在 SQL 防护方面,开发者虽然引入了一套过滤机制,但由于配置逻辑不当,最终还是导致了 SQL注入 漏洞的产生。

关于SpringBoot+Vue系统的技术分享截图

在征得原作者同意后,我决定将本次代码审计的思路整理出来,与各位开发者朋友分享探讨。

微信聊天记录截图

SQL注入分析

MyBatis-Plus 框架对绝大多数 SQL 场景都采用了预编译处理,安全性较高。但对于动态表名、ORDER BY 等需要拼接 SQL 的场景,若配置不当,依然会存在风险。本次要记录的,便是在一次代码审计中遇到的“奇特”案例:明明对请求参数进行了非常严格的黑名单过滤,却因为代码逻辑设计不当,仍然导致了 SQL 注入。

分页功能中的SQL注入

首先查看项目的 pom.xml 文件,确认其持久层框架为 MyBatis-Plus。

MyBatis-Plus的Maven依赖配置

在查阅工具类时,我发现项目中有一个专门用于过滤 SQL 注入的类 SQLFilter

SQLFilter.java 类源码截图

那么,这个过滤器具体在哪些地方被调用呢?通过 IDE 的查找引用功能,我发现只在 com.utils.Query 类中有 4 处调用。

在Query类中调用SQLFilter.sqlInject方法的截图

跟进 com.utils.Query 类查看,发现它有两个重载的构造方法,分别接收不同类型的分页参数:

  • JQPageInfo:封装了分页参数的实体类。
  • Map<String, Object> params:Map 集合类型的参数。

Query类的两个构造方法截图

虽然入参不同,但其防御 SQL 注入的逻辑是相同的:都是对 sidxorder 参数调用 SQLFilter.sqlInject() 进行过滤,然后将处理后的参数封装到 Page 对象中。

Query类第一个构造方法中调用SQLFilter的截图
Query类第二个构造方法中调用SQLFilter的截图

Query 类的完整源码如下:

package com.utils;

import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import com.baomidou.mybatisplus.plugins.Page;

/**
 * 查询参数
 */
public class Query<T> extends LinkedHashMap<String, Object> {
    private static final long serialVersionUID = 1L;
    /**
     * mybatis-plus分页参数
     */
    private Page<T> page;
    /**
     * 当前页码
     */
    private int currPage = 1;
    /**
     * 每页条数
     */
    private int limit = 10;

    public Query(JQPageInfo pageInfo) {
        //分页参数
        if(pageInfo.getPage()!= null){
            currPage = pageInfo.getPage();
        }
        if(pageInfo.getLimit()!= null){
            limit = pageInfo.getLimit();
        }

        //防止SQL注入(因为sidx、order是通过拼接SQL实现排序的,会有SQL注入风险)
        String sidx = SQLFilter.sqlInject(pageInfo.getSidx());
        String order = SQLFilter.sqlInject(pageInfo.getOrder());

        //mybatis-plus分页
        this.page = new Page<>(currPage, limit);

        //排序
        if(StringUtils.isNotBlank(sidx) && StringUtils.isNotBlank(order)){
            this.page.setOrderByField(sidx);
            this.page.setAsc("ASC".equalsIgnoreCase(order));
        }
    }

    public Query(Map<String, Object> params){
        this.putAll(params);

        //分页参数
        if(params.get("page") != null){
            currPage = Integer.parseInt((String)params.get("page"));
        }
        if(params.get("limit") != null){
            limit = Integer.parseInt((String)params.get("limit"));
        }

        this.put("offset", (currPage - 1) * limit);
        this.put("page", currPage);
        this.put("limit", limit);

        //防止SQL注入(因为sidx、order是通过拼接SQL实现排序的,会有SQL注入风险)
        String sidx = SQLFilter.sqlInject((String)params.get("sidx"));
        String order = SQLFilter.sqlInject((String)params.get("order"));
        this.put("sidx", sidx);
        this.put("order", order);

        //mybatis-plus分页
        this.page = new Page<>(currPage, limit);

        //排序
        if(StringUtils.isNotBlank(sidx) && StringUtils.isNotBlank(order)){
            this.page.setOrderByField(sidx);
            this.page.setAsc("ASC".equalsIgnoreCase(order));
        }

    }

    public Page<T> getPage() {
        return page;
    }

    public int getCurrPage() {
        return currPage;
    }

    public int getLimit() {
        return limit;
    }
}

乍一看,这里的过滤逻辑似乎很完美。但奇怪的是,我翻阅了几个 SQL 映射文件(XML),发现都没有直接使用到这个 Query 类生成的 page 对象。

MyBatis XML映射文件截图

那么,实际的查询流程是怎样的呢?我们从页面中找一个带分页的查询请求来分析,例如:

/jixiaokaohe/page?page=1&limit=10&sort=id

这个请求会进入对应的 Controller -> com.controller.JixiaokaoheController#page 方法。可以看到,它调用了 jixiaokaoheService.queryPage 进行查询,并使用 MPUtil.sort 方法来自定义一个查询条件包装器 wrapper

JixiaokaoheController.page方法源码截图

继续跟进到 Service 实现层 com.service.impl.JixiaokaoheServiceImpl#queryPage。这里确实使用了我们之前看到的、带有 SQL 过滤的 Query 构造方法。

JixiaokaoheServiceImpl.queryPage方法源码截图

继续向下跟踪调用链,发现最终的 SQL 实现依赖的是那个自定义的 wrapper 来构建查询条件。

查询方法调用链截图
MyBatis XML中定义select语句的截图
Controller中调用MPUtil.sort方法的截图

问题的关键就在这个 MPUtil.sort 方法里。跟进去查看 com.utils.MPUtil#sort 的源码,发现它直接对传入的 sort 参数进行了字符串拼接,并且完全没有经过任何过滤

MPUtil.sort方法源码截图

漏洞点找到了,我们来构造一个 POC 测试一下:

page=1&limit=10&sort=id and updatexml(1,concat(0x7e,database()),0)#

发送请求,成功触发数据库报错,证实了 SQL 注入漏洞的存在。

触发SQL注入漏洞的HTTP请求与响应截图

其他潜在的SQL注入点

MyBatis-Plus 在 MyBatis 的基础上增强,因此 MyBatis 中 #{}(预编译)和 ${}(拼接)的 SQL 编写方式在 MyBatis-Plus 中同样适用。直接在项目中搜索 ${,可以发现多处使用了 SQL 拼接的地方。

在项目中搜索${符号的结果截图

我们选取其中一处进行分析。这是一个用于提醒计数的 SQL 片段,其中的表名、列名等均采用了 ${} 拼接。

CommonDao.xml中定义remindCount查询的截图

一路向上追溯调用链:
com.service.impl.CommonServiceImpl#remindCount

CommonServiceImpl.remindCount方法调用截图

最终到达 Controller 层 com.controller.CommonController#remindCount。发现整个过程对传入的参数没有任何过滤。

CommonController.remindCount方法源码截图

根据接口定义,构造 POC 如下(注意 type 参数需符合上下文逻辑,此处仅演示注入Payload):

users union select group_concat(SCHEMA_NAME) from information_schema.SCHEMATA

发送请求,成功获取数据库名信息,验证了该处也存在 SQL注入 漏洞。

通过SQL注入获取数据库名的请求响应截图

SQL注入问题总结

在处理动态表名、ORDER BY 等需要 SQL 拼接的场景时,代码实现必须格外仔细。总体而言,在 MyBatis-Plus 框架中挖掘 SQL 注入漏洞,有时比在原生 MyBatis 中更具挑战性,需要审计人员更加耐心和细致。

任意用户密码重置漏洞

在审计过程中,还无意发现一个“后门”性质的功能。一个重置密码的接口,竟然可以不进行任何权限校验就直接调用!

关键在于 @IgnoreAuth 这个注解,它的作用就是跳过权限认证。方法逻辑很简单:根据用户名查找用户,如果用户存在,则将其密码重置为 123456

UserController.resetPass方法源码截图

直接构造请求即可重置任意用户密码:

GET /springboot57n6g/users/resetPass?username=admin HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Accept: application/json, text/plain, */*
sec-ch-ua: "Chromium";v="122", "Not(A Brand";v="24", "Google Chrome";v="122"
sec-ch-ua-platform: "Windows"
sec-ch-ua-mobile: ?0
Accept-Encoding: gzip, deflate, br
Sec-Fetch-Site: same-origin
Sec-Fetch-Dest: empty
Referer: http://localhost:8081/
Sec-Fetch-Mode: cors
Accept-Language: zh-CN,zh;q=0.9

请求成功,管理员密码被重置。

重置密码成功的HTTP响应截图

任意文件上传漏洞

文件上传请求与响应截图

漏洞分析

文件上传功能在 com.controller.FileController#upload 中实现。它通过截取文件名最后一个点来获取后缀,然后结合时间戳生成新文件名。但整个过程没有对文件内容、后缀或 MIME 类型做任何安全检查,导致攻击者可以上传任意恶意文件(如 Webshell)。

FileController.upload方法源码截图

任意文件下载漏洞

文件下载请求与响应截图

漏洞分析

文件下载功能在 com.controller.FileController#download 中实现。关键问题在于第94行(根据截图上下文)的文件路径拼接逻辑:静态目录/upload/filename。由于 filename 参数完全由用户控制,且没有进行路径穿越过滤(如 ../),导致攻击者可以通过目录穿越下载服务器上的任意文件。

FileController.download方法源码截图

总结

本次对一套 Spring Boot 系统的代码审计 揭示了一个深刻的教训:开发者在注重功能实现的同时,必须将安全考量融入到设计编码的每一个环节。就像那个密码重置接口,开发者可能只考虑了“便捷性”,却完全忽略了其带来的巨大安全风险。安全防护是一个整体,一处疏漏便可能导致全盘皆失。希望这次的案例分析能为大家在开发中敲响警钟,写出更健壮、更安全的代码。

如果你想与更多开发者交流此类安全与开发实践,欢迎访问 云栈社区 进行探讨。




上一篇:使用MyBatis-Plus类型处理器优雅实现单列敏感数据加解密
下一篇:开源免费、功能全面的AI学习平台PageLM上线,NotebookLM的社区驱动替代方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-28 18:09 , Processed in 0.258827 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表