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

2130

积分

0

好友

299

主题
发表于 2025-12-30 15:25:55 | 查看: 24| 回复: 0

0x01 前言

本周分析了一个基于SpringBoot+Vue实现的后台管理系统源码,发现其中存在多处安全隐患。尤为典型的是,开发者在SQL注入防护上虽然配置了黑名单过滤,但由于代码逻辑缺陷,最终仍导致了注入漏洞。

基于SpringBoot+Vue的后台管理系统文章数据
图1:某公众号分享的SpringBoot+Vue系统文章,阅读量较高

在征得原作者同意后,本文将分享本次代码审计中关于SQL注入漏洞的发现思路。

与师傅沟通分享思路的聊天截图
图2:与项目作者沟通,获准分享技术思路

0x02 SQL注入漏洞分析

MyBatis-Plus框架虽然对绝大多数SQL场景进行了预编译处理,但在动态表名、ORDER BY等需要字符串拼接的场景下,如果配置不当,仍然会引发注入风险。本次审计就遇到了一个典型案例:在请求参数黑名单过滤十分严格的情况下,由于后端代码逻辑不当,导致了SQL注入。

分页功能中的SQL注入

通过项目pom.xml文件可知,该项目持久层使用的是MyBatis-Plus框架。

项目pom.xml中的MyBatis-Plus依赖
图3:项目依赖中包含mybatis-plus

在审计工具类时,发现项目提供了一个SQLFilter类用于防御SQL注入。

SQLFilter工具类中的关键词黑名单
图4:SQLFilter.sqlInject方法通过黑名单过滤危险关键词

全局搜索该方法调用点,发现仅在com.utils.Query类的构造方法中被调用了4次。

搜索SQLFilter.sqlInject方法的调用情况
图5:在Query类中找到了sqlInject方法的调用点

跟进com.utils.Query类,发现其有两个重载的构造方法,分别接收JQPageInfoMap<String, Object>类型参数,但核心防御逻辑一致:都对sidxorder参数进行过滤处理后,封装成Page对象。

Query类的两个构造方法定义
图6:Query类的构造方法接收分页参数

Query构造方法中对sidx和order参数进行过滤
图7:使用SQLFilter过滤sidx和order参数

另一个构造方法中同样进行过滤
图8:Map参数构造方法中的过滤逻辑

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;
    }
}

看似完美的过滤逻辑,但审计MyBatis的XML映射文件时,却发现这些查询并未使用Query对象生成的page对象进行排序。

MyBatis XML映射文件中的查询语句
图9:XML中的查询语句并未直接使用Page对象

那么真实的排序逻辑是如何实现的呢?我们从一个带分页的请求入手分析:

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

跟进到对应的Controller:com.controller.JixiaokaoheController#page。可以看到,它调用了jixiaokaoheService.queryPage进行查询,并使用MPUtil.sort方法自定义了一个Wrapper条件包装器。

JixiaokaoheController中的page方法
图10:Controller层调用服务方法并传入排序参数

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

JixiaokaoheServiceImpl中的查询方法
图11:Service层使用Query类处理参数

但关键点在于后续的调用链。继续向下跟踪,发现SQL的最终执行依赖于Mapper中定义的selectListView方法。

Mapper接口中的selectListView方法定义
图12:Mapper接口方法定义

XML中对应的selectListView查询语句
图13:XML中的SQL使用${ew.sqlSegment}拼接条件

可以看到,最终的SQL查询条件是通过${ew.sqlSegment}动态拼接的,而这个Wrapper的构建逻辑在MPUtil.sort方法中。

Controller中调用MPUtil.sort生成Wrapper
图14:使用MPUtil工具类生成排序Wrapper

核心漏洞点就在这里。跟进com.utils.MPUtil#sort方法,发现该方法直接对sort参数进行了字符串拼接,并且没有进行任何过滤

MPUtil.sort方法直接拼接sort参数
图15:sort方法中未过滤参数直接拼接,导致注入

构造Payload进行测试:

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

SQL注入漏洞验证成功
图16:发送Payload后,数据库报错信息泄露,证明注入成功

其他SQL注入点分析

MyBatis-Plus在MyBatis的基础上增强,因此MyBatis中#{}预编译和${}拼接的特性同样存在。直接在XML文件中搜索${,发现了多处使用字符串拼接的场景。

全局搜索${符号发现的拼接点
图17:项目存在多处SQL拼接点

以一处提醒功能的SQL为例进行分析,该SQL中tablecolumn字段直接使用了${}拼接。

存在SQL拼接的remindCount查询语句
图18:XML中${table}和${column}存在拼接风险

向上追踪调用链:com.service.impl.CommonServiceImpl#remindCount -> com.controller.CommonController#remindCount,发现全程未对传入的tablecolumn参数进行过滤。

CommonServiceImpl调用通用DAO
图19:Service层调用通用查询方法

CommonController中的remindCount接口
图20:Controller层接口,参数未过滤

根据接口构造Payload进行测试:

/remind/{table}/{column}?type=1&remindStart=2024-01-01&remindEnd=2024-04-01

{table}替换为注入语句:

users union select group_concat(SCHEMA_NAME) from information_schema.SCHEMATA

通过拼接参数实现Union注入
图21:利用参数拼接实现Union查询,注入成功

SQL注入漏洞总结

在对动态表名、ORDER BY等需要拼接SQL的场景进行开发时,必须格外仔细。本次代码审计案例表明,即使在工具类中配置了严格的黑名单过滤,如果业务逻辑流未能正确调用过滤方法,防护便会形同虚设。总体而言,在MyBatis-Plus框架中挖掘SQL注入漏洞,需要审计人员对代码执行流有更清晰的跟踪和更高的耐心。

0x03 任意用户密码重置

审计中无意发现一个功能设计存在缺陷的接口。该重置密码接口使用了@IgnoreAuth注解,意味着无需任何权限认证即可调用。

使用@IgnoreAuth注解的密码重置接口
图22:忽略鉴权的重置密码方法

接口逻辑是根据用户名查询用户,然后将其密码硬编码重置为“123456”。这导致攻击者可以重置任意用户的密码。

GET /springboot57n6g/users/resetPass?username=admin HTTP/1.1

成功重置管理员密码的请求与响应
图23:未授权请求成功将admin用户密码重置

0x04 任意文件上传

文件上传请求包
图24:文件上传功能的HTTP请求

分析com.controller.FileController#upload方法,发现其仅通过最后一个点来截取文件后缀,并结合时间生成新文件名,但未对文件内容、文件头或后缀名进行任何安全检查,导致可上传任意文件。

文件上传接口的核心代码
图25:上传逻辑缺少文件类型检查

0x05 任意文件下载

文件下载请求包,尝试路径穿越
图26:利用文件名参数进行目录穿越

分析com.controller.FileController#download方法,发现文件路径由固定目录/upload/与用户可控的filename参数直接拼接而成。由于未对filename中的路径遍历符(如../)进行过滤,导致可下载服务器上的任意文件。

文件下载接口中路径拼接代码
图27:下载逻辑中存在路径拼接漏洞

0x06 总结

本案例揭示了在快速开发过程中,开发者容易专注于功能实现而忽略安全边界检查。无论是SQL注入过滤逻辑的“绕路而行”,还是未授权接口、不安全的文件操作,都源于对用户输入缺乏充分的信任和验证。安全应当贯穿于软件开发的每一个环节,而非事后补救。希望本次在云栈社区的分享能为开发者们敲响警钟,在追求功能完善的同时,务必筑牢安全防线。




上一篇:冯·诺依曼架构的能耗瓶颈与Electron E1处理器的百倍能效突破
下一篇:Git遭遇性能瓶颈:大规模项目与AI时代下的挑战与替代方案
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-1-11 11:46 , Processed in 0.257247 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

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