Java动态加载字节码

这次是Java字节码学习,依旧是跟着P牛

Java字节码是什么

Java字节码是在Java虚拟机执行时用的一类指令,这是一种中间代码,是Java源代码经过编译后生成的一种二进制文件,使得Java具有跨平台的特性

Java_ByteCode_1.png

在P牛写的这篇动态加载字节码中,所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在探讨范围内。

我前面有一篇讲过这个ClassLoader,在很多场景中,我们可以有控制一些能够加载字节码函数的能力,那么可以通过加载远程的恶意类,来完成RCE。

如何加载类

URLClassLoader

Java的ClassLoader是用来加载字节码文件最基础的方法,它就是一个加载器,来告诉Java虚拟机如何加载这个类。Java默认的ClassLoader就是根据类名来加载类,这个类名是类完整路径,比如java.lang.Runtime

就先来说一下这个URLClassLoader

URLClassLoader实际上就是我们平时默认使用的AppClassLoader的父类,所以,解释URLClassLoader的工作过程实际上就是在解释默认的Java类加载器的工作流程。

那么接下来就先来看一下这个URLClassLoader是如何加载类的

1
参考文章:https://juejin.cn/post/7057728820270333966

核心逻辑在函数URLClassPath#getLoader(URL url)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Loader getLoader(final URL var1) throws IOException {
try {
return (Loader)AccessController.doPrivileged(new PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
String var1x = var1.getFile();
if (var1x != null && var1x.endsWith("/")) {
return (Loader)("file".equals(var1.getProtocol()) ? new FileLoader(var1) : new Loader(var1));
} else {
return new JarLoader(var1, URLClassPath.this.jarHandler, URLClassPath.this.lmap, URLClassPath.this.acc);
}
}
}, this.acc);
} catch (PrivilegedActionException var3) {
throw (IOException)var3.getException();
}
}

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

  • URL以/结尾,那么就是一个JAR,用JarLoader来寻找类
  • URL不以/结尾,且是file协议,就用FileLoader来寻找类
  • URL不以/结尾,且不是file协议,就创建一个Loader来寻找类

这里尝试用http协议从远程加载类,那么就是第三种情况

先写一个被调用的远程类

1
2
3
4
5
public class Hello {  
public Hello(){
System.out.println("Hello, world!");
}
}

再写一个测试类

1
2
3
4
5
6
7
8
9
10
11
import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
URL[] urls = {new URL("http://127.0.0.1:8888/")};
URLClassLoader ucl = URLClassLoader.newInstance(urls);
Class c = ucl.loadClass("Hello");
c.newInstance();
}
}

Java_ByteCode_2.png

这里也就成功请求到并执行了/Hello.class文件,输出了”Hello”

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

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

不管是加载远程的class文件,还是本地的classjar文件,Java经历的都是下面的过程:

1
ClassLoadedr#loadClass --> ClassLoader#findClass --> ClassLoader#defineClass

其中:

  • loadClass有两个作用:一是运行时动态加载指定的类,在加载过程中会读取字节码文件,验证正确性和解析类的依赖关系等等;而是检测类是否重复加载,包括对类加载器的继承关系,已加载类的缓存进行检测。使用ClassLoaderloadClass方法加载类时,如果类缓存、父加载器等位置找不到类,就会传入url,调用findClass去加载类。
  • findClass:根据传入的url,以对应的方式从本地class文件、jar包、远程http服务器等地方加载类的字节码,并且将字节码传给defineClass
  • defineClass:根据传入的已经加载的类的字节码,将其转换成对应的Class对象也就是一个真正的java类,并且返回。

真正的核心部分就是defineClass,他决定了如何将一段字节流转变成一个java类,那么接下来我们尝试用ClassLoader#defineClass加载字节码,其中由于Class#defineClass是一个保护属性,所以需要通过反射来调用

defineClass的左右就像它的名字,定义类,它只做把一个类定义出来的工作,它并不会初始化类对象。类对象还是需要通过显式调用构造函数,初始化代码才会被执行。所以如果我们想要的是任意代码执行,还需要想办法来调用构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.example;

import java.util.Base64;

import java.lang.reflect.Method;

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);

// source: bytecodes/Hello.java
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAHAoABgAOCQAPABAIABEKABIAEwcAFAcAFQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAHAAgHABYMABcAGAEAC0hlbGxvIFdvcmxkBwAZDAAaABsBAAVIZWxsbwEAEGphdmEvbGFuZy9PYmplY3QBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAIAAQAHAAgAAQAJAAAAHQABAAEAAAAFKrcAAbEAAAABAAoAAAAGAAEAAAACAAgACwAIAAEACQAAACUAAgAAAAAACbIAAhIDtgAEsQAAAAEACgAAAAoAAgAAAAQACAAFAAEADAAAAAIADQ==");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
}

上面的这段字节码生成自下面的java类

1
2
3
4
5
6
7
8
public class Hello {  
public Hello() {
}

static {
System.out.println("Hello World");
}
}

用TemplatesImpl加载类

上面已经讲了一点defineClass,这个方法是我们利用TemplatesImpl链的基石

大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它,就比如这个TemplatesImpl

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);
}
}

这里TransletClassLoader重写了defineClass,并且没有显式地声明其定义域,相当于用default声明。重写后本来的protected类型也变成了default,使它可以被外部调用。

那么什么时候这个TransletClassLoader#defineClass()会被调用呢?我们从TransletClassLoader#defineClass()向前追溯一下调用链(最好自己看一下源码):

1
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstamce() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()

这里最前面的两个方法TemplatesImpl#getOutputProperties()TemplatesImpl#newTransformer(),这两者的作用域是public,可以被外部调用。我们尝试用newTransformer(),这两者的作用域是public,可以被外部调用。那么我们尝试用newTransformer()来构造一个简单的poc

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
package org.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 TestTemplatesImpl {
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);
}
public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

obj.newTransformer();
}
}

在上面的这段poc中setFielfValue用来设置私有属性,构造了_bytecodes,_name,_tfactory三个属性来从newTransformer走到最终的defineClass_bytecodes是有字节码组成的数组;_name可以是任意字符串,主要不为null即可;_tfactory需要一个TransformerFactoryImpl()对象,因为TemplatesImpl#defineTransletClasses()方法里有调用到_tfactory.getExternalExtensionMap(),如果是null就会出错。

注意:TemplatesImpl中对加载的字节码有着一定要求:这个字节码对应的类必须是com.sun.org.apache.axlan.internal.xsltc.runtime.AbstractTranslet的子类

所以我们需要构造一个特殊的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 HelloTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

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

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

它继承了AbstractTranslet类,并在构造函数里插入Hello的输出。将其编译成字节码,即可被TemplatesImpl执行了:

java_Bytecode_3.png

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

利用BCEL ClassLoader加载字节码

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

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

1
2
3
4
5
6
7
8
9
10
11
12
package com.govuln;
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.Repository;

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

Java_ByteCode_4.png

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

Java_ByteCode_5.png

BCEL ClassLoaderFastjson等漏洞的利用链构造时都有被用到,这个类与TemplatesImpl都出自于Apache Xalan,但由于一些原因在Java 8u251的更新中这个ClassLoader被移除了。