1.安装
安装过程 ruoyi-admin\src\main\resources\application-druid.yml配置数据库等信息
2.审计过程
2.1 文件下载漏洞(v4.7.6)
在com.ruoyi.web.controller.common.resourceDownload存在文件下载
首先简单分析下其代码:请求url如:http://127.0.0.1/common/download/resource?resource=1.htm
跟进checkAllowDownload校验的方法,发现是白名单校验,这里htm也在名单中,所以通过校验
可以发现在RuoYiConfig.getProfile();获取到的路径为D:/ruoyi/uploadPath
此路径从配置文件中获取
而后面代码就是下载,在最后的writeBytes后,就是将D:/ruoyi/uploadPath写入到response中。
// 数据库资源地址 downloadPath为D:/ruoyi/uploadPath
String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
// 下载名称 downloadName为uploadPath
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, downloadName);
FileUtils.writeBytes(downloadPath, response.getOutputStream());
但是若依有一处定时任务,该定时任务可以直接调用Bean其对应方法
而在com.ruoyi.common.config包中有一个RuoYiConfig配置类。可以看到该类是与application.yml有关联。因为在application.yml中发现了profile设置了文件下载的路径,于是乎就可以通过定时任务的set方法来对其进行更改
D盘先准备个txt
ruoYiConfig.setProfile("要下载的文件");
稍后执行即可
但不是所有的Bean都能执行的。
跟进package com.ruoyi.quartz.controller.SysJobController中
可以看到其校验
跟进这两处判断看一下
else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
{return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
}
else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
{return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
}
首先第一处是黑名单校验
在containsIgnoreCase中,是一个黑名单校验。对传入的字符串进行校验。
第二处就是白名单校验,首先获取到RuoYiConfig的bean。
return处的containsAnyIgnoreCase功能是:查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
因为在其这里进行了取反,所以该判断也就绕过了。
继续看文件下载
请求url:http://127.0.0.1/common/download/resource?resource=1.pdf
此时RuoYiConfig.getProfile();为D://1.txt
最后会将D://1.txt下载
这里的文件名后缀必须是checkAllowDownload中白名单校验的后缀
2.3 新版SQL注入被修复(v4.7.7)
在com.ruoyi.system.mapper.SysDeptMapper中
都过该Mapper定位到了service层SysDeptServiceImpl
在往上就跟到了SysDeptController
可以发现自定义了一个DataScope注解,根据这个DataScope找到了一个aop。
在这个类中。handleDataScope对传入的dataScope进行了过滤
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
{// 获取当前的用户SysUser currentUser = ShiroUtils.getSysUser();if (currentUser != null){// 如果是超级管理员,则不过滤数据if (!currentUser.isAdmin()){String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),controllerDataScope.userAlias(), permission);}}
}
继续跟进dataScopeFilter方法看一下,可以看到在获取role.getDataScope();之后,进行了判断值。并将创建了一个新的sql语句。大致流程就是service层加入了DataScope注解,就会跑到aop中进行过滤。
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
{StringBuilder sqlString = new StringBuilder();List<String> conditions = new ArrayList<String>();for (SysRole role : user.getRoles()){String dataScope = role.getDataScope();if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope)){continue;}if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())&& !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission))){continue;}if (DATA_SCOPE_ALL.equals(dataScope)){sqlString = new StringBuilder();conditions.add(dataScope);break;}else if (DATA_SCOPE_CUSTOM.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,role.getRoleId()));}else if (DATA_SCOPE_DEPT.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));}else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)){sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",deptAlias, user.getDeptId(), user.getDeptId()));}else if (DATA_SCOPE_SELF.equals(dataScope)){if (StringUtils.isNotBlank(userAlias)){sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));}else{// 数据权限为仅本人且没有userAlias别名不查询任何数据sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));}}conditions.add(dataScope);}// 多角色情况下,所有角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据if (StringUtils.isEmpty(conditions)){sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));}if (StringUtils.isNotBlank(sqlString.toString())){Object params = joinPoint.getArgs()[0];if (StringUtils.isNotNull(params) && params instanceof BaseEntity){BaseEntity baseEntity = (BaseEntity) params;baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");}}
}
2.4 定时任务修复(v4.7.7)
com.ruoyi.quartz.controller#editSave进行了白名单校验。
跟进校验的函数
public static boolean whiteList(String invokeTarget)
{String packageName = StringUtils.substringBefore(invokeTarget, "(");int count = StringUtils.countMatches(packageName, ".");
if (count > 1)
{return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR);
}
Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]);
String beanPackageName = obj.getClass().getPackage().getName();
return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR)&& !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR);
}
违规字符串如下:
在上方中,可以看见String beanPackageName = obj.getClass().getPackage().getName();进行了获取包名
当传入ruoyiConfig类之后获取其包名com.ruoyi.common.config此时匹配成功为true,但是最后return中进行了取反操作。
最外层又进行了取反,最终结果是true。会error。