Java反序列化系列(3)——JDK动态代理
前言
今天我们来讲一下动态代理,动态代理在Java反序列化中会用到但不是非常关键的技术,但既然会用到,多准变一点总是有备无患的。
代理模式
动态代理是种特殊的代理模式,所以在讲动态代理之前先来讲一下什么是代理模式。
设计模式
在学习SpringAop的时候,曾了解过Java的设计模式,设计模式是解决特定问题的一系列套路,它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全解决方案。有如单例模式、工厂模式、代理模式、模板模式等等。
代理模式
定义: 为其它对象提供一个代理对象,并由代理对象控制这个对象的访问。
特点: 1、很直接的,实现同一个接口或者继承同一个抽象类。 2、 代理对象控制对被代理对象的访问。
这是代理模式的通用UML,涉及到的角色如下所示:
上图中,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(); } }
|
执行结果
这就是静态代理,这样的好处:可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 、公共业务发生扩展时变得更加集中和方便。
缺点:情况要是复杂起来,由于一个真实类对应一个代理角色,接口变了,代理也要跟着变;如果需求是比较重复的,在代理中也需要复写很多重复的代码
深入到实际业务当中,比如我们平常做的最多的 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(); } }
|
执行结果
动态代理
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生成的,不是我们直接写好的
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口——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方法被调用 删除了一个用户
|
动态代理额外的好处:
- 一个动态代理类代理的是一个接口,一般就是对应的一类业务
- 一个动态代理类可以代理多个类,只要是实现了同一个接口即可
总结:
- 静态代理,代理类需要自己编写代码写成。
- 动态代理,代理类通过 Proxy.newInstance() 方法生成。
- JDK实现的代理中不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。CGLib可以不需要接口。
- 动态代理通过 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这条链上,在后面走这两条链子时会有更深的体会。