CC链学习上
CC链学习上:
CC1:
这里主要还是学习的是P神的知识星球里面的Java安全漫谈。
Commons Collections的利用链也就是CC链,是Apache中的一个库,包括了Weblogic,JBoss,WebSphere,Jenkins等知名大型Java应用都使用了这个库。
这里的CC1指的是lazymap的那条链子,但是网上也有很多关于transformedmap的。
对于CC1的测试环境需要在Java 8u71之前,在此改动后,AnnotationInvocationHandler#readObject不再直接使用 反序列化得到的Map对象,而是直接新建了一个LinkedHashMap对象,并且将原来的键值添加进去。所以,后续对于Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再会执行set或是put操作。
测试环境:
- JDK 1.7
- Commons Collections 3.1
这一处漏洞最后会导致RCE漏洞的产生。
漏洞原理:
Apache Commons Collections
中提供了一个Transformer
的类,这个是个接口,功能就是将一个对象转换为另外一个对象。
这里首先参考一下P神的CC1利用链:
package org.vulhub.Ser; |
首先可以看到,这里实例化了一个Transformer
类的数组,数组中包含了两个对象。
虽然这里的Transformer
类是一个接口,本来是因为不能直接被实例化的,但是像这种情况出现,在后面应该不止有接口,应该还会存在一些匿名内部类,在匿名内部类的创建过程中实际上是实例化了一个实现接口。
因为在Java多态 中,父类可以引用指向子类对象,同理,接口也可以指向其实例化对象,在实例化对象中必然会实现接口中定义的方法和属性,同时,对象的类型必须是new出来的类型。
也就是说,重点是因为在这个数组接口中有匿名内部类的实现,而这也算是对接口是实现。
整个过程中涉及到以下几个接口和类:
TransformedMap
TransformedMap
用于对Java标准数据结构Map
,做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。我们通过下面这行代码对innerMap
进行修饰,传出的outerMap
即是修饰后的Map。
Map outerMap = TransformeMap.decorate(innerMap,keyTransformer,valueTransformer); |
其中,keyTransformer
是处理新元素key的回调,valueTransFormer
是处理新元素的value
的回调。
这里说的回调不是传统意义上的回调函数,而是一个实现了Transformer接口的类。
Transformer:
这是一个接口
这个接口中有一个待实现的方法,
public interface Transformer{ |
TransformerMap在转换Map的新元素的时候,就回到用transform方法,这个过程就类似在调用一个“回调函数“,但是这个回调的参数是原始对象。
两个实例化:
ConstantTransformer
这是实现了Transformer接口的一个类,过程就是在构造函函数的时候传入一个对象,斌且在Transform这个方法的时候,再将这个对象返回。
public ConstantTransformer(Object constantToReturn){
super();
iConstant = constantToReturn;
}
public Object transform(Object input){
return iConstant;
}所以他的作用其实就是包装任意一个对象,然后在执行回到的时候返回这个对象,进而方便后续操作。
InvokerTransformer
InvokerTransformer是实现了Transformer接口的一个类,这个类可以用来执行任意方法,也就是反序列化能够进行RCE的关键入手点。在实例化这个InvokerTransformer的时候,需要传入三个参数,第一个参数是待执行的方法名,第二个参数是这个函数的参数列表的参数类型,第三个参数是传递给这个函数的参数列表。
public InvokerTransformer(String methodName,Class[] paramTypes,Object[] args){
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}后面的回调transform方法,就是执行了input对象的MethodName方法:
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);
}
}可以看到这里,是通过反射的方式,来获取了对应
input
类中iMethodName
的函数和传入的参数类型。然后直接调用然后调用
iMethodName
对应的方法,以及iArgs对应的参数。
ChainedTransformer
ChainedTransformer也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串在一起,简单来说就是将前一个回调返回的结果作为后一个回调的参数传入。
public ChainedTransformer(Transformer[] transformers) { |
这里简单来看,就是将传入的Transformer类的数组进行了一个遍历,然后依次调用了对应的Transformer数组中的类的transform
方法。
这部分的遍历调用,就是能够将多个transform函数连环调用的关键。
回头看一下源码:
这部分其实就是创建了一个Transformer数组,内部包含两个实例,然后通过调用ChainedTransformer
来连续调用。
这里是首先调用了Runtime
下的getRuntime()
方法,这个方法会返回一个新的Runtime
类,然后常规操作就是调用这个新Runtime类中的exec
函数来进行命令执行。
在这里,是将这个新返回的Runtime类作为参数传入,然后进行一重包装,等待调用时返回。
然后是InvokerTransformer
这一部分,这部分主要是用来将参数和调用的函数名字传入。
可以看到这里是调用了exec()
函数,也就是调用了Runtime.getRuntime()
返回的类currentRuntime()
中的exec()
函数,来执行命令执行,然后这里同时标注了传入的Class对象数组,其中有一个String类的Class对象。也就是告诉我们参数类型是String。并给出了调用的计算器的绝对路径作为参数,以方便执行。
然后创建了子类ChainedTransformer()
的一个对象,用来连锁调用参数和传来的内容。
因为这里只是一系列回调,所以需要用来包装innerMap,使用前面的TransformedMap.decorate
Map innerMap = new HashMap(); |
这里就是对内部映射(innerMap)进行了一个包装,使用了transformerChain
的转换器。
最后想要触发回调,就是想Map中放入一个新的元素。
例如:
outerMap.put("test","xxxx"); |
调用流程:
整个调用流程就是:
1、创建一个Transfomer类的数组,transformers,在其内部添加我们想要执行的函数,参数,内容。
2、然后调用ChainedTransfomer类,将上述数组作为参数传入,创建一个对象。
3、新建一个HashMap类的对象,创建了一个数据类型为Map的对象。然后通过TransformedMap.decorate(),来调用数据类型为Map的对象,也就是innerMap,进而来调用ChainedTransformer类的对象——transformerChain类中的transformer()方法。
4、这个时候,就会依次执行ConstantTransformer类和InvokerTransfomer类中的transformer()方法,也就完成了参数的返回和命令的执行。
5、上述就是调用流程,为了触发这个调用流程,就需要想outerMap中传入一个键值对,这个时候,就会自动开始调用transformerChain中的方法。
只能传入值,删减值是没有效果的
也就是说,最后的命令执行点是在InvokerTransformer类中的transforme()函数中。
如何生成一个可以利用的序列化对象:
为了调用这个链子,我们需要进行一个写入操作来进行触发。但是有一个问题,是因为这里我们是在自己的demo里,可以通过手动触发。
但是当我们把outerMap这个对象序列化之后,就不能这么进行了,所以我们需要找到一个readObject逻辑里,有一个写入的操作。
这个类就是sun.reflect.AnnotationInvocationHandler
,我们查看一下readObject
代码:
private void readObject(java.io.ObjectInputStream s) |
这个是8u17版本的调用链,现在新版本中是不能使用的。
可以看到,在这个私有类中,定义了一个Map类型的数据,memberTypes
,这个变量是存储了所有成员变量的类型。
根据我们之前的使用方法,应该就能知道这里是主要的调用点。
然后下面进入了一个for循环,在这个循环里,注意首先设置了一个Map.Entry
类型的变量,memberValue
。这个类型是一个接口,用来存储Map
中的键值对。
在循环中会首先获取HashMap中的键,然后使用get(name)
方法去获取值的信息,随后进行一个if判断,判断这个值的类型是否为Null。
如果不是Null,就使用getValue()函数获取这个键对应的值,然后再进行一个if判断,在这个if里,会判断Type
是不是memberType
这个类的实例,或是ExceptionProxy
这个类的实例。
如果不是上面任何一种的实例,就进入判断语句。
这部分就是利用的点了,可以看到这里调用了setValue()
函数,这个函数比较有意思的一点在于,这个函数实际上是改变memberValue
这个变量中的键值对,而不是直接改变Map
对象中的键值对。
简单来说,就像是一本书,你只改了书的目录,但是书的内容是不变的。
这里因为memberValues就是反序列化之后的Map,同时也经过了TransformedMap修饰的对象,所以调用setvalue的时候,就能触发构造的恶意序列化。
这里的核心逻辑就是:
- Map.Entry<String, Object> memberValue : memberValues.entrySet()
- memberValue.setValue()
触发条件:
- memberValues是我们准备好的经过TransformedMap类修饰的Map
- 其中的值不能是Null,同时也不是Exception的实例,或者不是memberType中记录的类型。
也就是说,实际上是可以直接用我们修饰好的类传入进去就可以了。
为了调用AnnotationInvocationHandler
中的readObject()
,所以我们需要创建一个对应的实例,用于序列化,并将Map设置进来。
为了创建这个对象,需要使用反射的形式来进行创建:
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
这里就是通过反射的方式,创建了一个反射实例obj
然后将这个实例进行序列化。
这时因为sun.reflect.annotation.AnnotationInvocationHandler
是JDK中的一个内部类,不能使用new来直接进行实例化,所以也不能直接进行序列化。
在创建实例的时候,将我们之前修饰过的Map作为参数outerMap
传入。
修改原本的POC:
随后,可以尝试对这个反射后创建的实例进行序列化。
但是会出现报错:
java.io.NotSerializableException: java.lang.Runtime
这是因为在之前写的调用方式中,Runtime这个类是不支持Serializable
这个接口的,当Java要进行序列化的时候,必须要保证序列化的对象和它内部的所有属性对象都要实现Serializable
接口。
因此,需要对Runtime进行反射,然后才能进行序列化。
所以需要添加反射部分:
Method m = Runtime.class.getMethod("getRuntime"); |
第一行就是获取Runtime的class对象,然后获取getRuntime()
的静态方法。
然后调用getRuntime()
方法,返回一个Object对象,强制转换为Runtime类型。然后调用其中的exec函数。
只要通过上述步骤就可以完成调用。
因为InvokeTransformer
类中其实就是通过反射的方式来进行调用的,所以,可以通过反射的方式,来对Runtime中的exec进行调用。
调用的部分是这里:
Class cls = input.getClass(); |
调用顺序:
- Runtime.getClass(),然后获取其中的getMethod方法,然后invoke()调用getMethod()方法,这里就等于是执行了getMethod(“getRuntime”)。
- 上一次的getMethod()函数会返回一个Method对象,这个Method中包含了Runtime中的所有方法,也就相当于那个m,然后获取其自身的invoke()方法,然后调用,因为不需要参数,就直接使用null。
- 然后再重复上面的步骤,调用其中的exec函数,来进行命令执行。
这里最主要的可以实行的原因,是因为Runtime.class是一个java.lang.Class对象,实现了Serializable接口,所以可以直接进行序列化。
无法触发的原因:
这是因为在AnnotationInvocationHandler:readObject()
中存在一个逻辑:
这个if语句会对var7的值进行判断,只有在值不为null的时候,才会进入if内部,执行setValue
,否则会不会进入,也就不会触发漏洞。
P神这里直接告诉我们,通过两个条件可以让var7不为Null。
sun.reflect.annotation.AnnotationInvocationHandler
构造函数的第一个参数必须是Annotation的子类,并且其中必须含有至少一个方法,假设方法名是X- 被
TransformedMap.decrate
修饰的Map中必须有一个键名为X的元素。
这里可以看一下为什么需要做上述两个操作:
为什么要设置两个条件:
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { |
首先,为什么我们一定要传入一个Annotation类:
是因为这里,调用的是AnnotationType的getInstance()
方法,而this.type就是我们传入的Annotation类。
首先跟入getInstance()
定义了一个var1,调用函数,用于获取访问权限。
定义了一个AnnotationType类型的var2,这里调用了getAnnotationType()函数,获取了我们传入的Annotation类的注解。
然后只要存在注解,就能够进入if。
此时,将var2重新赋值为一个AnnotationType类型,这里跟进构造函数:
因为此时我们传入的var0是Annotation类,所以直接执行else。
在else中,返回了var1,也就是我们定义的Annotation
中的所有方法。
然后创建了三个HashMap对象,设置了初始容量和负载因子。
然后定义了一个var3的数组,获取var2中的所有Methods,var4是方法的数量。
进入了一个for循环,依次遍历var2中的所有方法。
如果当前遍历到的方法有参数,就报错。
随后,定义了一个var7,用于存储方法名。
然后var8存储返回类型。
然后将var7作为键,调用了一个函数处理var8,函数定义在下面:
函数用于将var8存放的类型,转换为类型对应的类。这里作为值传入。
注意:
这个地方memberTypes
这个变量是非常重要的,在我们的AnnotationInvocationHandler.class
中可以看到,var3其实就是var2调用了一个memberTypes()
方法的返回值。
而这里的函数的返回值其实也就是我们创建的memberTypes这个变量。
因此,需要重视这个变量。
var7
是我们传入AnnotationInvocationHandler
类构造方法的Annotation
类中所有方法的方法名(因为是遍历)
也就是说memberTypes
这个HashMap类中的键值对中,键是所有方法的方法名,而值是方法对应的返回值的类型。
因此:
var3
是上述的memberTypes
。
在这部分,var4是一个用于遍历集合的类,上述将memberValues的内部键值对作为一个视图返回,然后存储在var4中。
只要var4中还有可以遍历的元素,就执行while循环。
var5
是一个存放键值对的类,用于表示var4
中的元素
var6
存放这个var5
这个键值对中的键,而var7
则是存放var3
中,键对应的值。
而memberValue
是我们传入的,经过Transformer
修饰的Map类。
也就是说,在我们创建的Map中,存在的键在var3中有对应的值,也就是说,在Map中的键,必须是Annotation这个类中的某个方法名。
到这里,我们就已经知道为什么需要满足那两个条件了。
使用Retention:
这里可以看到,Retention.class
是Annotation的子类,这里是满足我们的使用要求的。
其中,存在一个方法,叫做value()
,按照我们的要求,我们需要在被Transformer
修饰的Map
中传入一个键,名字叫value。
所以可以使用:
innermap.put("value", "xxx"); |
就可以进入if,完成setValue,进而触发我们的Payload,完成命令执行。
当然,这里只要是Annotation
的子类就可以
这两个其实也行。
也就是说,我们对innerMap()的修改,其实是可以有多种的。
主要是根据使用的子类中的方法名来决定的。
原版POC:
package org.example; |
为什么Java高版本不能使用:
当我们创建了POC之后,会发现,只能在8u17版本中进行利用。
这是因为在高版本中,对AnnotationInvocationHandler.class
这个类的readObject()
方法进行了修改:
.io.Serial |
这是因为在类代码的656行,新建了一个类对象,LinkedHashMap
的对象mv,然后在673行可以看到将我们传入的Map的键和值传入了mv中。
因为我们是不能控制mv这个对象的,所以不能进行RCE操作。
这里就要看到在ysoserial中的代码,里面使用的不是TransformedMap,而是改用了LazyMap
这个类。
LazyMap:
什么是LazyMap
?LazyMap
和TransformedMap
类似,都是来自于Common-Collections库,并继承了AbstractMapDecorator
。
这两个利用链的主要区别是,TransformedMap是在写入元素的时候执行的transform,而LazyMap是在它的get方法中执行的factory.transform
。
这个函数的作用,就像是LazyMap的描述一样,是懒加载,也就是在get找不到值的时候,它会调用factory.transform方法去获取一个值。
public Object get(Object key) { |
上述是源码,这里可以看到,在这个函数中,当我们尝试从一个map类型的参数中获取它的键的时候,如果找不到键,就调用factory.transform(key)
但是相比于TransformedMap
的利用方法,LazyMap的利用更加复杂。
sun.reflect.annotation.AnnotationInvocationHandler
的readObject()没有直接调用到Map中的get方法
也就是说,如果我们想要调用其中的get()方法,就必须要使用别的方式来进行。
ysoserial中可以给出一个新的路线,在AnnotationInvocationHandler
类的invoke方法中有调用到get()
public Object invoke(Object var1, Method var2, Object[] var3) { |
这里还是jdk1.8里面的AnnotationInvocationHandler
类。
为了调用这个地方的invoke()方法,这里需要使用Java中的对象代理,也就是类似于实现PHP中的魔术方法来进行对对象的调用。
这里回头看一下AnnotationInvocationHandler
类,可以发现这里其实就是一个继承了InvocationHandler接口的类。
也就是说,这个类实际上我们是可以进行代理的。
只要我们将这个类进行代理,那么在进行readObject的时候,不管是调用这个类的什么方法,都会通过代理类,然后受到代理类Invoke()
方法的调控,进而触发LazyMap#get
。
因为我们需要调用AnnotationInvocationHandler
中的Invoke()方法从而进入LazyMap
。
调用思路:
因此我们不难得出此处POC需要的几个要素:
- 不使用TransformedMap进行修饰,而是换成LazyMap进行修饰
- 为了调用
AnnotationInvocationHandler
中的Invoke()函数,所以需要对这个类进行代理,将这个类作为handler参数给代理类使用。当代理类进行任何方法的调用的时候,都会直接调用AnnotationInvocationHandler
中的Invoke()
函数,这是由Java中代理的性质决定的。- 对于上述的代理类,因为我们反序列化的入口还是AnnotationInvocationHandler,所以需要将上述的代理类包装一次,变为AnnotationInvocationHandler类,来进行序列化。
POC:
package org.example; |
这里的POC主要还是参考的P神的Java安全漫谈。
当运行了这个POC之后就会直接弹出计算器。
这里我们再仔细的看一下具体的函数调用特点:
在这个LazyMap中的get方法里,调用的factory.transform(key)
实际上就是调用的Tranformer
这个类中的transform()方法,同时可以知道factory是可控的。
就像是TransformedMap链中一样,当我们调用decorate()函数的时候,就会创建一个新的Map类,因此我们会直接将其中的factory类设置为ChainedTransformer类,随后就能直接调用其中的transform()方法,然后完成对链的调用。
这条链子的区别和之前的TransformedMap的区别就只有前面这部分,不包含后面部分。
关于ysoserial中的ConstantTransformer(1):
根据P神的Java安全漫谈中的说法,应该是为了隐藏启动进程的日志特征。