Sql注入

常见基本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
57
-- 查询users表所有列
SELECT * FROM users;

-- 查询users表特定列:id, username, email
SELECT id, username, email FROM users;

-- 带条件查询
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE username = 'admin' AND status = 1;

-- 模糊查询
-- 查询名字以 'admin' 开头的用户
SELECT * FROM users WHERE username LIKE 'admin%';

-- 查询名字包含 'test' 的用户
SELECT * FROM users WHERE username LIKE '%test%';

-- 排序 (ORDER BY)
-- 按 ID 升序
SELECT * FROM users ORDER BY id ASC;

-- 按创建时间降序
SELECT * FROM users ORDER BY create_time DESC;

-- 聚合函数
-- 统计总行数
SELECT COUNT(*) FROM users;

-- 计算平均值
SELECT AVG(age) FROM users;

-- 分组统计
SELECT role, COUNT(*) FROM users GROUP BY role;

-- 联合查询 (UNION)
SELECT id, username FROM users UNION SELECT 1, database();

-- 插入数据 (INSERT)
-- 插入单条数据
INSERT INTO users (username, password, email) VALUES ('test_user', '123456', 'test@example.com');

-- 插入多条数据
INSERT INTO users (username, password) VALUES ('user1', 'pass1'), ('user2', 'pass2');

-- 更新数据 (UPDATE) 注意:一定要带 WHERE 条件,否则会更新整张表
-- 更新特定用户的密码
UPDATE users SET password = 'new_password' WHERE id = 10;

-- 同时更新多个字段
UPDATE users SET status = 0, update_time = NOW() WHERE username = 'admin';

-- 删除数据 (DELETE) 注意:一定要带 WHERE 条件,否则会清空整张表
-- 删除特定 ID 的记录
DELETE FROM users WHERE id = 5;

-- 删除状态为禁用的用户
DELETE FROM users WHERE status = -1;

Mybatis框架#

  • #{} 用于预编译的 SQL 语句中,是预编译处理(安全)。
  • ${} 则用于动态 SQL 语句中,是直接字符串替换(危险),字符串拼接。

Mybatis Plus框架#

审计MyBatis Plus框架中的SQL注入问题,核心在于识别动态SQL拼接的场景。

在 MyBatis/MyBatis Plus 中,MyBatis Plus 完全兼容 MyBatis的语法:

  • #{} 用于预编译的 SQL 语句中,是预编译处理(安全)。
  • ${} 则用于动态 SQL 语句中,是直接字符串替换(危险),字符串拼接。

语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 增加:useGeneratedKeys 获取自增主键 -->
        INSERT INTO users (name, email) VALUES (#{name}, #{email})
    </insert>
    <!-- 删除 -->
    <delete id="deleteUserById" parameterType="int">
        DELETE FROM users WHERE id = #{id}
    </delete>
    <!-- 修改 -->
        UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
    </update>
    <!-- 查询:resultType 指定返回类型 -->
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>

JeecgBoot 3.9.0 /sys/dict/loadTreeData tableName参数SQL 注入#

  1. SysDictController.java , loadTreeData 接口直接接收了前端传来的 tableName 参数。
 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
/**
	 * 查询数据 查出所有部门,并以树结构数据格式响应给前端
	 * 
	 * @return
	 */
	@RequestMapping(value = "/queryTreeList", method = RequestMethod.GET)
	public Result<List<SysDepartTreeModel>> queryTreeList(@RequestParam(name = "ids", required = false) String ids) {
		Result<List<SysDepartTreeModel>> result = new Result<>();
		try {
			// 从内存中读取
//			List<SysDepartTreeModel> list =FindsDepartsChildrenUtil.getSysDepartTreeList();
//			if (CollectionUtils.isEmpty(list)) {
//				list = sysDepartService.queryTreeList();
//			}
			if(oConvertUtils.isNotEmpty(ids)){
				List<SysDepartTreeModel> departList = sysDepartService.queryTreeList(ids);
				result.setResult(departList);
			}else{
				List<SysDepartTreeModel> list = sysDepartService.queryTreeList();
				result.setResult(list);
			}
			result.setSuccess(true);
		} catch (Exception e) {
			log.error(e.getMessage(),e);
		}
		return result;
	}
  1. 处理 (Service 层) 在 SysDictServiceImpl.java 的 queryTreeList 方法中

    有一段逻辑专门用来分隔 tableName :String[] arr = table.split(" (?i)where ");

    由于一系列操作导致注入语句可以被赋值给filterSql:filterSql = oConvertUtils.getString(arr[1], null);

    然后会进行SQL检测:SqlInjectionUtil.specialFilterContentForDictSql(filterSql);

    private static String specialDictSqlXssStr = "exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|+|--";

    这个filterSql又被放入:queryParams Map中queryParams.put("_tableFilterSql", filterSql);

 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
	@Override
	public List<TreeSelectModel> queryTreeList(Map<String, String> query, String table, String text, String code, String pidField, String pid, String hasChildField, int converIsLeafVal) {
		//为了防止sql(jeecg提供了防注入的方法,可以在拼接 SQL 语句时自动对参数进行转义,避免SQL注入攻击)
		// 1.针对采用 ${}写法的表名和字段进行转义和check
        //update-begin---author:chenrui ---date:20251015  for:[QQYUN-13741]【客户问题 南自】online表单自定义树 表后边加条件时 不生效------------
        // 分割SQL获取表名和条件
        String filterSql = null;
        if(table.toLowerCase().indexOf(DataBaseConstant.SQL_WHERE)>0){
            String[] arr = table.split(" (?i)where ");
            table = arr[0];
            filterSql = oConvertUtils.getString(arr[1], null);
        }
        table = SqlInjectionUtil.getSqlInjectTableName(table);
		text = SqlInjectionUtil.getSqlInjectField(text);
		code = SqlInjectionUtil.getSqlInjectField(code);
		pidField = SqlInjectionUtil.getSqlInjectField(pidField);
		hasChildField = SqlInjectionUtil.getSqlInjectField(hasChildField);

		if(oConvertUtils.isEmpty(text) || oConvertUtils.isEmpty(code)){
			log.warn("text={},code={}", text, code);
			log.warn("加载树字典参数有误,text和code不允许为空!");
			return null;
		}
		
		// 2.检测最终SQL是否存在SQL注入风险
		String dictCode = table + "," + text + "," + code;
		SqlInjectionUtil.filterContentMulti(dictCode);
        SqlInjectionUtil.specialFilterContentForDictSql(filterSql);

		// 【QQYUN-6533】表字典白名单check
		sysBaseAPI.dictTableWhiteListCheckByDict(table, text, code);
		// 3.表字典SQL表名黑名单 Check
		if(!dictQueryBlackListHandler.isPass(dictCode)){
			log.error("Sql异常:{}", dictQueryBlackListHandler.getError());
			return null;
		}
		// 4.检测查询条件是否存在SQL注入
		Map<String, String> queryParams = queryParams = new HashMap<>(4);
		if (query != null) {
			for (Map.Entry<String, String> searchItem : query.entrySet()) {
				String fieldName = searchItem.getKey();
				queryParams.put(SqlInjectionUtil.getSqlInjectField(fieldName), searchItem.getValue());
			}
		}
        // 代码逻辑说明: [QQYUN-13741]【客户问题 南自】online表单自定义树 表后边加条件时 不生效------------
        if(oConvertUtils.isNotEmpty(filterSql)){
            queryParams.put("_tableFilterSql", filterSql);
        }
		
		return baseMapper.queryTreeList(queryParams, table, text, code, pidField, pid, hasChildField, converIsLeafVal);
	}
  1. 最后在 XML 中,MyBatis 使用 ${} 直接执行了_tableFilterSql

    1
    2
    3
    
    <when test="key == '_tableFilterSql'">
        and ${value}  <!-- 这里执行的就是 filterSql 的内容 -->
    </when>
 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
<select id="queryTreeList" parameterType="Object" resultType="org.jeecg.modules.system.model.TreeSelectModel">
		select ${text} as "title",
			   ${code} as "key",
			   <if test="hasChildField != null and hasChildField != ''">
				   <choose>
					   <when test="converIsLeafVal!=null and converIsLeafVal==1">
						   (case when ${hasChildField} = '1' then 0 else 1 end) as isLeaf,
					   </when>
					   <otherwise>
						   ${hasChildField} as isLeaf,
					   </otherwise>
				   </choose>
			   </if>
			   ${pidField} as parentId
			   from ${table}
			   where
		       <!-- 父ID条件 -->
			   <if test="query == null">
				   <choose>
					   <when test="pid != null and pid != ''">
						   ${pidField} = #{pid}
					   </when>
					   <otherwise>
						   (${pidField} = '' OR ${pidField} IS NULL)
					   </otherwise>
				   </choose>
			   </if>
			   <!-- 查询条件组装 -->
			   <if test="query!= null">
			       1 = 1
				   <foreach collection="query.entrySet()" item="value"  index="key" >
					   <choose>
						   <when test="key == 'tenant_id'">
							   and tenant_id = #{value}
						   </when>
			               <when test="key == '_tableFilterSql'">
                               and ${value}
                           </when>
						   <otherwise>
							   and ${key} LIKE #{value}
						   </otherwise>
					   </choose>
				   </foreach>
					 <!-- 【issues/3709】自定义树查询条件没有处理父ID,没有树状结构了 -->
					 <choose>
						 <when test="pid != null and pid != ''">
							 and ${pidField} = #{pid}
						 </when>
						 <otherwise>
							 and (${pidField} = '' OR ${pidField} IS NULL)
						 </otherwise>
					 </choose>
			   </if>
	</select>

POC

1
http://127.0.0.1:8080/sys/dict/loadTreeData?tableName=sys_user where 1=1 and length(database())>0

JDBC 框架#