JAVA安全学习笔记--静态代理&动态代理

静态代理&动态代理

1. Java的代理模式

先说说什么是代理模式,要说代理模式,得从代理说起。举我们生活中一个非常熟悉的例子,租房大部分情况下都不是直接找房东,而是通过中介代理租房的操作。Java中的代理模式也是这个道理。

Java中的代理模式又分为静态代理与动态代理。

静态代理

静态代理是指在编译时期就已经确定了代理类的实现。代理类需要实现与目标对象相同的接口,并持有目标对象的引用,通过代理对象调用目标对象的方法。

依旧以租客找中介向房东租房为例子。想要用代码实现它,就必须有4个文件,一个是房源定义了实现租房的接口,二是房东实现了租房的接口,三是中介代理租房,四是租户来租房。

这里感谢Drun1baby师傅提供的代码:

https://drun1baby.top/2022/06/01/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-04-JDK%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86

  • Rent.java:这是一个接口,可以抽象的理解为房源,作为房源,它有一个方法 rent()租房
package StaticProxy;

// 租房的接口
public interface Rent {

    public void rent();
}
  • Host.java:实现Rent接口
package StaticProxy;

public class Host implements Rent {

    public void rent(){
        System.out.println("房东要出租房子");
    }
}
  • Proxy.java:这是一个类,这个类是中介,也就是代理,他需要有房东的房源,然而我们通常不会继承房东,而会将房东作为一个私有的属性 host,我们通过 host.rent() 来实现租房的方法。并且中介肯定海货收取中介费,也就是说代理是可以扩展自己的方法的,避免修改了原有的类。
package StaticProxy;

// 中介
public class Proxy {

    private Host host;

    public Proxy(){}
    public Proxy(Host host){
        this.host = host;
    }

    public void rent(){
        host.rent();
        contract();
        fare();
    }

    // 看房
    public void seeHouse(){
        System.out.println("中介带你看房");
    }

    // 收中介费
    public void fare(){
        System.out.println("收中介费");
    }

    // 签租赁合同
    public void contract(){
        System.out.println("签租赁合同");
    }
}
  • Client.java:租客,实现找中介看房
package StaticProxy;

// 启动器
public class Client {
    public static void main(String[] args) {
        Host host = new Host();
        Proxy proxy = new Proxy(host);
        proxy.rent();
    }
}

这样基本的看房便完成了,我们可以运行一下main方法

image-20250907133009003

优点:

  • 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .
  • 公共的业务由代理来完成 . 实现了业务的分工 ,
  • 公共业务发生扩展时变得更加集中和方便 .

缺点 :

  • 一个真是类对应一个代理角色,代码量翻倍,开发效率降低 .

我们想要静态代理的好处,又不想要静态代理的缺点,所以 , 就有了动态代理 !

动态代理

动态代理是指在运行时动态生成代理类,而不需要提前定义实现类。它的实现主要依赖于Java的java.lang.reflect.ProxyInvocationHandler

前面所讲的静态代理中,每多一个房东就需要多一个中介,这显然不符合生活认知(对于租客来说,如果是用静态代理模式,每当想要换一个房东,那就必须要再换一个中介,在开发中,如果有多个中介代码量就更大了)

动态代理的出现就是为了解决上面静态代理的缺点。

动态代理的实现步骤

  1. 定义接口。
  2. 创建InvocationHandler接口的实现类,用来处理方法调用。
  3. 使用Proxy.newProxyInstance()生成动态代理对象。

实例代码:

首先是接口类UserService.java

package DynamicProxy;

public interface UserService {
    public void add();
    public void delete();
    public void update();
    public void query();
}

然后是实现接口的类UserServiceImpl.java

package DynamicProxy;

public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("增加了一个用户");
    }

    @Override
    public void delete() {
        System.out.println("删除了一个用户");
    }

    @Override
    public void update() {
        System.out.println("更新了一个用户");
    }

    @Override
    public void query() {
        System.out.println("查询了一个用户");
    }
}

接着,是动态代理的实现类

package DynamicProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class UserProxyInvocationHandler implements InvocationHandler {

    // 被代理的接口
    private UserService userService;

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    // 动态生成代理类实例
    public Object getProxy(){
        Object obj = Proxy.newProxyInstance(this.getClass().getClassLoader(), userService.getClass().getInterfaces(), this);
        return obj;
    }

    // 处理代理类实例,并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log(method);
        Object obj = method.invoke(userService, args);
        return obj;
    }

    //业务自定义需求
    public void log(Method method){
        System.out.println("[Info] " + method.getName() + "方法被调用");
    }
}
  • 最后编写我们的 Client,也就是启动器
package DynamicProxy;

import DynamicProxy.UserServiceImpl;

public class Client {
    public static void main(String[] args) {
        // 真实角色
        UserServiceImpl userServiceImpl = new UserServiceImpl();
        // 代理角色,不存在
        UserProxyInvocationHandler userProxyInvocationHandler = new UserProxyInvocationHandler();
        userProxyInvocationHandler.setUserService((UserService) userServiceImpl); // 设置要代理的对象

        // 动态生成代理类
        UserService proxy = (UserService) userProxyInvocationHandler.getProxy();

        proxy.add();
        proxy.delete();
        proxy.update();
        proxy.query();
    }
}

我们先运行看看效果,再解释代码

image-20250907134655905

代码的解释

我们从Client.java,也就是启动器来说

  • 首先创建了一个UserServiceImpl对象,实现了UserService接口。

  • 然后创建了 UserProxyInvocationHandler 并把真实对象传进去:setUserService(...)

    • 注意到这里使用了(UserService)强制类型转换为接口类了,因为我多态代理其实是对接口进行代理,不同于静态代理是对实现类进行代理。
  • 再接着通过 getProxy()(内部调用 Proxy.newProxyInstance(...))得到代理对象 proxy(类型是 UserService)。

    • 这里需要重点说一下getProxy方法,我们可以在代理类中看对应的实现代码
      public Object getProxy(){
          Object obj = Proxy.newProxyInstance(this.getClass().getClassLoader(), userService.getClass().getInterfaces(), this);
          return obj;
      }

    注意到这里调用了Proxy类中的newProxyInstance方法,方法中一共有三个参数。其中this.getClass().getClassLoader()

    为了获取类的加载器。userService.getClass().getInterfaces()则是获取实现类实现的所有接口。this则代指调用此方法的对象。最终是返回了生成的代理对象。

  • 然后通过代理对象调用add()delete()等方法

    • 在代理对象中便会触发invoke方法
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          log(method);
          Object obj = method.invoke(userService, args);
          return obj;
      }

    其中参数中的proxy便是指的代理对象,Method则是代指方法对象(这里便指的是add(),delete()等方法),Object[] args是方法的参数数组。

    在函数体中,调用了log(method);这是业务自定义需求。

      public void log(Method method){
          System.out.println("[Info] " + method.getName() + "方法被调用");
      }

    接着调用method对象的invoke方法,传入的参数则是userService(注意:这里的userService其实是实现类UserServiceImpl的实例对象向上转型的,由于多态的特性,所以执行的其实是UserServiceImpl类的方法)

通过以上的代码最终的效果便是,通过动态生成的代理类调用了实现类的方法,并且允许自定义的扩展。

一句话总结:JDK 动态代理就是在运行时生成一个实现了目标接口的“替身”对象,把方法调用统一交给 InvocationHandler,由它在调用真实对象前后插入额外逻辑,从而实现横切关注点的统一管理。

2. 动态代理在反序列化中的作用

讲了一大堆开发里面的内容,那么这个动态代理可以怎么利用到反序列化里面呢?

我们前面提到过要利用反序列化漏洞,我们首先是需要一个入口类的。

这里我们假设存在一个可以被漏洞利用的类方法为B.f,比如Runtime.exec这种,我们将入口类定义为A,我们最理想的情况便是

A(Object) -> Object.f -> 传入B -> B.f

但是实际情况下这种情况比较少见。

回到实战情况。比如我们的入口类A存在O.abc这个方法,也就是A[O] -> O.abc;但是,重点来了,O是一个动态代理的类,调用O的abc方法其实会执行O的invoke方法,如果invoke方法中存在.f方法,便可以漏洞利用了

利用流程如下:

A[O] -> O.abc
O[O2] invoke -> O2.f // 此时将 B 去替换 O2
最后  ---->
O[B] invoke -> B.f // 达到漏洞利用效果

动态代理在反序列化当中的利用和 readObject 是异曲同工的。

readObject 方法在反序列化当中会被自动执行。
invoke 方法在动态代理当中会自动执行。

image-20250907141626559

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注