Java反序列化系列(3)——JDK动态代理

Java反序列化系列(3)——JDK动态代理

前言

今天我们来讲一下动态代理,动态代理在Java反序列化中会用到但不是非常关键的技术,但既然会用到,多准变一点总是有备无患的。

代理模式

动态代理是种特殊的代理模式,所以在讲动态代理之前先来讲一下什么是代理模式。

设计模式

在学习SpringAop的时候,曾了解过Java的设计模式,设计模式是解决特定问题的一系列套路,它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全解决方案。有如单例模式、工厂模式、代理模式、模板模式等等。

代理模式

定义: 为其它对象提供一个代理对象,并由代理对象控制这个对象的访问。

特点: 1、很直接的,实现同一个接口或者继承同一个抽象类。 2、 代理对象控制对被代理对象的访问。

这是代理模式的通用UML,涉及到的角色如下所示:

image-20230101141355009

上图中,Subject是一个抽象类或者接口,RealSubject是实现方法类,具体的业务执行,Proxy则是RealSubject的代理,直接和client接触的。

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

代理模式优点

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

以租房为例,我们一般用租房软件、找中介或者找房东。这里的中介就是代理者。

首先定义一个提供了租房方法的接口:Rent.java

1
2
3
public interface Rent {
void rent();
}

定义租房的实现类:Host.java

1
2
3
4
5
6
public class Host implements Rent {  

public void rent(){
System.out.println("房东要出租房子");
}
}

定义中介 // 这里中介也实现了租房的接口:Proxy.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
29
30
31
32
// 中介  
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("签租赁合同");
}
}

测试

1
2
3
4
5
6
7
public class Client {  
public static void main(String[] args) {
Host host = new Host();
Proxy proxy = new Proxy(host);
proxy.rent();
}
}

执行结果

1
2
3
房东要出租房子!
签租赁合同
收中介费

这就是静态代理,这样的好处:可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 、公共业务发生扩展时变得更加集中和方便。

缺点:情况要是复杂起来,由于一个真实类对应一个代理角色,接口变了,代理也要跟着变;如果需求是比较重复的,在代理中也需要复写很多重复的代码

深入到实际业务当中,比如我们平常做的最多的 CRUD

UserService.java,这是一个接口,我们定义四个抽象方法。

1
2
3
4
5
6
public interface UserService {  
public void add();
public void delete();
public void update();
public void query();
}

UserServiceImpl.java,实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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("查询了一个用户");
}
}

现在我们需要实现一个功能,在实现CRUD功能时,输出对应的日志。如果在实现类上面添加代码,那么有多少个功能就需要写多少个相似的代码,相当重复繁琐。我们可以增加一个代理**UserServiceProxy.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
29
30
31
32
33
// 代理  
public class UserServiceProxy implements UserService{
private UserServiceImpl userService;

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

public void add() {
log("add");
userService.add();
}

public void delete() {
log("delete");
userService.delete();
}

public void update() {
log("update");
userService.update();
}

public void query() {
log("query");
userService.query();
}

// 增加日志方法
public void log(String msg){
System.out.println("[Debug]使用了 " + msg +"方法");
}
}

测试类

1
2
3
4
5
6
7
8
9
10
public class Client {  
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();

UserServiceProxy proxy = new UserServiceProxy();
proxy.setUserService(userService);
proxy.add();

}
}

执行结果

1
2
[Debug]使用了add方法
增加了一个用户
动态代理
  • 动态代理和静态代理角色一样
  • 动态代理的代理类是动态生成的,不是我们直接写好的
  • 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
    • 基于接口——JDK动态代理
    • 基于类:cglib
    • Java字节码实现:javasist

需要了解两个类:Proxy :代理 ,InvocationHandler :调用处理程序

InvocationHandler是由代理实例的调用处理程序实现的接口。每个代理实例都有一个关联的调用处理程序,当在代理实例上调用方法时,方法调用将被编码并分派到其调用处理程序的invoke方法。

Proxy提供了创建动态代理类和实例的静态方法,它也是又这些方法创建的所有动代理类的超类。

我们来看代码

动态代理的实现类:UserServiceProxy

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
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() + "方法被调用");
}
}

测试类

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

执行结果

1
2
3
4
[Info]add方法被调用
增加了一个用户
[Info]]delete方法被调用
删除了一个用户

动态代理额外的好处:

  • 一个动态代理类代理的是一个接口,一般就是对应的一类业务
  • 一个动态代理类可以代理多个类,只要是实现了同一个接口即可

总结:

  1. 静态代理,代理类需要自己编写代码写成。
  2. 动态代理,代理类通过 Proxy.newInstance() 方法生成。
  3. JDK实现的代理中不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。CGLib可以不需要接口。
  4. 动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。

动态代理在反序列化漏洞中的应用

比方说我们找到了一个有漏洞利用的地方B类的f方法 B.f,然后我们找到一个入口类A,里面接受一个类A[O],最理想的情况是A接受这个类,并且调用这个类的f方法,即A[O] --> O.f ,这样我们把B类作为参数传入进去即可。但现实情况可能没有这么顺利,比如A是调用这个类的其他方法A[O] --> O.xxx,这种情况下如果O是动态代理类,他的invoke方法里可能调用了f,即O[O2] invoke --> O2.f,此时将 B 去替换 O2就调用了危险方法B.f,达到利用漏洞的效果。其核心思想是不管调用其什么方法都会去调用它的动态代理方法。

小结

这里我们就学完了代理模式和动态代理能够产生安全问题的原因,代理模式是并且SpringAop的底层,重要性不言而喻;动态代理的安全应用主要在cc第一条链上和jdk7u21这条链上,在后面走这两条链子时会有更深的体会。