处理关于类似0x7e或0x5c导致的SQL注入漏洞

公司做过一个项目,去年就上线了,6月初检测出来一个漏洞,具体描述如下:

漏洞描述

请求地址: https://taojiang.nyfwypt.com/api/blade-system/tenant/list?updatexml(1,concat(0x7e,user(),0x7e),1)

漏洞描述: SQL注入漏洞

返回信息示例如下:

{"code":500,"success":false,"data":{},"msg":"\n### Error querying database.  Cause: java.sql.SQLException: XPATH syntax error: '\\root@172.22.0.1\\'\n### The error may exist in org/springblade/system/mapper/DictBizMapper.java (best guess)\n### The error may involve defaultParameterMap\n### The error occurred while setting parameters\n### SQL: SELECT id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, is_deleted, dept_id FROM blade_dict_biz WHERE is_deleted = 0 AND (updatexml(1, concat(0x5c, user(), 0x5c), 1) LIKE ?) ORDER BY sort ASC\n### Cause: java.sql.SQLException: XPATH syntax error: '\\root@172.22.0.1\\'\n; uncategorized SQLException; SQL state [HY000]; error code [1105]; XPATH syntax error: '\\root@172.22.0.1\\'; nested exception is java.sql.SQLException: XPATH syntax error: '\\root@172.22.0.1\\'"}

处理方法

刚开始我也挺纳闷的,这个框架本身就是比较成熟的,不会存在SQL注入情况的。

第一次解决这个bug是在2024年6月初,漏洞报告中使用的参数包含0x7e 检查相应的请求和方法,当中有个参数为了动态传值,使用了map,而且接收值的mapper.xml里面使用的是${} 而不是#{} 所以当时比较简单的以为是把这个$符号,替换为#符号,就可以了。

修改前xml内容如下:

<select id="exportUser" resultType="org.springblade.system.user.excel.UserExcel">
 SELECT id, tenant_id, user_type, account, name, real_name, email, phone, 
birthday, role_id, dept_id, post_id 
FROM blade_user ${ew.customSqlSegment}

</select>

修改后xml内容如下:

<select id="exportUser" resultType="org.springblade.system.user.excel.UserExcel">
    	SELECT id, tenant_id, user_type, account, name, real_name, email, phone, birthday,
 		role_id, dept_id, post_id FROM blade_user
    	where 1=1
    	<if test="user.tenantId!=null and user.tenantId != ''">
       	 and tenant_id = #{user.tenantId}
   	 </if>
   	 <if test="user.account!=null and user.account != ''">
   	     and account like concat('%',concat(#{user. account},'%');
   	 </if>
    	<if test="user.realName!=null and user.realName != ''">
 		       and real_name like concat('%',concat(#{user.realName},'%');
    	</if>
 </select>

结果在6月底,又发现了一次类似的漏洞,这次我发现并不完全是上述方面的问题。 而是该框架有个方法存在问题

Condition.getQueryWrapper(dict, DictBiz.class)
public static <T> QueryWrapper<T> getQueryWrapper(Map<String, Object> query, Class<T> clazz) {
        Kv exclude = Kv.create().set("Blade-Auth", "Blade-Auth").set("current", "current").set("size", "size").set("ascs", "ascs").set("descs", "descs");
        return getQueryWrapper(query, exclude, clazz);
    }
public static <T> QueryWrapper<T> getQueryWrapper(Map<String, Object> query, Map<String, Object> exclude, Class<T> clazz) {
        exclude.forEach((k, v) -> {
            query.remove(k);
        });
        QueryWrapper<T> qw = new QueryWrapper();
        qw.setEntity(BeanUtil.newInstance(clazz));
        SqlKeyword.buildCondition(query, qw);
        return qw;
    }
public static void buildCondition(Map<String, Object> query, QueryWrapper<?> qw) {
        if (!Func.isEmpty(query)) {
            query.forEach((k, v) -> {
                if (!Func.hasEmpty(new Object[]{k, v}) && !k.endsWith("_ignore")) {
                    if (k.endsWith("_equal")) {
                        qw.eq(getColumn(k, "_equal"), v);
                    } else if (k.endsWith("_notequal")) {
                        qw.ne(getColumn(k, "_notequal"), v);
                    } else if (k.endsWith("_likeleft")) {
                        qw.likeLeft(getColumn(k, "_likeleft"), v);
                    } else if (k.endsWith("_likeright")) {
                        qw.likeRight(getColumn(k, "_likeright"), v);
                    } else if (k.endsWith("_notlike")) {
                        qw.notLike(getColumn(k, "_notlike"), v);
                    } else if (k.endsWith("_ge")) {
                        qw.ge(getColumn(k, "_ge"), v);
                    } else if (k.endsWith("_le")) {
                        qw.le(getColumn(k, "_le"), v);
                    } else if (k.endsWith("_gt")) {
                        qw.gt(getColumn(k, "_gt"), v);
                    } else if (k.endsWith("_lt")) {
                        qw.lt(getColumn(k, "_lt"), v);
                    } else if (k.endsWith("_datege")) {
                        qw.ge(getColumn(k, "_datege"), DateUtil.parse(String.valueOf(v), "yyyy-MM-dd HH:mm:ss"));
                    } else if (k.endsWith("_dategt")) {
                        qw.gt(getColumn(k, "_dategt"), DateUtil.parse(String.valueOf(v), "yyyy-MM-dd HH:mm:ss"));
                    } else if (k.endsWith("_dateequal")) {
                        qw.eq(getColumn(k, "_dateequal"), DateUtil.parse(String.valueOf(v), "yyyy-MM-dd HH:mm:ss"));
                    } else if (k.endsWith("_datele")) {
                        qw.le(getColumn(k, "_datele"), DateUtil.parse(String.valueOf(v), "yyyy-MM-dd HH:mm:ss"));
                    } else if (k.endsWith("_datelt")) {
                        qw.lt(getColumn(k, "_datelt"), DateUtil.parse(String.valueOf(v), "yyyy-MM-dd HH:mm:ss"));
                    } else if (k.endsWith("_null")) {
                        qw.isNull(getColumn(k, "_null"));
                    } else if (k.endsWith("_notnull")) {
                        qw.isNotNull(getColumn(k, "_notnull"));
                    } else {
                        qw.like(getColumn(k, "_like"), v);
                    }

                }
            });
        }
    }

通过上面跟踪代码可以发现,其实并没有太大的问题,但是它会把参数带到查询的里面去,而且没有去过滤sql注入的关键字信息。

因此为了保险起见,弃用了这存在风险的方法,而采用比较安全的方法比如

  1. 使用封装的安全的Condition.getQueryWrapper方法

    示例:

public R<Code> detail(Code code) {
		Code detail = codeService.getOne(Condition.getQueryWrapper(code));
		return R.data(detail);
	} 
  1. 不使用map对象直接转QueryWrapper,手动构造QueryWrapper对象

    示例:

public R<IPage<Code>> list(@ApiIgnore @RequestParam Map<String, Object> code, Query query) {

		//Condition.getQueryWrapper(code, Code.class)
		QueryWrapper<Code> codeQueryWrapper = new QueryWrapper<>();
		if(code!=null&&code.get("codeName")!=null){
			codeQueryWrapper.lambda().like(Code::getCodeName,code.get("codeName"));
		}
		if(code!=null&&code.get("tableName")!=null){
			codeQueryWrapper.lambda().like(Code::getTableName,code.get("tableName"));
		} 

		IPage<Code> pages = codeService.page(Condition.getPage(query), codeQueryWrapper);
		return R.data(pages);
	} 

漏洞修复验证

请求地址: https://taojiang.nyfwypt.com/api/blade-system/tenant/list?updatexml(1,concat(0x7e,user(),0x7e),1)

请求头参数:

Accept:application/json, text/plain, / Cache-Control:no-cache Authorization:Basic c2FiZXI6c2FiZXJfc2VjcmV0 Blade-Auth:bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRfaWQiOiIwMDAwMDAiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInJlYWxfbmFtZSI6Iuahg-axn-WOvyIsImF2YXRhciI6Imh0dHBzOi8vZ3cuYWxpcGF5b2JqZWN0cy5jb20vem9zL3Jtc3BvcnRhbC9CaWF6ZmFueG1hbU5Sb3h4VnhrYS5wbmciLCJhdXRob3JpdGllcyI6WyJhZG1pbmlzdHJhdG9yIl0sImNsaWVudF9pZCI6InNhYmVyIiwicm9sZV9uYW1lIjoiYWRtaW5pc3RyYXRvciIsImxpY2Vuc2UiOiJwb3dlcmVkIGJ5IGJsYWRleCIsInBvc3RfaWQiOiIxMTIzNTk4ODE3NzM4Njc1MjAxIiwidXNlcl9pZCI6IjExMjM1OTg4MjE3Mzg2NzMzNzEiLCJyb2xlX2lkIjoiMTEyMzU5ODgxNjczODY3NTIwMSIsInNjb3BlIjpbImFsbCJdLCJuaWNrX25hbWUiOiLnrqHnkIblkZgiLCJvYXV0aF9pZCI6IiIsImRldGFpbCI6eyJ0eXBlIjoid2ViIn0sImV4cCI6MTcxOTIyMjUyMywiZGVwdF9pZCI6IjE2ODQ0NTc5Njc3NTM3NDg0ODEiLCJqdGkiOiI0ODFkM2JjMS0wNzRhLTRlYzItYWE0Ny01NjM4ZDE0YjIwMmQiLCJhY2NvdW50IjoiYWRtaW4ifQ.K3swkMOGUG0CIuaq8JNWdtyiaikEOHPMvTGPpezYmRE

请求返回结果

{"code":200,"success":true,"data":[],"msg":"操作成功"}

再检查使用到此方法的相关漏洞,一一排查处理,直到漏洞已修复完毕。