JavaWeb-动态加载字节码

这次续上CC3里没讲完的动态加载字节码。

利用 URLClassLoader 加载远程class文件

Java的ClassLoader来用来加载字节码文件最基础的方法。

学到这里,知道反射的人应该也很清楚,ClassLoader就是一个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的ClassLoader就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime

ClassLoader的概念的确很宽泛,所以我也不做深入分析,这里要说到的是这个

ClassLoaderURLClassLoader

URLClassLoader实际上是我们平时默认使用的AppClassLoader的父类。

所以我们解释URLClassLoader的工作过程实际上就是在解释默认的Java类加载器的工作流程。

正常情况下,Java会根据配置项sun.boot.class.pathjava.class.path中列举到的基础路径(这些路径是经过处理后的java.net.URL类)来寻找.class文件来加载。

而这个基础路径有分为三种情况:

1
2
3
·	URL未以斜杠/结尾,则认为是一个JAR文件,使用JarLoader来寻找类,即为在Jar包中寻找.class文件 
· URL以斜杠/结尾,且协议名是file,则使用FileLoader来寻找类,即为在本地文件系统中寻找.class文件
· URL以斜杠/结尾,且协议名不是file,则使用最基础的Loader来寻找类

我们正常开发的时候通常遇到的是前两者,那什么时候才会出现使用Loader寻找类的情况呢?

当然是非file协议的情况下,最常见的就是http协议

HTTP小测:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.eddiemurphy;
import java.net.URL;
import java.net.URLClassLoader;

public class HelloClassLoader {
public static void main(String[] args) throws Exception
{
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}

我们编译一个简单的HelloWorld程序,放在http://localhost:8000/Hello.class:

image-20240730003225504

image-20240730003325045

成功请求到我们的/Hello.class文件,并执行了文件里的字节码,输出”Hello World”。

所以,作为攻击者,如果我们能够控制目标Java ClassLoader的基础路径为一个http服务器,则可以利用远程加载的方式执行任意代码了。

利用 ClassLoader#defineClass 直接加载字节码

其实不管是加 载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用:

image-20240730002859489

其中:

1
2
3
·	loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass 
· findClass 的作用是根据基础URL指定的方式来加载类的字节码,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
· defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java 默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。

我们可以编写一个简单的代码,来演示如何让系统的 defineClass 来直接加载字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.eddiemurphy;

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在目标机器上执行任意代码,需要想办法调用构造函数。

执行上述example,输出了Hello World:

image-20240730003751282

这里,因为系统的ClassLoader#defineClass是一个保护属性,所以我们无法直接在外部访问,不得不使用反射的形式来调用。

在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链TemplatesImpl的基石。

利用 TemplatesImpl 加载字节码

重头戏来了。

虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它(否则他也没存在的价值了),这就是TemplatesImpl

引入一个玩意:

com.sun.org.apache.xalan.internal.xsltc.trax.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
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()向前追溯一下调用链:

1
2
3
4
5
TemplatesImpl#getOutputProperties() -> 
TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() ->
TemplatesImpl#defineTransletClasses() ->
TransletClassLoader#defineClass()

追到最前面两个方法TemplatesImpl#getOutputProperties()TemplatesImpl#newTransformer(),这两者的作用域是public,可以被外部调用。我们尝试用 newTransformer()构造一个简单的POC:

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
·	_bytecodes是由字节码组成的数组;

· _name可以是任意字符串,只要不为null即可;

· _tfactory需要是一个TransformerFactoryImpl对象,因为 TemplatesImpl#defineTransletClasses()方法里有调用到_tfactory.getExternalExtensionsMap(),如果是null会出错。

另外值得注意的是,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
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 执行了:

image-20240730005336767

利用BCEL ClassLoader加载字节码

若我们认为所有能够恢复成一个类并在JVM虚拟机里加载的字节序列都在我们的探讨范围内。

那么,BCEL字节码也必然在我们的讨论范围内,且占据着比较重要的地位。

BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,

但其因为被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的原生库中。

关于BCEL的详细介绍,请阅读p牛写的另一篇文章《BCEL ClassLoader去哪了》,建议阅读完这篇文章再来阅读。

我们可以通过BCEL提供的两个类RepositoryUtility来利用:

Repository用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;

Utility用于将原生的字节码转换成BCEL格式的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.eddiemurphy;

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);
}
}

image-20240730005943078

前面加个$$BCEL$$标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.eddiemurphy;

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 {
//encode();
decode();
}
protected static void encode() throws Exception {

JavaClass cls = Repository.lookupClass(evil.Hello.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
protected static void decode() throws Exception {
new ClassLoader().loadClass
("$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMK$c3$40$Q$7d$db$af41$b5$b5$b5$f5$5b$5bOU$c4$5c$E$P$V$_$82x$I$wT$ea$c1$d3$b6$5d$ea$96M$oiZ$f0g$e9A$c1$83$3f$c0$l$r$cen$LA$e8$k$e61of$de$be$d9$fd$f9$fd$fa$Gp$86C$H$F$ac$db$a8$a3Q$c4$86$83MlY$d8$b6$b0$c3P$b8$90$a1L$$$Z$b2$ed$a3$kC$ee$w$g$K$86$b2$_Cq$3b$N$fa$o$7e$e0$7dEL$d5$8f$G$5c$f5x$yu$be$ms$c9$b3$9c0$b8$be$98I$e5$dd$I$a5$a2$O$b1$B$97$nC$a3$fd$e4$8f$f9$8c$7b$8a$87$p$af$9b$c42$iu$cc$j$3c$k$d1T" + "mI$99$c1$e9F$d3x$m$ae$a5$d6w$8c$e4$a9nsa$a1ha$d7$c5$k$f6$ZJ$a6r$d2$7c$8cb5lY8p$d1D$8b$sR$t$" + "M$95T$ff$ae$3f$W$83$e4$l$d5$7d$9d$q$o$a0$c5$a3$v$V$eas32$f2$ee$c9IB$7e$E$P$c8Om$J$cd$60$bd$e8L$d1$96$f5$f6$b2$r$d1B$9e$9e$5c$9f$M$98$b6N$d1$a6$cc$pd$84$f9$e3O$b07Sv$u$W$M$99" + "$c5$KEw$de$40X$o$b4$b1$8a$f2b$f8$dc$88$R$f7$8eL5$fb$81$5c$w$e0$Q$ea$a1$oI$a5$o6$wX$p$a4$ef3$9d" + "$b5$3f3$dbS$w$S$C$A$A").newInstance();
}
}

image-20240730011732779

BCEL ClassLoaderFastjson等漏洞的利用链构造时都有被用到,其实这个类和前面的 TemplatesImpl 都出自于同一个第三方库,Apache Xalan

但是由于各种原因(详见前面所说的 《BCEL ClassLoader 去哪了》),在Java 8u251的更新中,这个ClassLoader被移除了,所以我JDK8u401的版本是会报错的:

image-20240730011841209

但是这种使用Repository.lookupClass()的方法很常用,经常用于打入恶意字节码,后续遇到具体题目的时候就知道了。打复现的时候这几个加载字节码的方式都可能会出现。

参考:

p牛-Java安全漫谈 - 13.Java中动态加载字节码的那些方法

知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具 (zsxq.com)


JavaWeb-动态加载字节码
https://eddiemurphy89.github.io/2024/07/29/JavaWeb-动态加载字节码/
作者
EddieMurphy
发布于
2024年7月29日
许可协议