安全矩阵

 找回密码
 立即注册
搜索
查看: 450|回复: 0

若依CMS代码审计

[复制链接]

181

主题

182

帖子

721

积分

高级会员

Rank: 4

积分
721
发表于 2023-5-27 18:05:55 | 显示全部楼层 |阅读模式
本帖最后由 wangqiang 于 2023-5-27 18:45 编辑

若依CMS代码审计原创

met32 红队蓝军 2023-05-26 15:00 发表于四川
转载自:
若依CMS代码审计

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中。

  1. // 数据库资源地址  downloadPath为D:/ruoyi/uploadPath
  2. String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
  3. // 下载名称 downloadName为uploadPath
  4. String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
  5. response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
  6. FileUtils.setAttachmentResponseHeader(response, downloadName);
  7. 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中 可以看到其校验



跟进这两处判断看一下

  1. else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
  2. {
  3.     return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
  4. }
  5. else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
  6. {
  7.     return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
  8. }
复制代码
首先第一处是黑名单校验


在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进行了过滤

  1. protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
  2. {
  3.     // 获取当前的用户
  4.     SysUser currentUser = ShiroUtils.getSysUser();
  5.     if (currentUser != null)
  6.     {
  7.         // 如果是超级管理员,则不过滤数据
  8.         if (!currentUser.isAdmin())
  9.         {
  10.             String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
  11.             dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
  12.                             controllerDataScope.userAlias(), permission);
  13.         }
  14.     }
  15. }
复制代码
继续跟进dataScopeFilter方法看一下,可以看到在获取role.getDataScope();之后,进行了判断值。并将创建了一个新的sql语句。
大致流程就是service层加入了DataScope注解,就会跑到aop中进行过滤。
  1. public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
  2. {
  3.     StringBuilder sqlString = new StringBuilder();
  4.     List<String> conditions = new ArrayList<String>();

  5.     for (SysRole role : user.getRoles())
  6.         {
  7.             String dataScope = role.getDataScope();
  8.             if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope))
  9.             {
  10.                 continue;
  11.             }
  12.             if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
  13.                 && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
  14.             {
  15.                 continue;
  16.             }
  17.             if (DATA_SCOPE_ALL.equals(dataScope))
  18.             {
  19.                 sqlString = new StringBuilder();
  20.                 conditions.add(dataScope);
  21.                 break;
  22.             }
  23.             else if (DATA_SCOPE_CUSTOM.equals(dataScope))
  24.             {
  25.                 sqlString.append(StringUtils.format(
  26.                     " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
  27.                     role.getRoleId()));
  28.             }
  29.             else if (DATA_SCOPE_DEPT.equals(dataScope))
  30.             {
  31.                 sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
  32.             }
  33.             else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
  34.             {
  35.                 sqlString.append(StringUtils.format(
  36.                     " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
  37.                     deptAlias, user.getDeptId(), user.getDeptId()));
  38.             }
  39.             else if (DATA_SCOPE_SELF.equals(dataScope))
  40.             {
  41.                 if (StringUtils.isNotBlank(userAlias))
  42.                 {
  43.                     sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
  44.                 }
  45.                 else
  46.                 {
  47.                     // 数据权限为仅本人且没有userAlias别名不查询任何数据
  48.                     sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
  49.                 }
  50.             }
  51.             conditions.add(dataScope);
  52.         }

  53.     // 多角色情况下,所有角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据
  54.     if (StringUtils.isEmpty(conditions))
  55.     {
  56.         sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
  57.     }

  58.     if (StringUtils.isNotBlank(sqlString.toString()))
  59.     {
  60.         Object params = joinPoint.getArgs()[0];
  61.         if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
  62.         {
  63.             BaseEntity baseEntity = (BaseEntity) params;
  64.             baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
  65.         }
  66.     }
  67. }
复制代码

2.4 定时任务修复(v4.7.7)
com.ruoyi.quartz.controller#editSave进行了白名单校验。

跟进校验的函数

  1. public static boolean whiteList(String invokeTarget)
  2. {
  3.     String packageName = StringUtils.substringBefore(invokeTarget, "(");
  4. int count = StringUtils.countMatches(packageName, ".");
  5. if (count > 1)
  6. {
  7.     return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR);
  8. }
  9. Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]);
  10. String beanPackageName = obj.getClass().getPackage().getName();
  11. return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR)
  12.     && !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR);
  13. }
复制代码

违规字符串如下:

在上方中,可以看见String beanPackageName = obj.getClass().getPackage().getName();进行了获取包名当传入ruoyiConfig类之后获取其包名

com.ruoyi.common.config此时匹配成功为true,但是最后return中进行了取反操作。
最外层又进行了取反,最终结果是true。会error。




回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-4-16 15:05 , Processed in 0.013521 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表