Java代码审计中的sql注入2.0

Java代码审计中的sql注入2.0

参考

Java OWASP 中的 SQL 注入代码审计 | Drunkbaby’s Blog (drun1baby.top)

JAVA常用框架SQL注入审计-先知社区 (aliyun.com)

预编译真的能完美防御SQL注入吗?_预编译能完全防止sql注入吗-CSDN博客

【MyBatis】预编译SQL与即时SQL_mybatis sql预处理-CSDN博客

MyBatis-Plus快速入门-(干货满满+超详细)_mybatis-plus 入门-CSDN博客

Mybatis-Plus 的 SQL 注入探讨

使用apply直接拼接sql语句

实际的apply场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 将此方法绑定到 HTTP 请求路径 /mybatis_plus/mpVuln01
@RequestMapping("/mybatis_plus/mpVuln01")
// 定义一个控制器方法,接收两个参数 name 和 id,并返回一个 Employee 对象
public Employee mpVuln01(String name, String id) {
// 创建一个查询条件构造器
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
// 添加等值查询条件:WHERE name = #{name}
wrapper.eq("name", name);
// 直接拼接 SQL 片段,变成:AND id=xxx(存在 SQL 注入风险)
wrapper.apply("id=" + id);
// 执行查询,只返回一条符合条件的记录
Employee employee = employeeMapper.selectOne(wrapper);
// 返回查询结果
return employee;
}

可以直接看到这里的参数传入是直接拼接的,存在sql注入漏洞

但是由于selectOne函数的存在(只返回一条符合条件的记录),这里只能进行报错注入,万能密码会返回全部数据,由于特殊函数限制,无法注入成功

apply场景的防护

在语句后加上参数占位符{0}即可

1
2
3
4
5
6
@RequestMapping("/mybatis_plus/mpSec02")  
public List<Employee> mpSec02( String id) {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.apply("id={0}",id);
return employeeMapper.selectList(wrapper);
}

last方法产生的sql注入

last()方法重写后有两个方法

1
2
last(String lastSql)
last(boolean condition, String lastSql)

此方法中lastSql可以直接用来编写SQL语句,写一个新接口

猜测实战黑盒利用场景为在线sql语句执行程序,可以编写新接口进行渗透

这里直接使用Drunkbaby师傅的漏洞环境

项目地址:[JavaSecurityLearning/JavaSecurity/Java 代码审计/CodeReview/JavaSec-Code/MybatisPluSqli at main · Drun1baby/JavaSecurityLearning (github.com)](https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/Java 代码审计/CodeReview/JavaSec-Code/MybatisPluSqli)

1
2
3
4
5
6
@RequestMapping("/mybatis_plus/last")
public List<Employee> mpVuln03( String id) {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.last("order by " + id);
return employeeMapper.selectList(wrapper);
}

启动环境时出现报错

图片

在MybatisPluSqliApplication.java文件加上一行代码即可

1
@MapperScan("com.drunkbaby.mapper")

文件全部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.drunkbaby;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.drunkbaby.mapper")
public class MybatisPluSqliApplication {

public static void main(String[] args) {
SpringApplication.run(MybatisPluSqliApplication.class, args);
}

}

实操后发现不需要payload也是直接爆出所有数据的

图片

可能是我的操作出了什么问题,但是本来控制代码就不是很多,感觉应该是没有问题的

正常payload

1
http://localhost:8081/mybatis_plus/last?id=1%20or%201=1

order by相关的sql注入

刚开始想要搭这个环境的主要原因是没见过order by后拼接万能密码的

再次复习了一下order by相关

在 SQL 中,ORDER BY 子句用于对查询结果进行排序。ORDER BY 后的参数有以下要求和注意事项:

  1. 列名或别名
  • 可以使用表中的列名或查询中定义的列别名。

  • 示例:

    1
    SELECT name, age FROM students ORDER BY age;
  1. 列的序号(sql注入中常用于判断列数)
  • 可以使用 SELECT 子句中列的序号(从 1 开始)。

  • 示例:

    1
    SELECT name, age FROM students ORDER BY 2; -- 按第二列 age 排序
  1. 排序方式
  • 默认是升序(ASC),也可以显式指定。

  • 降序使用 DESC

  • 示例:

    1
    SELECT name, age FROM students ORDER BY age DESC;

    4.多个排序条件

  • 可以指定多个列,按优先级依次排序。

  • 示例:

    1
    SELECT name, age, grade FROM students ORDER BY grade DESC, age ASC;
  1. 表达式或计算结果
  • 可以使用表达式或计算结果进行排序。

  • 示例:

    1
    SELECT name, salary, bonus FROM employees ORDER BY (salary + bonus) DESC;
  1. NULL 值排序
  • 不同数据库对

    1
    NULL

    的排序处理可能不同:

    • 一般情况下,NULL 在升序中排在最前,在降序中排在最后。
    • 可以使用 NULLS FIRSTNULLS LAST 明确指定。
  • 示例:

    1
    SELECT name, age FROM students ORDER BY age ASC NULLS LAST;

注意事项:

  • 列必须存在:ORDER BY 中引用的列或别名必须在查询结果中有效。
  • 性能影响:排序操作可能会影响查询性能,尤其是大数据集时。

并且order by语句后无法拼接变量,但是可以拼接sql语句,如果万能密码能够使用,猜测是被当作sql语句执行

exists/notExists 拼接产生的SQL 注入

1
2
3
4
5
6
@RequestMapping("/mybatis_plus/mpVuln04")  
public List<Employee> mpVuln04( String id) {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.exists("select * from employees where id = " + id);
return employeeMapper.selectList(wrapper);
}

having 语句

1
2
3
4
5
6
@RequestMapping("/mybatis_plus/mpVuln06")  
public List<Employee> mpVuln06( String id) {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select().groupBy("id").having("id >" + id);
return employeeMapper.selectList(wrapper);
}

order by 语句(写order by 的时候不能预编译,下面有一个模块详细讲解)

相关接口写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public List<Employee> orderby01( String id) {  
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select().orderBy(true, true, id);
return employeeMapper.selectList(wrapper);
}

@RequestMapping("/mybatis_plus/orderby02")
public List<Employee> orderby02( String id) {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select().orderByAsc(id);
return employeeMapper.selectList(wrapper);
}

@RequestMapping("/mybatis_plus/orderby03")
public List<Employee> orderby03( String id) {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select().orderByDesc(id);
return employeeMapper.selectList(wrapper);
}

group By/order by

inSql/notInSql

1
2
3
4
5
6
@RequestMapping("/mybatis_plus/insql")  
public List<Employee> inSql( String id) {
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select().inSql(id, "select * from employees where id >" + id);
return employeeMapper.selectList(wrapper);
}

这几种方法都是不支持预编译绑定参数,会直接将字符串拼接到最终 SQL 末尾**,**不会做任何参数绑定或转义处理。导致攻击者传入恶意代码造成语句拼接,用户信息泄露。

分页插件的 SQL 注入情况

分页插件自带的 addOrder() 方法

  • 配置分页插件

    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
    package com.drunkbaby.config;  

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class MybatisPlusConfig {

    /**
    * 注册插件
    */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {

    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加分页插件
    PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor();
    // 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false
    pageInterceptor.setOverflow(false);
    // 单页分页条数限制,默认无限制
    pageInterceptor.setMaxLimit(500L);
    // 设置数据库类型
    pageInterceptor.setDbType(DbType.MYSQL);

    interceptor.addInnerInterceptor(pageInterceptor);
    return interceptor;
    }
    }
  • 漏洞接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RequestMapping("/mybatis_plus/PageVul01")  
    public List<Person> mybatisPlusPageVuln01(Long page, Long size, String id){
    QueryWrapper<Person> queryWrapper = new QueryWrapper<>();
    Page<Person> personPage = new Page<>(1,2);
    personPage.addOrder(OrderItem.asc(id));
    IPage<Person> iPage= personMapper.selectPage(personPage, queryWrapper);
    List<Person> people = iPage.getRecords();
    return people;
    }

    这里的 Page<Person> personPage = new Page<>(1,2); 的参数由自己定义

    这里对应的 payload

    1
    2
    3
    4
    ?id=1%20and%20extractvalue(1,concat(0x7e,(select%20database()),0x7e)))

    // 或者是
    ?id=1' and sleep(5)

    必须是通过盲注的形式,如果是普通的注入,是不会有回显的;因为这里分页查找,size 就把你的数据数量限定死了,如果超过这个数据就会报错,所以只能盲注。

pagehelper

这里的原理就和 order by 一样,不赘述了

因为Order by排序时不能进行预编译处理,所以在使用插件时需要额外注意如下function,同样会存在SQL注入风险:

  • com.github.pagehelper.Page
    • 主要是setOrderBy(java.lang.String)方法
  • com.github.pagehelper.page.PageMethod
    • 主要是startPage(int,int,java.lang.String)方法
  • com.github.pagehelper.PageHelper
    • 主要是startPage(int,int,java.lang.String)方法

mybatis Plus SQL 注入的修复

以上列出的所有方法,除了apply方法可以使用参数占位符进行防护,其他方法全部不支持预编译绑定参数,会直接将字符串拼接到最终 SQL 末尾**,**不会做任何参数绑定或转义处理。导致攻击者传入恶意代码造成语句拼接,用户信息泄露。

能想到的防护方法只有对传入的参数进行检测和过滤

还有写Filter进行过滤

过滤器集成:Drun1baby/AWD-AWDP_SecFilters: 为了准备 AWD,写了个 Filter 的集合 (github.com)

Hibernate框架下的SQL注入

Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。

一般是默认进行预编译的

Hibernate可以使用hql来执行SQL语句,也可以直接执行SQL语句,无论是哪种方式都有可能导致SQL注入

HQL

hql语句:

1
String hql = "from People where username = '" + username + "' and password = '" + password + "'";

这种拼接方式存在SQL注入

正确使用以下几种HQL参数绑定的方式可以有效避免注入的产生:

1.命名参数(named parameter)

1
2
3
4
Query<User> query = session.createQuery("from users name = ?1", User.class);
String parameter = "g1ts";
Query<User> query = session.createQuery("from users name = :name", User.class);
query.setParameter("name", parameter);

2.位置参数(Positional parameter)

1
2
3
String parameter = "g1ts";
Query<User> query = session.createQuery("from users name = ?1", User.class);
query.setParameter(1, parameter);

3.命名参数列表(named parameter list)

1
2
3
List<String> names = Arrays.asList("g1ts", "g2ts");
Query<User> query = session.createQuery("from users where name in (:names)", User.class);
query.setParameter("names", names);

4.类实例(JavaBean)

1
2
3
user1.setName("g1ts");
Query<User> query = session.createQuery("from users where name =:name", User.class);
query.setProperties(user1);

5.HQL拼接方法

这种方式是最常用,而且容易忽视且容易被注入的,通常做法就是对参数的特殊字符进行过滤,推荐大家使用 Spring工具包的StringEscapeUtils.escapeSql()方法对参数进行过滤:

1
2
3
4
5
import org.apache.commons.lang.StringEscapeUtils;
public static void main(String[] args) {
String str = StringEscapeUtils.escapeSql("'");
System.out.println(str);
}

SQL

Hibernate支持使用原生SQL语句执行,所以其风险和JDBC是一致的,直接使用拼接的方法时会导致SQL注入

语句如下:

1
Query<People> query = session.createNativeQuery("select * from user where username = '" + username + "' and password = '" + password + "'");

正确写法:

1
2
3
String parameter = "g1ts";
Query<User> query = session.createNativeQuery("select * from user where name = :name");
query.setParameter("name",parameter);

预编译下的sql注入

原理

预编译是将sql语句参数化,可预编译的语句,如 where语句中的内容是被参数化的。这就是说,预编译仅仅只能防御住可参数化位置的sql注入。那么,对于不可参数化的位置,预编译将没有任何办法。

不可参数化的位置:

  1. 表名、列名
  2. order by、group by
  3. limit
  4. join

我们以order by举例,现在有一个sql语句如下(以下均为伪代码)

1
SELECT * FROM users ORDER BY {user_input};

其中user_input是传递过来的参数,例如 id

1
SELECT * FROM users ORDER BY id;

这个语句是没有问题的,但是如果user_input输入为 id;drop table users –

1
SELECT * FROM users ORDER BY id;drop table users --+

这样就被成功注入了,而这种位置是不可被参数化的,所以是无法通过预编译防御的

如何防御

所以,对于sql注入存在两种情况,可参数化的,不可参数化的。

对于可参数化没商量,直接预编译解决一切。

而对于不可参数化的,只能通过设置白名单,过滤特殊符号,通过加引号强制转为字符串等方式进行拦截。


Java代码审计中的sql注入2.0
http://huang-d1.github.io/2025/06/20/Java代码审计中的sql注入2.0/
作者
huangdi
发布于
2025年6月20日
许可协议