Java动态加载字节码 这次是Java字节码学习,依旧是跟着P牛
Java字节码是什么 Java字节码是在Java虚拟机执行时用的一类指令,这是一种中间代码,是Java源代码经过编译后生成的一种二进制文件,使得Java具有跨平台的特性
在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.path
和java.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(); } }
这里也就成功请求到并执行了/Hello.class
文件,输出了”Hello”
所以作为攻击者,如果能够控制目标java ClassLoader
的基础路径为一个http
服务器,那么就可以利用远程加载的方式来执行任意代码了
利用ClassLoader#defineClass
直接加载字节码 不管是加载远程的class
文件,还是本地的class
或jar
文件,Java经历的都是下面的过程:
1 ClassLoadedr#loadClass --> ClassLoader#findClass --> ClassLoader#defineClass
其中:
loadClass
有两个作用:一是运行时动态加载指定的类,在加载过程中会读取字节码文件,验证正确性和解析类的依赖关系等等;而是检测类是否重复加载,包括对类加载器的继承关系,已加载类的缓存进行检测。使用ClassLoader
的loadClass
方法加载类时,如果类缓存、父加载器等位置找不到类,就会传入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 ); 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 ; if (_loadedExternalExtensionFunctions != null ) { ret = _loadedExternalExtensionFunctions.get(name); } if (ret == null ) { ret = super .loadClass(name); } return ret; } 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 { 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反序列化利用链以及fastjson
、jackson
的漏洞中,都曾出现过TemplatesImpl
的身影。
利用BCEL ClassLoader加载字节码 BCEL的全名应该是Apache Commons BCEL
,属于Apache Commons
项目下的一个子项目,但其因为被Apache Xalan
所使用,而Apache Xalan
又是Java内部对于JAXP
的实现,所以BCEL也被包含在了JDK的原生库中。
我们可以通过BCEL提供的两个类Repository
和utility
来利用: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); } }
而BCEL ClassLoader
用于加载这串特殊的”字节码”,并可以执行其中的代码
BCEL ClassLoader
在Fastjson
等漏洞的利用链构造时都有被用到,这个类与TemplatesImpl
都出自于Apache Xalan
,但由于一些原因在Java 8u251
的更新中这个ClassLoader
被移除了。