迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

admin 2024年11月8日19:32:25评论56 views字数 20313阅读67分42秒阅读模式

Apache ZooKeeper Admin Server IPAuthenticationProvider 认证绕过漏洞 (CVE-2024-51504)

免责声明:本文内容为机器人搜集最新漏洞及POC分享,仅供技术学习参考,请勿用作违法用途,任何个人和组织利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责,与作者无关!!!

漏洞名称

3.9.0 <= Apache Zookeeper < 3.9.3存在Apache ZooKeeper Admin Server IPAuthenticationProvider 认证绕过漏洞 (CVE-2024-51504)

漏洞描述

Apache ZooKeeper是一个开源的分布式协调服务,它用于维护配置信息、命名、提供分布式同步以及提供组服务。AdminServer是其中一个特性,提供了HTTP接口来供用户通过API访问ZooKeeper的相关命令。2024年11月,官方披露其在使用 IPAuthenticationProvider 时使用IP白名单进行认证的情况下,攻击者可伪造X-Forwarded-For头绕过相关验证。

FOFA语句

body="zookeeper"

简单分析

环境搭建

下载代码

yum install -y wget
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.9.0/apache-zookeeper-3.9.0-bin.tar.gz
tar -xvf apache-zookeeper-3.9.0-bin.tar.gz

下面安装java环境直接使用默认配置

yum install -y java
cp apache-zookeeper-3.9.0-bin/conf/zoo_sample.cfg apache-zookeeper-3.9.0-bin/conf/zoo.cfg
cd apache-zookeeper-3.9.0-bin

直接启动

[root@localhost apache-zookeeper-3.9.0-bin]# bin/zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /root/apache-zookeeper-3.9.0-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
[root@localhost apache-zookeeper-3.9.0-bin]# 

未授权复现

默认是不需要授权的,先测试一下未授权 (quit退出)

./zkCli.sh -server 192.168.3.102:2181

连接上了会进入一个交互式控制台,如下

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

在交互式终端中可以控制节点和查看节点数据,例如

[zk: 192.168.3.102:2181(CONNECTED) 0] ls /
[zookeeper]
[zk: 192.168.3.102:2181(CONNECTED) 1]

一般进一步的利用就是看看如Dubbo等应用相关的配置(有什么骚操作请大佬们在评论区教教!)

还有一个就是默认启动的8080端口其实也是未授权的,应该可以在渗透测试的时候简单水个洞

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

好了,那未授权就到这里,下面设置个白名单看看

白名单配置

同样连接进去,看看默认的Acl配置

./zkCli.sh -server 192.168.3.102:2181
getAcl /

可以看见默认是没有限制的,任何人可连接

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

设置成127.0.0.1地址(使用setAcl / world:anyone:cdrwa可以设置回来)

setAcl / ip:127.0.0.1:cdrwa

此时再查看就发现没有权限了

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞
image-20241107155414040

quit退出换个ip连接命令,确认服务端的Acl配置无误

./zkCli.sh -server 127.0.0.1:2181

ok,现在去试试如何编写漏洞poc来达到连接127.0.0.1的效果

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

然后跟进cli调用的时候就会发现跑偏了(是的,看了小半天发现跑偏了),因为这里的是Admin Server IPAuthenticationProvider 认证绕过漏洞,针对的是8080端口的web服务的,而这个acl控制的是cli连接的,二者没有关系,下面的才是正题~

后面追代码发现是在配置文件来配置的,从官方手册可以得知地址该参数默认为0.0.0.0,可以在配置文件中修改

https://zookeeper.apache.org/doc/r3.5.3-beta/zookeeperAdmin.html#sc_adminserver_config

    public JettyAdminServer() throws AdminServerException, IOException, GeneralSecurityException {
        this(
            System.getProperty("zookeeper.admin.serverAddress", DEFAULT_ADDRESS),
            Integer.getInteger("zookeeper.admin.serverPort", DEFAULT_PORT),
            Integer.getInteger("zookeeper.admin.idleTimeout", DEFAULT_IDLE_TIMEOUT),
            System.getProperty("zookeeper.admin.commandURL", DEFAULT_COMMAND_URL),
            Integer.getInteger("zookeeper.admin.httpVersion", DEFAULT_HTTP_VERSION),
            Boolean.getBoolean("zookeeper.admin.portUnification"),
            Boolean.getBoolean("zookeeper.admin.forceHttps"),
            Boolean.getBoolean("zookeeper.admin.needClientAuth"));
    }

那么我们就在zoo.cfg这里加上一个配置admin.serverAddress=127.0.0.1

sh bin/zkServer.sh restart

这时候就可以发现监听变成了本地的

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

那么这个漏洞要怎么利用呢?如果配置了服务端的访问ip,那么别的ip访问就会被阻断,XFF伪造好像就是扯淡了

漏洞补丁

回到正轨,先去对比3.9.3和3.9.2的代码(用的beyond compare 4),可以发现org/apache/zookeeper/server/auth/IPAuthenticationProvider.java文件中多了一行

public static final String USE_X_FORWARDED_FOR_KEY = "zookeeper.IPAuthenticationProvider.usexforwardedfor";

在getClientIPAddress函数中加了判断

        if (!Boolean.getBoolean(USE_X_FORWARDED_FOR_KEY)) {
            return request.getRemoteAddr();
        }

在漏洞版本的getClientIPAddress函数中,clientIP优先取的是XFF头,所以才能伪造来源ip。

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

那么根据新增的代码逻辑,如果传输来的参数中使用了X-Forwarded-For字段,就返回request取到的RemoteAddr,这样限制就修好了cve描述中提到的问题。

使用idea往前追踪函数的调用,可以发现只有org/apache/zookeeper/server/admin/JettyAdminServer.java文件中存在调用

迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

对于这里的类的描述可以知道默认设置下,启动一个ZooKeeper服务器,并访问http://hostname:8080/commands,你将看到所有已注册命令的链接。

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package org.apache.zookeeper.server.admin;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.zookeeper.common.QuorumX509Util;
import org.apache.zookeeper.common.SecretUtils;
import org.apache.zookeeper.common.X509Util;
import org.apache.zookeeper.server.ZooKeeperServer;
import org.apache.zookeeper.server.auth.IPAuthenticationProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class encapsulates a Jetty server for running Commands.
 *
 * Given the default settings, start a ZooKeeper server and visit
 * http://hostname:8080/commands for links to all registered commands. Visiting
 * http://hostname:8080/commands/commandname will execute the associated
 * Command and return the result in the body of the response. Any keyword
 * arguments to the command are specified with URL parameters (e.g.,
 * http://localhost:8080/commands/set_trace_mask?traceMask=306).
 *
 * @see Commands
 * @see CommandOutputter
 */

public class JettyAdminServer implements AdminServer {

    static final Logger LOG = LoggerFactory.getLogger(JettyAdminServer.class);

    public static final int DEFAULT_PORT = 8080;
    public static final int DEFAULT_IDLE_TIMEOUT = 30000;
    public static final String DEFAULT_COMMAND_URL = "/commands";
    private static final String DEFAULT_ADDRESS = "0.0.0.0";
    public static final int DEFAULT_STS_MAX_AGE = 1 * 24 * 60 * 60;  // seconds in a day
    public static final int DEFAULT_HTTP_VERSION = 11;  // based on HttpVersion.java in jetty

    private final Server server;
    private final String address;
    private final int port;
    private final int idleTimeout;
    private final String commandUrl;
    private ZooKeeperServer zkServer;

    public JettyAdminServer() throws AdminServerException, IOException, GeneralSecurityException {
        this(
            System.getProperty("zookeeper.admin.serverAddress", DEFAULT_ADDRESS),
            Integer.getInteger("zookeeper.admin.serverPort", DEFAULT_PORT),
            Integer.getInteger("zookeeper.admin.idleTimeout", DEFAULT_IDLE_TIMEOUT),
            System.getProperty("zookeeper.admin.commandURL", DEFAULT_COMMAND_URL),
            Integer.getInteger("zookeeper.admin.httpVersion", DEFAULT_HTTP_VERSION),
            Boolean.getBoolean("zookeeper.admin.portUnification"),
            Boolean.getBoolean("zookeeper.admin.forceHttps"),
            Boolean.getBoolean("zookeeper.admin.needClientAuth"));
    }

    public JettyAdminServer(
        String address,
        int port,
        int timeout,
        String commandUrl,
        int httpVersion,
        boolean portUnification,
        boolean forceHttps,
        boolean needClientAuth)
 throws IOException, GeneralSecurityException {

        this.port = port;
        this.idleTimeout = timeout;
        this.commandUrl = commandUrl;
        this.address = address;

        server = new Server();
        ServerConnector connector = null;

        if (!portUnification && !forceHttps) {
            connector = new ServerConnector(server);
        } else {
            SecureRequestCustomizer customizer = new SecureRequestCustomizer();
            customizer.setStsMaxAge(DEFAULT_STS_MAX_AGE);
            customizer.setStsIncludeSubDomains(true);

            HttpConfiguration config = new HttpConfiguration();
            config.setSecureScheme("https");
            config.addCustomizer(customizer);

            try (QuorumX509Util x509Util = new QuorumX509Util()) {
                String privateKeyType = System.getProperty(x509Util.getSslKeystoreTypeProperty(), "");
                String privateKeyPath = System.getProperty(x509Util.getSslKeystoreLocationProperty(), "");
                String privateKeyPassword = getPasswordFromSystemPropertyOrFile(
                        x509Util.getSslKeystorePasswdProperty(),
                        x509Util.getSslKeystorePasswdPathProperty());

                String certAuthType = System.getProperty(x509Util.getSslTruststoreTypeProperty(), "");
                String certAuthPath = System.getProperty(x509Util.getSslTruststoreLocationProperty(), "");
                String certAuthPassword = getPasswordFromSystemPropertyOrFile(
                        x509Util.getSslTruststorePasswdProperty(),
                        x509Util.getSslTruststorePasswdPathProperty());
                KeyStore keyStore = null, trustStore = null;

                try {
                    keyStore = X509Util.loadKeyStore(privateKeyPath, privateKeyPassword, privateKeyType);
                    trustStore = X509Util.loadTrustStore(certAuthPath, certAuthPassword, certAuthType);
                    LOG.info("Successfully loaded private key from {}", privateKeyPath);
                    LOG.info("Successfully loaded certificate authority from {}", certAuthPath);
                } catch (Exception e) {
                    LOG.error("Failed to load authentication certificates for admin server.", e);
                    throw e;
                }

                SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
                sslContextFactory.setKeyStore(keyStore);
                sslContextFactory.setKeyStorePassword(privateKeyPassword);
                sslContextFactory.setTrustStore(trustStore);
                sslContextFactory.setTrustStorePassword(certAuthPassword);
                sslContextFactory.setNeedClientAuth(needClientAuth);

                if (forceHttps) {
                    connector = new ServerConnector(server,
                            new SslConnectionFactory(sslContextFactory, HttpVersion.fromVersion(httpVersion).asString()),
                            new HttpConnectionFactory(config));
                } else {
                    connector = new ServerConnector(
                            server,
                            new UnifiedConnectionFactory(sslContextFactory, HttpVersion.fromVersion(httpVersion).asString()),
                            new HttpConnectionFactory(config));
                }
            }
        }

        connector.setHost(address);
        connector.setPort(port);
        connector.setIdleTimeout(idleTimeout);

        server.addConnector(connector);

        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
        context.setContextPath("/*");
        constrainTraceMethod(context);
        server.setHandler(context);

        context.addServlet(new ServletHolder(new CommandServlet()), commandUrl + "/*");
    }

    /**
     * Start the embedded Jetty server.
     */

    @Override
    public void start() throws AdminServerException {
        try {
            server.start();
        } catch (Exception e) {
            // Server.start() only throws Exception, so let's at least wrap it
            // in an identifiable subclass
            String message = String.format(
                "Problem starting AdminServer on address %s, port %d and command URL %s",
                address,
                port,
                commandUrl);
            throw new AdminServerException(message, e);
        }
        LOG.info("Started AdminServer on address {}, port {} and command URL {}", address, port, commandUrl);
    }

    /**
     * Stop the embedded Jetty server.
     *
     * This is not very important except for tests where multiple
     * JettyAdminServers are started and may try to bind to the same ports if
     * previous servers aren't shut down.
     */

    @Override
    public void shutdown() throws AdminServerException {
        try {
            server.stop();
        } catch (Exception e) {
            String message = String.format(
                "Problem stopping AdminServer on address %s, port %d and command URL %s",
                address,
                port,
                commandUrl);
            throw new AdminServerException(message, e);
        }
    }

    /**
     * Set the ZooKeeperServer that will be used to run Commands.
     *
     * It is not necessary to set the ZK server before calling
     * AdminServer.start(), and the ZK server can be set to null when, e.g.,
     * that server is being shut down. If the ZK server is not set or set to
     * null, the AdminServer will still be able to issue Commands, but they will
     * return an error until a ZK server is set.
     */

    @Override
    public void setZooKeeperServer(ZooKeeperServer zkServer) {
        this.zkServer = zkServer;
    }

    private class CommandServlet extends HttpServlet {

        private static final long serialVersionUID = 1L;

        @Override
        protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response)
 throws ServletException, IOException {
            // Capture the command name from the URL
            String cmd = request.getPathInfo();
            if (cmd == null || cmd.equals("/")) {
                // No command specified, print links to all commands instead
                for (String link : commandLinks()) {
                    response.getWriter().println(link);
                    response.getWriter().println("<br/>");
                }
                return;
            }
            // Strip leading "/"
            cmd = cmd.substring(1);

            // Extract keyword arguments to command from request parameters
            @SuppressWarnings("unchecked") Map<String, String[]> parameterMap = request.getParameterMap();
            Map<String, String> kwargs = new HashMap<>();
            for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
                kwargs.put(entry.getKey(), entry.getValue()[0]);
            }
            final String authInfo = request.getHeader(HttpHeader.AUTHORIZATION.asString());

            // Run the command
            final CommandResponse cmdResponse = Commands.runGetCommand(cmd, zkServer, kwargs, authInfo, request);
            response.setStatus(cmdResponse.getStatusCode());

            final Map<String, String> headers = cmdResponse.getHeaders();
            for (final Map.Entry<String, String> header : headers.entrySet()) {
                response.addHeader(header.getKey(), header.getValue());
            }
            final String clientIP = IPAuthenticationProvider.getClientIPAddress(request);
            if (cmdResponse.getInputStream() == null) {
                // Format and print the output of the command
                CommandOutputter outputter = new JsonOutputter(clientIP);
                response.setContentType(outputter.getContentType());
                outputter.output(cmdResponse, response.getWriter());
            } else {
                // Stream out the output of the command
                CommandOutputter outputter = new StreamOutputter(clientIP);
                response.setContentType(outputter.getContentType());
                outputter.output(cmdResponse, response.getOutputStream());
            }
        }

        /**
         * Serves HTTP POST requests. It reads request payload as raw data.
         * It's up to each command to process the payload accordingly.
         * For example, RestoreCommand uses the payload InputStream directly
         * to read snapshot data.
         */

        @Override
        protected void doPost(final HttpServletRequest request,
                              final HttpServletResponse response)
 throws ServletException, IOException {
            final String cmdName = extractCommandNameFromURL(request, response);
            if (cmdName != null) {
                final String authInfo = request.getHeader(HttpHeader.AUTHORIZATION.asString());
                final CommandResponse cmdResponse = Commands.runPostCommand(cmdName, zkServer, request.getInputStream(), authInfo, request);
                final String clientIP = IPAuthenticationProvider.getClientIPAddress(request);
                sendJSONResponse(response, cmdResponse, clientIP);
            }
        }

        /**
         * Extracts the command name from URL if it exists otherwise null
         */

        private String extractCommandNameFromURL(final HttpServletRequest request,
                                                 final HttpServletResponse response)
 throws IOException {
            String cmd = request.getPathInfo();
            if (cmd == null || cmd.equals("/")) {
                printCommandLinks(response);
                return null;
            }
            // Strip leading "/"
            return cmd.substring(1);
        }

        /**
         * Prints the list of URLs to each registered command as response.
         */

        private void printCommandLinks(final HttpServletResponse response) throws IOException {
            for (final String link : commandLinks()) {
                response.getWriter().println(link);
                response.getWriter().println("<br/>");
            }
        }

        /**
         * Send JSON string as the response.
         */

        private void sendJSONResponse(final HttpServletResponse response,
                                      final CommandResponse cmdResponse,
                                      final String clientIP)
 throws IOException {
            final CommandOutputter outputter = new JsonOutputter(clientIP);

            response.setStatus(cmdResponse.getStatusCode());
            response.setContentType(outputter.getContentType());
            outputter.output(cmdResponse, response.getWriter());
        }
    }

    /**
     * Returns a list of URLs to each registered Command.
     */

    private List<String> commandLinks() {
        return Commands.getPrimaryNames().stream().sorted().map(command -> String.format("<a href="%s">%s</a>", commandUrl + "/" + command , command)).collect(Collectors.toList());
    }

    /**
     * Add constraint to a given context to disallow TRACE method
     * @param ctxHandler the context to modify
     */

    private void constrainTraceMethod(ServletContextHandler ctxHandler) {
        Constraint c = new Constraint();
        c.setAuthenticate(true);

        ConstraintMapping cmt = new ConstraintMapping();
        cmt.setConstraint(c);
        cmt.setMethod("TRACE");
        cmt.setPathSpec("/*");

        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
        securityHandler.setConstraintMappings(new ConstraintMapping[] {cmt});

        ctxHandler.setSecurityHandler(securityHandler);
    }

    /**
     * Returns the password specified by the given property or stored in the file specified by the
     * given path property. If both are specified, the password stored in the file will be returned.
     * @param propertyName the name of the property
     * @param pathPropertyName the name of the path property
     * @return password value
     */

    private String getPasswordFromSystemPropertyOrFile(final String propertyName,
                                                       final String pathPropertyName)
 {
        String value = System.getProperty(propertyName, "");
        final String pathValue = System.getProperty(pathPropertyName, "");
        if (!pathValue.isEmpty()) {
            value = String.valueOf(SecretUtils.readSecret(pathValue));
        }
        return value;
    }
}

有点迷惑的一个cve,不太清楚具体的利用方式,也可能是有别的配置方式没找到,后续看看有没有哪个好同志披露吧~

原文始发于微信公众号(安全光圈):迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月8日19:32:25
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   迷惑的CVE-2024-51504-所谓ZooKeeper Admin Server认证绕过漏洞https://cn-sec.com/archives/3374089.html

发表评论

匿名网友 填写信息