处理关于类似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注入的关键字信息。
因此为了保险起见,弃用了这存在风险的方法,而采用比较安全的方法比如
-
使用封装的安全的Condition.getQueryWrapper方法
示例:
public R<Code> detail(Code code) {
Code detail = codeService.getOne(Condition.getQueryWrapper(code));
return R.data(detail);
}
-
不使用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":"操作成功"}
再检查使用到此方法的相关漏洞,一一排查处理,直到漏洞已修复完毕。