Java反序列化基础篇-04-JDK动态代理
0x00 前言
时不我待,只争朝夕。
参考教程:
Java动态代理详细讲解-使用方式及应用场景_动态代理常用的地方-CSDN博客
0x01 概述
要学习Java的动态代理,就要先了解两个概念:静态/动态、代理。
在 Java 中,编译期间即可确定的功能被称为静态功能,例如静态代理;而在程序运行时才动态确定的功能被称为动态功能,例如动态代理。
给出一个有趣的例子:
通俗来说,就是我想点份外卖,但是手机没电了,于是我让同学用他手机帮我点外卖。在这个过程中,其实就是我同学(代理对象)帮我(被代理的对象)代理了点外卖(被代理的行为),在这个过程中,同学可以完全控制点外卖的店铺、使用的APP,甚至把外卖直接吃了都行(对行为的完全控制)。
我们可以总结出代理的四个要素:
- 代理对象
- 被代理的对象
- 被代理的行为
- 对行为的完全控制
一言以蔽之:被代理的对象的被代理的行为会被代理对象执行,代理对象完全控制被代理的行为。
我们要实现代理功能,实际上就是实现上述的四个要素。
先实现静态代理
0x02 静态代理
假设有一个需求:当执行一个方法excute
时,需要记录该方法的执行时间,最简单的方法就是记录方法开始和结束时的时间戳,但是如果该方法逻辑比较复杂,就需要写很多个记录时间戳的代码,如下:
1 | package com.pax.UniserializeProxy.DynamicProxies; |
能不能简化代码呢?有一个办法:跳出方法内,在执行者层面进行时间戳记录:
1 | package com.pax.UniserializeProxy.DynamicProxies; |
但是如果程序在很多个地方被调用——也就是说有很多个执行者,或者说某个执行者需要调用很多个类似excute
这样需要记录时间戳的方法,那么我们还是要写很多个记录时间戳的代码。
基于解决第一个问题:如果程序在很多个地方被调用——也就是说有很多个执行者,可以采用静态代理来帮助我们统一记录时间戳:
1 | package com.pax.UniserializeProxy.DynamicProxies; |
这样每一个执行者只需要调用Proxy的execute方法即可:
1 | package com.pax.UniserializeProxy.DynamicProxies; |
但是我们往往还需要同时面对第二个问题:某个执行者需要调用很多个类似excute
这样需要记录时间戳的方法。最复杂的情况下就是:不仅有很多个执行者,而且每个执行者都需要执行很多个类似execute
的方法(这些方法不一定来自相同的类),这时就需要动态代理了。
0x03 动态代理
设想
不妨先想想,我们需要动态代理解决什么问题,怎么解决问题?
问题是:不仅有很多个执行者,而且每个执行者都需要执行很多个类似execute
的方法(这些方法不一定来自相同的类)。
其实execute方法属于一个类,称之为Executor
类。我们不妨把该类的所有方法都代理了,这样就算该类新增方法,我们也能通过一个代理便捷地使用这些方法。
但是这样仍然不够,如果另一个类ExcutorToo
类也有对应的方法,难道我要再搞一个对应的代理?太麻烦了。能不能直接面向这些需要代理的方法呢?
没错,面向接口,代理接口!
操作
我们前面谈过代理四要素,动态代理也需要满足这四个要素。基于此,下面我们自己尝试产生一个动态代理
代理对象
我们需要知道动态代理属于哪个类,换言之,动态代理怎么产生的。动态代理属于如下类
java.lang.reflect.Proxy
产生方式如下:
1 | public static Object newProxyInstance(ClassLoader loader, |
我们可以从方法签名里看到这四个要素:
newProxyInstance
:代理对象ClassLoader loader
:被代理的对象Class<?>[] interfaces
:被代理的行为InvocationHandler h
:行为的完全控制
被代理的行为
应该是一个接口
1 | package com.pax.UniserializeProxy.Proxy_term; |
被代理的对象
该对象应该是实现接口的对象
1 | package com.pax.UniserializeProxy.Proxy_term; |
行为的完全控制
这里是创建动态代理的核心!
先了解一下这个类:InvocationHandler
1 | public class TimeLogHandler implements InvocationHandler { |
InvocationHandler
类定义了代理的行为该如何实现。
既然是要实现代理行为的完全控制,那么就需要提供代理的其他三个要素,看下面代码:
1 | package com.pax.UniserializeProxy.Proxy_term; |
代理对象:Object proxy
被代理的对象和方法:Method method
(不懂的话可以去了解一下Method
类)
最后,行为的完全控制的代码实现在invoke
方法的函数体。
产生动态代理
上面三个要素都实现完毕,可以看看怎么生成动态代理类了:
1 | package com.pax.UniserializeProxy.Proxy_term; |
通过Proxy.newProxyInstance
方法生成代理对象,然后执行代理对象对应的方法。
输出:
最简单的使用
在主程序里最简单的操作:
1 | package com.pax.UniserializeProxy.Proxy_term; |
- 代理对象:
ExecutorInterface
类型的executor
- 被代理的对象(实际上是个类,不要被称呼误导):
Executor
类 - 被代理的行为以及对该行为的完全控制:
executor.hello()
,该方法实现:(对接口定义的所有方法都适用)
进一步思考
如果此时Executor类新增方法,我们还能不能使用动态代理呢?
可以的,但是接口必须要有该方法,换言之,如果接口没有该方法,就不能用动态代理来代理该方法。
进一步想想,其实动态代理是面向接口的。如果一个类继承了该接口,那么我们就可以动态代理这个类,不管我们需要用几个方法(接口有定义这些方法),代理对象都可以直接使用。
小结
现在读者可以再想想动态代理是怎么解决这两个问题的
面向接口,是一个很完美的方法。因为接口可以定义好几个方法——被代理的行为;接口又可以被好几个类实现——这些类就是被代理的对象。
如此,就解决了这两个问题:
不仅有很多个执行者,而且每个执行者都需要执行很多个类似execute的方法(这些方法不一定来自相同的类)。
0x04 与Java反序列化
- 概述
无论是PHP反序列化还是Java反序列化,都需要一个触发条件。这里还是讨论Java反序列化:
反序列化时readObject
方法会自动执行,与之类似,在动态代理时invoke
方法会被自动执行。
- 例子
入口类A有一个方法:
A[O] -> O.abc
我们希望传入B
,使得B.abc
可以getshell
:
A[B] -> B.abc{Runtime.getRuntime().exec("whoami")}
但是这很理想,一般来说见不到,这是比较常见的情况:O
类是一个动态代理类,O
类的invoke
方法可以调用O2
类的f
方法(该方法是危险方法):
O[O2] -> O2.f
那么不妨如下:
O[B] -> B.f{Runtime.getRuntime().exec("whoami")}
简单来说,就是利用动态代理时invoke
方法自动触发这一机制,进一步延申链长以寻找反序列化的突破口。
- 深入
有没有思考,为什么动态代理会触发invoke
方法,触发谁的invoke
方法?其实上面早已回答:
当进行动态代理时会调用动态代理类的invoke
方法。
- 再深入
通过JDK动态代理机制在Java反序列化里的利用,我们可以学到什么呢?
在实际情况下,我们难以找到简单的链子,我们要去寻找在某些条件下可以自动进行一些操作的机制,进一步延申链长,寻找新的可能。
0x05 结语
写了有一段时间了,原来的那个教程讲不清楚,还好我及时看别的教程了——学习要理解原理,这样才能学得透。