RuoYi 多版本代码审计
SQL 注入 RuoYi <= 4.6.1
漏洞成因
RuoYi <= 4.6.1 版本的 mybatis 数据库中使用了 ${}
漏洞复现
到系统管理 —-> 用户管理界面下,存在 SQL 注入
对应的 POST 数据包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| POST /system/role/list HTTP/1.1 Host: localhost Content-Length: 179 Pragma: no-cache Cache-Control: no-cache sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Microsoft Edge";v="104" Accept: application/json, text/javascript, */*; q=0.01 Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequest sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36 Edg/104.0.1293.70 sec-ch-ua-platform: "Windows" Origin: http://localhost Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://localhost/system/role 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,ja;q=0.5,zh-TW;q=0.4 Cookie: Idea-3800bf0b=ca6f81a2-cc80-4b2d-9111-1085adff8048; JSESSIONID=ac4b0550-9f78-49a8-8bb6-bc5b58cbdda2 Connection: close
pageSize=&pageNum=&orderByColumn=&isAsc=&roleName=&roleKey=&status=¶ms[beginTime]=¶ms[endTime]=¶ms[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))
|
对应的字段存在 SQL 注入,payload:
1
| pageSize=&pageNum=&orderByColumn=&isAsc=&roleName=&roleKey=&status=¶ms[beginTime]=¶ms[endTime]=¶ms[dataScope]=and extractvalue(1,concat(0x7e,(select database()),0x7e))
|
关于 Java SQL 注入的 Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Component public class SqlInjectionFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest req=(HttpServletRequest)servletRequest; HttpServletRequest res=(HttpServletRequest)servletResponse; Enumeration params = req.getParameterNames(); String sql = ""; while (params.hasMoreElements()) { String name = params.nextElement().toString(); String[] value = req.getParameterValues(name); for (int i = 0; i < value.length; i++) { sql = sql + value[i]; } } if (sqlValidate(sql)) { throw new IOException("您发送请求中的参数中含有非法字符"); } else { chain.doFilter(servletRequest,servletResponse); } }
protected static boolean sqlValidate(String str) { str = str.toLowerCase(); String badStr = "'|and|exec|execute|insert|select|delete|update|count|drop|*|%|chr|mid|master|truncate|" + "char|declare|sitename|net user|xp_cmdshell|;|or|-|+|,|like'|and|exec|execute|insert|create|drop|" + "table|from|grant|use|group_concat|column_name|" + "information_schema.columns|table_schema|union|where|select|delete|update|order|by|count|*|" + "chr|mid|master|truncate|char|declare|or|;|-|--|+|,|like|//|/|%|#"; String[] badStrs = badStr.split("\\|"); for (int i = 0; i < badStrs.length; i++) { if (str.indexOf(badStrs[i]) >= 0) { return true; } } return false; } }
|
漏洞修复
1、SysDeptMapper.xml
中的updateParentDeptStatus
节点使用了${ancestors}
,修改相关逻辑。转成数组方式修改部门状态。
1 2 3 4 5 6 7 8 9 10 11
|
private void updateParentDeptStatusNormal(Dept dept) { String ancestors = dept.getAncestors(); Long[] deptIds = Convert.toLongArray(ancestors); deptMapper.updateDeptStatusNormal(deptIds); }
|
2、数据权限相关使用了${params.dataScope}
,DataScopeAspect.java
数据过滤处理时添加clearDataScope
拼接权限sql
前先清空params.dataScope
参数防止注入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class DataScopeAspect { ...... @Before("dataScopePointCut()") public void doBefore(JoinPoint point) throws Throwable { clearDataScope(point); handleDataScope(point); }
private void clearDataScope(final JoinPoint joinPoint) { Object params = joinPoint.getArgs()[0]; if (StringUtils.isNotNull(params) && params instanceof BaseEntity) { BaseEntity baseEntity = (BaseEntity) params; baseEntity.getParams().put(DATA_SCOPE, ""); } } ...... }
|
目录遍历 RuoYi <= v4.5.0
检测漏洞:CommonController.java
,/common/download/resource
接口是否包含checkAllowDownload
用于检查文件是否可下载,如果没有此方法则需要修改,防止被下载关键信息。
漏洞复现
略
修复手段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
@GetMapping("/common/download/resource") public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response) throws Exception { try { if (!FileUtils.checkAllowDownload(resource)) { throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource)); } String localPath = Global.getProfile(); String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX); String downloadName = StringUtils.substringAfterLast(downloadPath, "/"); response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); FileUtils.setAttachmentResponseHeader(response, downloadName); FileUtils.writeBytes(downloadPath, response.getOutputStream()); } catch (Exception e) { log.error("下载文件失败", e); } }
public static boolean checkAllowDownload(String resource) { if (StringUtils.contains(resource, "..")) { return false; }
if (ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource))) { return true; }
return false; }
|
SQL注入攻击 RuoYi <= v3.2.0
若依管理系统使用了PageHelper,PageHelper提供了排序(Order by)的功能,前端直接传参完成排序。系统没有做字符检查,导致存在被注入的风险,最终造成数据库中存储的隐私信息全部泄漏。
检测漏洞:BaseController.java
是否包含 String orderBy = pageDomain.getOrderBy();
,如果没有字符检查需要修改,防止被执行注入攻击。
解决方案:升级版本到 >=v.3.2.0
,或者重新添加字符检查String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
,防止注入绕过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.ruoyi.common.utils.sql;
import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.utils.StringUtils;
public class SqlUtil {
public static String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
public static String escapeOrderBySql(String value) { if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) { throw new BaseException("参数不符合规范,不能进行查询"); } return value; }
public static boolean isValidOrderBySql(String value) { return value.matches(SQL_PATTERN); } }
|
shiro550 RuoYi <= v4.3.0
若依管理系统使用了Apache Shiro,Shiro 提供了记住我(RememberMe)的功能,下次访问时无需再登录即可访问。系统将密钥硬编码在代码里,且在官方文档中并没有强调修改该密钥,导致框架使用者大多数都使用了默认密钥。攻击者可以构造一个恶意的对象,并且对其序列化、AES加密、base64编码后,作为cookie的rememberMe字段发送。Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞,进而在目标机器上执行任意命令。
检测漏洞:ShiroConfig.java
是否包含 fCq+/xW488hMTCD+cmJ3aQ==
,如果是使用的默认密钥则需要修改,防止被执行命令攻击。
解决方案:升级版本到 >=v.4.3.1
,并且重新生成一个新的秘钥替换cipherKey
,保证唯一且不要泄漏。
1 2 3 4 5
| shiro: cookie: cipherKey: zSyK5Kp6PZAAjlT+eeNMlg==
|
1 2 3 4
| KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecretKey deskey = keygen.generateKey(); System.out.println(Base64.encodeToString(deskey.getEncoded()));
|