0x01 漏洞介绍 Apache Solr 是一个开源搜索服务器。Apache Solr 的 PKIAuthenticationPlugin 插件存在认证绕过漏洞,攻击者可以构造恶意路径绕过认证机制。
影响版本
5.3.0 <= Apache Solr < 8.11.4
9.0.0 <= Apache Solr < 9.7.0
0x02 环境搭建 1 2 docker pull solr:9.6.0 docker run -d -p 8983:8983 --name my_solr solr:9.6.0
访问成功则表示搭建完成。
0x03 漏洞分析 solr 会将请求丢到 SolrDispatchFilter.doFilter 用于处理 HTTP 请求的过滤逻辑
doFilter
负责处理各种请求和路由,其中 excludedPath
如果为真则不进行后续检查。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java#L199
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 public void doFilter ( ServletRequest _request, ServletResponse _response, FilterChain chain, boolean retry) throws IOException, ServletException { if (!(_request instanceof HttpServletRequest)) return ; HttpServletRequest request = closeShield((HttpServletRequest) _request, retry); HttpServletResponse response = closeShield((HttpServletResponse) _response, retry); if (excludedPath(excludePatterns, request, response, chain)) { return ; } Tracer t = getCores() == null ? GlobalTracer.get() : getCores().getTracer(); request.setAttribute(ATTR_TRACING_TRACER, t); RateLimitManager rateLimitManager = coreService.getService().getRateLimitManager(); try { ServletUtils.rateLimitRequest( rateLimitManager, request, response, () -> { try { dispatch(chain, request, response, retry); } catch (IOException | ServletException | SolrAuthenticationException e) { throw new ExceptionWhileTracing (e); } }); } finally { ServletUtils.consumeInputFully(request, response); SolrRequestInfo.reset(); SolrRequestParsers.cleanupMultipartFiles(request); } }
跟进到 excludedPath
excludedPath
设定了一个排除列表 excludePatterns
,如果命中了正则就表示这个请求不需要进一步处理。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java#L153
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static boolean excludedPath ( List<Pattern> excludePatterns, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String requestPath = getPathAfterContext(request); if (excludePatterns != null ) { for (Pattern p : excludePatterns) { Matcher matcher = p.matcher(requestPath); if (matcher.lookingAt()) { if (chain != null ) { chain.doFilter(request, response); } return true ; } } } return false ; }
查看 excludePatterns
正则列表,是关于静态文件的匹配规则,如果为静态文件则不需要进行后续过滤检查。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/webapp/web/WEB-INF/web.xml#L36
1 2 3 4 <init-param > <param-name > excludePatterns</param-name > <param-value > /partials/.+,/libs/.+,/css/.+,/js/.+,/img/.+,/templates/.+</param-value > </init-param >
非静态文件则返回 doFilter
进入 dispatch
函数。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java#L199
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 public void doFilter ( ServletRequest _request, ServletResponse _response, FilterChain chain, boolean retry) throws IOException, ServletException { if (!(_request instanceof HttpServletRequest)) return ; HttpServletRequest request = closeShield((HttpServletRequest) _request, retry); HttpServletResponse response = closeShield((HttpServletResponse) _response, retry); if (excludedPath(excludePatterns, request, response, chain)) { return ; } Tracer t = getCores() == null ? GlobalTracer.get() : getCores().getTracer(); request.setAttribute(ATTR_TRACING_TRACER, t); RateLimitManager rateLimitManager = coreService.getService().getRateLimitManager(); try { ServletUtils.rateLimitRequest( rateLimitManager, request, response, () -> { try { dispatch(chain, request, response, retry); } catch (IOException | ServletException | SolrAuthenticationException e) { throw new ExceptionWhileTracing (e); } }); } finally { ServletUtils.consumeInputFully(request, response); SolrRequestInfo.reset(); SolrRequestParsers.cleanupMultipartFiles(request); } }
dispatch
通过 authenticateRequest
进行鉴权验证。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java#L235
跟进 authenticateRequest
authenticateRequest
会调用插件进行身份校验,其中 PKIAuthenticationPlugin.HEADER
或 PKIAuthenticationPlugin.HEADER_V2
不为空则使用 PKIAuthenticationPlugin
插件进行验证。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java#L307
这两个值对应以下两个关键词。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java#L557
1 2 public static final String HEADER = "SolrAuth" ;public static final String HEADER_V2 = "SolrAuthV2" ;
因此只要 Header 里面携带了其中一个就会使用 PKIAuthenticationPlugin
验证。
继续看 PKIAuthenticationPlugin
的验证逻辑 doAuthenticate
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java#L136
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 @SuppressForbidden(reason = "Needs currentTimeMillis to compare against time in header") @Override public boolean doAuthenticate ( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { long receivedTime = System.currentTimeMillis(); String requestURI = request.getRequestURI(); if (requestURI.endsWith(PublicKeyHandler.PATH)) { assert false : "Should already be handled by SolrDispatchFilter.authenticateRequest" ; numPassThrough.inc(); filterChain.doFilter(request, response); return true ; } PKIHeaderData headerData = null ; String headerV2 = request.getHeader(HEADER_V2); String headerV1 = request.getHeader(HEADER); if (headerV1 == null && headerV2 == null ) { return sendError(response, true , "No PKI auth header was provided" ); } else if (headerV2 != null ) { int nodeNameEnd = headerV2.indexOf(' ' ); if (nodeNameEnd <= 0 ) { return sendError(response, true , "Could not parse node name from SolrAuthV2 header." ); } headerData = decipherHeaderV2(headerV2, headerV2.substring(0 , nodeNameEnd)); } else if (headerV1 != null && acceptPkiV1) { List<String> authInfo = StrUtils.splitWS(headerV1, false ); if (authInfo.size() != 2 ) { return sendError(response, false , "Invalid SolrAuth header: " + headerV1); } headerData = decipherHeader(authInfo.get(0 ), authInfo.get(1 )); } if (headerData == null ) { return sendError(response, true , "Could not validate PKI header." ); } long elapsed = receivedTime - headerData.timestamp; if (elapsed > MAX_VALIDITY) { return sendError(response, true , "Expired key request timestamp, elapsed=" + elapsed); } final Principal principal = "$" .equals(headerData.userName) ? CLUSTER_MEMBER_NODE : new BasicUserPrincipal (headerData.userName); numAuthenticated.inc(); filterChain.doFilter(wrapWithPrincipal(request, principal), response); return true ; }
其中有一段关于路径的验证,如果末尾以 PublicKeyHandler.PATH
结尾就跳过校验。
1 2 3 4 5 6 7 if (requestURI.endsWith(PublicKeyHandler.PATH)) { assert false : "Should already be handled by SolrDispatchFilter.authenticateRequest" ; numPassThrough.inc(); filterChain.doFilter(request, response); return true ; }
搜索一下 PublicKeyHandler.PATH
。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/security/PublicKeyHandler.java#L33
1 public static final String PATH = "/admin/info/key" ;
只要路由以 /admin/info/key
结尾则不进行校验。
问题就变成了,如何构建末尾是 /admin/info/key
的路径,且还能访问想要访问的页面。
回到 dispatch
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java#L235
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 private void dispatch ( FilterChain chain, HttpServletRequest request, HttpServletResponse response, boolean retry) throws IOException, ServletException, SolrAuthenticationException { AtomicReference<HttpServletRequest> wrappedRequest = new AtomicReference <>(); authenticateRequest(request, response, wrappedRequest); if (wrappedRequest.get() != null ) { request = wrappedRequest.get(); } ...... HttpSolrCall call = getHttpSolrCall(request, response, retry); ExecutorUtil.setServerThreadFlag(Boolean.TRUE); try { Action result = call.call(); switch (result) { case PASSTHROUGH: getSpan(request).log("SolrDispatchFilter PASSTHROUGH" ); chain.doFilter(request, response); break ; case RETRY: getSpan(request).log("SolrDispatchFilter RETRY" ); doFilter(request, response, chain, true ); break ; case FORWARD: getSpan(request).log("SolrDispatchFilter FORWARD" ); request.getRequestDispatcher(call.getPath()).forward(request, response); break ; case ADMIN: case PROCESS: case REMOTEQUERY: case ADMIN_OR_REMOTEQUERY: case RETURN: break ; } } finally { call.destroy(); ExecutorUtil.setServerThreadFlag(null ); } }
通过 call()
会得到一个 result。
1 Action result = call.call();
因此去 HttpSolrCall
跟踪。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java#L528
查看初始化函数。
https://github.com/apache/solr/blob/releases/solr/9.6.0/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java#L223
此处做了一个截断,如果路径中包含了 :
则取前面的内容为正常路径,过滤后面的路径。
因此,只要请求包 Header 中包含了 SolrAuth
或 SolrAuthV2
字段就可以使用 PKIAuthenticationPlugin
插件进行验证。而 PKIAuthenticationPlugin
插件中存在一个白名单,即路径末尾是 /admin/info/key
就不进行验证返回 True。为了正常访问未授权页面不被末尾的 /admin/info/key
干扰,就可以利用 Solr 的安全机制,截断 :
之后的字符。
使用以下请求就可以成功访问未授权页面。
1 2 3 GET /solr/admin/info/properties:/admin/info/key HTTP/1.1 Host : 127.0.0.1:8983SolrAuth : gumgum
0x04 漏洞补丁 https://github.com/apache/solr/commit/bd61680bfd351f608867739db75c3d70c1900e38#diff-1c90375c5d91492ff469c7fec2d07c32161bd0a3c3cd52c9d5f555b76a84f079
删除了 PKIAuthenticationPlugin
中的 /admin/info/key
白名单。删除了 :
截断的逻辑。
0x05 参考