1. 前言
java内存马注入一般分为两种
- 动态注册添加新的listener/filter/servlet/controller等等
- agent注入修改已有class,插入恶意代码
在理解内存马注入前,有几个概念需要掌握的。
- 类加载
- 双亲委派问题以及context
- 类反射
2. 基础
2.1. class对象
java中的对象可以分为两种对象:Class对象和实例对象
- 信息属性:从对象的作用看,Class对象保存每个类型运行时的类型信息,如类
名、属性、方法、父类信息等等。在JVM中,一个类只对应一个Class对象 - 普适性:Class对象是java.lang.Class类的对象,和其他对象一样,我们可以获取
并操作它的引用 - 运行时唯一性:每当JVM加载一个类就产生对应的Class对象,保存在堆区,类
型和它的Class对象时一一对应的关系。一旦类被加载了到了内存中,那么不论通过哪种
方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class对象引用。
JVM不会创建两个相同类型的Class对象
Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过
调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。
2.2.类加载
一个类被加载到内存并供我们使用需要经历如下三个阶段:
- 加载,这是由类加载器(ClassLoader)执行 的。通过一个类的全限定名来获
取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结
构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类
的java.lang.Class对象 - 链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机
的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且
如果必需的话,将常量池中的符号引用转化为直接引用。 - 初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该
类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行
初始化
所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)
触发类加载的方式
- Class.forName("类的全限定名")
- new 类构造方法
除了上述方式,还可以通过网络加载字节码方式来调用外部类
- oadclass:判断是否已加载,使用双亲委派模型,请求父加载器,都为空,使用
findclass - findclass:根据名称或位置加载.class字节码,然后使用defineClass
- defineclass:解析定义.class字节流,返回class对象
- loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是
双亲委派机制),在前面没有找到的情况下,执行 findClass - findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中
说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交
给 defineClass - defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成
一个Java类,Java默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言
代码中
classloader推荐
jxxload_help.PathVFSJavaLoader#loadClassFromBytes org.python.core.BytecodeLoader1#loadClassFromBytes sun.org.mozilla.javascript.internal.DefiningClassLoader#defineCl ass java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource) org.mozilla.classfile.DefiningClassLoader#defineClass org.mozilla.javascript.DefiningClassLoader com.sun.org.apache.bcel.internal.util.ClassLoader com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类装载器是用来把类(class)装载进 JVM 的。JVM 规范定义了两种类型的类装载器:
启动类装载器(bootstrap)和用户自定义装载器(user-defined class loader)。 JVM在运行时会
产生3个类加载器组成的初始化加载器层次结构 ,如下图所示:
//1、获取系统类的加载器 ClassLoader classLoader = ClassLoader.getSystemClassLoader(); System.out.println(classLoader); //2. 获取系统类加载器的父类加载器(扩展类加载器,可以获取). classLoader = classLoader.getParent(); System.out.println(classLoader); //3. 获取扩展类加载器的父类加载器(引导类加载器,不可获取). classLoader = classLoader.getParent(); System.out.println(classLoader);
2.3. 类反射
其实就是获取Class对象,然后调用该对象进行一系列操作
- Class: 是一个类; 一个描述类的类
- 封装了描述方法的 Method
- 描述字段的 Filed
- 描述构造器的 Constructor 等属性
如何得到 Class 对象
- Person.class
- person.getClass()
- Class.forName("com.atguigu.javase.Person")
关于 Method
如何获取 Method:
- getDeclaredMethods: 得到 Method 的数组
- getDeclaredMethod(String methondName, Class ...
parameterTypes) 可以拿到反射类中的公共方法、私有方法、保
护方法、默认访问,但不获得父类方法 - getMethod(String methondName, Class ... parameterTypes)可以
拿到反射类及其父类中的所有公共方法, 但无法获取私有方法
如何调用 Method
- 如果方法时 private 修饰的, 需要先调用 Method 的
setAccessible(true), 使其变为可访问 - method.invoke(obj, Object ... args)
关于 Field
如何获取 Field: getField(String fieldName)
如何获取 Field 的值
- setAccessible(true)
- field.get(Object obj)
如何设置 Field 的值
- field.set(Obejct obj, Object val)
如果属性用 final 修饰的,需要获取Field的mofilers属性,将FINAL约
束去掉,则可修改
ips:
数组反射
声明数组对象
Array.newInstance(int.class, 3);
数组class
Class intArray = Class.forName("[I"); Class byteArray = Class.forName("[B"); Class stringArrayClass = Class.forName("[Ljava.lang.String;"); // 上面的字符串可以通过打印来观察 System.out.println(LinkOption[].class); // 输出 class [Ljava.nio.file.LinkOption; // 取巧 Class theClass = getClass(theClassName); Class stringArrayClass = Array.newInstance(theClass, 0).getClass();
获取数组值
Array.get(obj, index);
2.4. 双亲委派
双亲委派的作用:
- 为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采
用了双亲委派的方式来加载类,防止相同类的重复加载。一般来说应用启动的
时候会有统一的AppClassLoader来加载项目里的class - 保证启动类加载器优先加载,防止JDK的class被篡改
打破双亲委派:ClassLoader#loadClass方法就是以双亲委派逻辑编写的,只要继承
ClassLoader重写loadClass去掉双亲委派的代码就可以打破双亲委派。也可以通过
defineclass绕过loadclass。
tomcat类加载器需要破坏双亲委派机制
tomcat是个web容器,要解决以下问题
- 一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖
同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离
的 - 部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类
库被加载进JVM - web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离
- web容器支持jsp文件修改后不用重启,jsp文件也是要编译成.class文件的,支持
HotSwap功能
- 架构图
tomcat自己定义的类加载器
- CommonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat和
各个webapp访问 - CatalinaClassLoader:tomcat私有的类加载器,webapp不能访问其加载路径下的
class,即对webapp不可见 - SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见
- WebappClassLoader:webapp私有的类加载器,只对当前webapp可见
- JspClassLoader
- 每一个web应用程序对应一个WebappClassLoader,每一个jsp文件对应一个
JspClassLoader,所以这两个类加载器有多个实例
Tomcat 中有 4 类容器组件,从上至下依次是:
- Engine,实现类为 org.apache.catalina.core.StandardEngine
- Host,实现类为 org.apache.catalina.core.StandardHost
- Context,实现类为 org.apache.catalina.core.StandardContext
- Wrapper,实现类为 org.apache.catalina.core.StandardWrapper
“从上至下” 的意思是,它们之间是存在父子关系的
- Engine:最顶层容器组件,其下可以包含多个 Host
- Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context
- Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
- Wrapper:一个 Wrapper 代表一个 Servlet
srping
3. 动态注册方式注入
关于各个协议和组件的内存马的构造思路其实都大同小异
- 分析涉及处理请求的对象,阅读它的源码看看是否能获取请求内容,同时能否
控制响应内容 - 然后分析该对象是如何被注册到内存当中的,最后我们只要模拟下这个过程即
可 - 一般的流程就是request->context->addListener/addFilter
我怎么拿到当前请求的request对象,request对象里一般会有context,context里面一
般都有一些注册组件的方法或者变量存储
需要注意问题
- 如果不是web相关类加载器,可能出现类加载报类找不到的问题,这是因为双亲
委派隔离 - 如果用当前上下文的类加载,则相同类名只能加载一次
要解决上面两个问题,就是基于当前上下文的类加载去new一个新的类加载器。
如下用Mlet就是一种方法
new javax.management.loading.MLet(new java.net.URL[0], conreq.getClass().getClassLoader())
参考代码
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[] {byte[].class, int.class, int.class}); defineClassMethod.setAccessible(true); Class cc = (Class) defineClassMethod.invoke(new javax.management.loading.MLet(new java.net.URL[0], conreq.getClass().getClassLoader()), new Object[]{classBytes, new Integer(0), new Integer(classBytes.length)});
在研究中间件之前,最好去了解下该中间件的发展历史,有哪些版本,因为不同版
本的代码肯定会有不同层度的改动,你的内存马是否适配每个版本是个问题