返回介绍

1 Java 动态加载字节码

发布于 2024-09-16 15:35:01 字数 7455 浏览 0 评论 0 收藏 0

1.1 字节码

严格来说,Java 字节码其实仅仅指的是 Java 虚拟机执行使用的一类指令,通常被存储 在 .class 文件中。

众所周知,不同平台、不同 CPU 的计算机指令有差异,但因为 Java 是一门跨平台的编译型语言,所以这些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台的 JVM 虚拟机中。

1.2 利用 URLClassLoader 加载远程 class 文件

利用 Java 的 ClassLoader 来用来加载字节码文件最基础的方法。嘉文主要说明 URLClassLoader 。正常情况下,Java 会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类) 来寻找 .class 文件来加载,而这个基础路径有分为三种情况:

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

正常开发的时候通常遇到的是前两者,那什么时候才会出现使用 Loader 寻找类的情况呢?当然是非 file 协议的情况下,最常见的就是 http 协议。

使用 HTTP 协议来测试,从远程 HTTP 服务器上加载 .class 文件:

ClassLoader.java

package com.geekby.javavuln;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {

    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");
        Method f = c.getMethod("test");
        f.invoke(null, null);
    }
}

Hello.java

public class Hello {
    public static void test() {
        System.out.println("test");
    }
}

执行:

成功请求到 /Hello.class 文件,并执行了文件里的字节码,输出了「test」。

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

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

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

  • ClassLoader#loadClass
  • ClassLoader#findClass
  • ClassLoader#defineClass

其中:

  • loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类,在前面没有找到的情况下,执行 findClass
  • findClass 的作用是根据基础 URL 指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar 包或远程 http 服务器上读取字节码,然后交给 defineClass
  • defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类

因此,真正核心的部分其实是 defineClass ,其决定了如何将一段字节流转变成一个 Java 类,Java 默认的 ClassLoader#defineClass 是一个 native 方法,逻辑在 JVM 的 C 语言代码中。

通过简单的代码示例,演示 defineClass 加载字节码:

public class defineClassDemo {
    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);

        // 读取字节码并进行 base64 编码
        byte[] b = Files.readAllBytes(Paths.get("Hello.class"));
        String code = Base64.getEncoder().encodeToString(b);

        // base64 解码
        byte[] byteCode = Base64.getDecoder().decode(code);
        Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", byteCode, 0, byteCode.length);
        Method m = hello.getMethod("test", null);
        m.invoke(null, null);
    }
}

信息 在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的 static 块中,在 defineClass 时也无法被直接调用到。所以,如果要使用 defineClass 在目标机器上执行任意代码,需要想办法调用构造函数。

在实际场景中,因为 defineClass 方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是常用的一个攻击链 TemplatesImpl 的基石。

1.4 利用 TemplatesImpl 加载字节码

前面提到过,开发者不会直接使用到 defineClass 方法,但是,Java 底层还是有一些类用到了它,如: TemplatesImpl

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类 TransletClassLoader ,这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java 中默认情况下,如果一个方法没有显式声明作用域,其作用域为 default。因此,这里被重写的 defineClass 由其父类的 protected 类型变成了一个 default 类型的方法,可以被类外部调用。

TransletClassLoader#defineClass() 向前追溯一下调用链:

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

追到最前面两个方法 TemplatesImpl#getOutputProperties()TemplatesImpl#newTransformer() ,这两者的作用域是 public,可以被外部调用。尝试用 newTransformer() 构造一个简单的 POC:

public static void main(String[] args) throws Exception {
    String code = "...";
    byte[] byteCode = Base64.getDecoder().decode(code);
  
    TemplatesImpl obj = new TemplatesImpl();
  	// _bytecodes 是由字节码组成的数组
    Class c = TemplatesImpl.class;
    Field _bytecodes = c.getDeclaredField("_bytecodes");
    _bytecodes.setAccessible(true);
    _bytecodes.set(obj, new byte[][]{byteCode});
  
    // _name 可以是任意字符串,只要不为 null 即可
    Field _name = c.getDeclaredField("_name");
    _name.setAccessible(true);
    _name.set(obj, "HelloTemplatesImpl");
    
    // 固定写法
    Field _tfactory = c.getDeclaredField("_tfactory");
    _tfactory.setAccessible(true);
    _tfactory.set(obj, new TransformerFactoryImpl());
  
    obj.newTransformer();
}

但是, TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。需要构造一个特殊的类:

public class HelloTemppaltesImpl 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 HelloTemppaltesImpl() {
        super();
        System.out.println("Hello TemplatesImpl");
    }
}

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

1.5 利用 BCEL ClassLoader 加载字节码

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

通过 BCEL 提供的两个类 RepositoryUtility 来利用:用于将一个 Java Class 先转换成原生字节码,当然这里也可以直接使用 javac 命令来编译 java 文件生成字节码;Utility 用于将原生的字节码转换成 BCEL 格式的字节码:

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

public class BCELdemo {
    public static void main(String[] args) throws Exception {
        JavaClass cls = Repository.lookupClass(evil.Hello.class);
        String code = Utility.encode(cls.getBytes(), true);
        System.out.println(code);
    }
}

BCEL ClassLoader 用于加载这串特殊的 bytecode ,并可以执行其中的代码:

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文