动态代理在Java反序列化中的应用
动态代理简介
何为代理?在日常生活中我们或多或少都接触过房产中介、4S店以及各种各样的代理商,他们在经济社会运行当中扮演着代理的角色,负责对接厂商与客户。
用户如果想要投诉产品、寻求赔偿等,可以统一找到代理商,由代理商向厂商提出,这样便极大的节约了用户的各方面成本。
在编程中所谓的代理模式也是同样的道理,当我们想要对某一个类进行功能扩展而又不想直接修改当前类的代码的时候,我们可以创建一个代理类来对目标类进行包装。
通过在当前类的运行前、运行后、运行异常时添加新的代码从而实现目标类功能的增强与拓展。这便是代理模式的运行。如下图,A表示被代理类,B表示代理类,当A没有被代理时,外界的其他方法可以直接调用A的方法,
当A被代理后,C的方法如果要想调用A的方法就需要先通过B类再由B类调用A类的方法,那么我们便可以在B类中增加一些其他的功能。B类此时就类似一个收保护费的,要想从此过,留下买路钱,正所谓漫天要价,坐地还钱。
代理按照代理类创建的时间节点不同又可分为静态代理与动态代理,所谓静态代理即对每一个被代理类均创建一个对应的代理类以代理其功能并按照需要进行扩展,这样就面临一个困境,即如果有很多的需要被代理的类,
那么就需要手动创建对应数量的代理类,这无疑增加了工作量与管理复杂度。
当目标类逐渐在增多时,对应的代理类数量随之扩张。
而动态代理则能很好地屏蔽这个缺陷,动态代理通过代理接口类实现了代理类的运行时动态生成,无论存在多少被代理类,
只要他们实现了相关的接口便可以动态地进行代理类的生成。
当使用动态代理时,无论由多少个目标类A B D … 只需保证他们都实现了统一接口SA,则只需代理SA接口一次即可对所有的目标类进行统一的管理。
在 JAVA
中,动态代理的实现主要依赖于 Proxy
类以及相应的 InvocationHandler
实现,Proxy
类负责代理类的生成,InvocationHandler
接口负责目标方法的功能扩展。
动态代理实现目标类功能扩展的核心在于InvocationHandler
,用户通过自定义InvocationHandler
可以实现统一的日志管理,状态检查以及其他更高级的功能,如在本文中将会提及的修改方法返回对象、屏蔽方法调用异常、进行方法调用分流等。
动态代理的应用
在JAVA
原生反序列化漏洞的利用过程中,最困难的不在于寻找可控输入的readObject
方法调用,而在于寻找一条可用的反序列化调用链。
在寻找反序列化利用链的过程中,可能会遇到如下几个问题:
- 找到了可以进行反射方法调用的地方但是只能调用特定类型的方法;
- 找到了能够调用某个类的所有
getter
的方法但是方法调用顺序是随机的,某些方法的调用会产生异常导致程序退出。
这些问题都能利用动态代理代理类的特性或者InvocationHandler
实现的功能解决。
修改方法返回对象
使用 sun.reflect.annotation.AnnotationInvocationHandler
可以修改被代理类方法的返回值,其 invoke
方法实现如下。
1 | public Object invoke(Object var1, Method var2, Object[] var3) { |
在上面 invoke
方法的实现中,最终的返回值为var6
,而var6
来自于memberValues
对象get
方法的调用,memberValues
是一个Map
对象,其值在AnnotationInvocationHandler
实例化的过程中被赋予,即该值是可控的。memberValues
对象在取值时其key
为var4
,为被调用方法的方法名,即其值是已知的。又因invoke
方法的返回值为Object
类型,故invoke
方法的返回值可以被修改为用户控制的任意对象。AnnotationInvocationHandler
的这一特性在 Spring1
链中有所体现。Spring1
链是在Spring
框架中发现的一条反序列化利用链,其能达成远程命令执行的效果。
下面是 ysoserial
工具创建Spring1
链的代码。
1 | public Object getObject(String command) throws Exception { |
在上面的代码中,如果将[1]到[7]看作序列化的过程,那么从[7]到[1]就是反序列化的过程。
动态代理发挥作用的过程使用下面的图片进行说明。
在反序列化的过程中首先被调用的是SerializableTypeWrapper$MethodInvokeTypeProvider
的 readObject
方法
1 | private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException { |
在标记[9]的地方invokeMethod
方法被调用,通过反射进行无参方法调用,参数method
为Method
对象,this.provider.getType()
则用于指定调用方法所属对象。
众所周知,TemplatesImpl
类的newTransformer
方法是一个常用的反序列化链片段,刚好该方法是一个无参方法。 所以如果method
刚好是newTransformer
方法,this.provider.getType()
刚好返回TemplatesImpl
对象就恰到好处了。method
来自于标记[8]的方法调用。这也是一个反射方法调用,所以只需要在构造SerializableTypeWrapper$MethodInvokeTypeProvider
对象时设置methodName
属性为newTransformer
即可。
调用方法的问题解决了,还需要调用对象的配合,调用对象来自于this.provider.getType()
的配合,this.provider
是一个TypeProvider
类型,观察其getType
方法的签名发现该方法返回值类型为Type
类型,与所需的TemplatesImpl
类型不匹配。
一般情况下路走到这里就走到死胡同了,不过通过签名提到的AnnotationInvocationHandler
的特性我们可以将这个死胡同打穿形成一条新的路。AnnotationInvocationHandler
的invoke
方法调用可以修改被代理方法的返回值,所以可以创建一个代理类代理 SerializableTypeWrapper$TypeProvider
类,将TemplatesImpl
对象以方法名getType
为key
放到AnnotationInvocationHandler
的memberValues
对象中。
这便是本节标记[3]处代码的意义。
到了这里似乎就可以结束了,但是前面忽略了一点就是getType
本身是返回Type
类型的,虽然替换了返回结果为TemplatesImpl
满足了反序列化链的需求,但是TemplatesImpl
本身并不实现Type
接口。
那么便可以继续生成一个代理类让其同时实现Type
以及Templates
接口,这便是本节标记[2]处代码的意义。AutowireUtils$ObjectFactoryDelegatingInvocationHandler
的invoke
方法是这样的。其会从ObjectFactory
属性中通过getObject方
法 获取目标对象,若要使得getObject
方法返回TemplatesImpl
对象,
则需要再次使用AnnotationInvocationHandler
代理ObjectFactory
。 这就是本节标记[1]处代码的意义。
1 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
通过上面的分析可知,在Spring1
链中,多次使用了AnnotationInvocationHandler
可以修改方法调用的放回值的特性利用动态代理机制完成反序列化利用链片段的连接。
当然,AnnotationInvocationHandler
的使用并不是没有限制的。 首先我们从上面Spring1
链的分析中得知 AnnotationInvocationHandler
的invoke
方法在调用完成后返回被修改后的对象,
该对象要保证能够被接受的变量所兼容,即需要保证接收变量为该返回值的父类型,在Spring1
的例子中invokeMethod的方法签名的第二个参数用于接收修改后的对象,恰好其为Object
类型为所有类型的父类型所以并没有发生异常。
1 | public static Object invokeMethod(Method method, Object target) |
而在代理getType
方法时,因为getType
方法本身接收的响应类型为Type
类型,而我们希望响应TemplatesImpl
类型,所以需要额外借助AutowireUtils$ObjectFactoryDelegatingInvocationHandler
再创建一个代理类同时代理Type
以及Templates
类型以达到目的。
所以当AnnotationInvocationHandler
被用作替换对象类型的时候往往不是单独使用的,其往往需要其他代理类来代理特定的类型以避免类型转换异常。
无关方法调用屏蔽
Jackson
是JAVA
中使用广泛的一个处理JSON
字符串的工具包,在Jackson
的代码中存在一条完成的JAVA
反序列化利用链。该链在实际使用过程中偶尔会执行错误导致不能成功进行命令执行,
这源于该链执行过程中使用的TemplatesImpl
对象的getOutputProperties
方法调用不稳定,当 getStylesheetDOM
先于getOutputProperties
被调用时将导致反序列化失败。
在实践中我们发现当这出现这种情况时,无论你重新尝试多少次都不会再成功执行,除非目标系统重启。
失败的原因在于getStylesheetDOM
方法调用时_sdom
的值为空,又因为_sdom
是一个被transient
修饰的瞬态变量并不参与JAVA
原生的序列化与反序列化。
为了解决这一问题,在Jackson
链的实践中使用了动态代理的特性:使用反射获取一个代理类上的所有方法时,只能获取到其代理的接口方法。
具体的操作方法是创建目标类的代理类,代理其某一个接口,如果该接口定义了我们希望调用的方法而没有定义其他的不被希望调用的方法,那么在通过反射获取代理类的方法时只能获取到被代理接口中定义的方法。
下面图片简要总结了Jackson
链调用过程以及增加动态代理后程序的执行逻辑。
以下代码是ysoserial
中生成Jackson
链Payload
的代码。
1 | public static void main(String[] args) throws Exception { |
在标记代码块[3]中使用了BadAttributeValueExpException
作为反序列化的起始点,BadAttributeValueExpException
是一个比较常用的类,其readObject
方法调用会触发val
属性值的toString
方法。
将val
设置为一个POJONode
对象,其toString
方法定义在父类BaseJsonNode
方法中。
1 | public String toString() { |
nodeToString
方法将调用STD_WRITTER
的writeValueAsString
方法,该方法在调用过程中会通过反射的方式尝试获取POJONode
方法实例化时传入参数对象的所有getter
方法,然后按照获取顺序依次调用。
1 | public static String nodeToString(BaseJsonNode n) { |
getter
方法的获取顺序是随机的并且会被缓存机制缓存,当StylesheetDOM
的获取在OutputProperties
之前时便会因为_sdom
为空导致程序异常退出。
为了处理这个问题使用了JdkDynamicAopProxy
代理Templates
接口创建代理类,代理类在通过反射获取方法时只能获取到被代理接口中定义的方法,如此便可以屏蔽掉getStylesheetDOM
方法,这便是标记代码块[2]处代码的意义。
方法调用分流
如上一节通过代理接口屏蔽无关方法调用从而屏蔽异常的方式有严格的限制,需要满足恰好被代理接口定义了我们需要的目标方法且没有定义一些其他的可能对结果产生干扰的方法,这种情况往往可遇不可求。本节将提供另外一种对方法调用异常进行屏蔽的方法。CompositeInvocationHandlerImpl
能够对方法调用异常进行屏蔽的核心在于其能根据方法名对方法的调用进行分流,对于在调用过程中会导致异常的方法通过AnnotationInvocationHandler
直接替换掉响应结果即可屏蔽可能导致异常的过程调用。
具体的实现方法是通过将可能导致异常的方法所属类作为key
,将AnnotationInvocationHandler
作为value
放到一个Map
中,当方法调用时首先尝试从这个Map
中获取对应的handler
再使用该handler
进行实际的方法调用。
1 | public Object invoke( Object proxy, Method method, Object[] args ) |
json-lib
同样是用来处理JSON
字符串的Java
工具包,其中也存在一条反序列化利用链,这条链被成为JSON1
。
在JSON1
链中便使用了方法调用分流方式来屏蔽getCompositeType
方法调用导致的异常。
1 | public static Map makeCallerChain(Object payload, Class... ifaces) throws OpenDataException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, Exception, ClassNotFoundException { |
JSON1
在反序列化的过程中会触发getCompositeType
方法并产生异常,故使用CompositeInvocationHandlerImpl
对CompositeData
类中的getter
进行分流处理,使用AnnotationInvocationHandler
直接响应有效对象避免调用过程中的异常。
而对于需要调用的目标方法(TemplatesImpl的getOutputProperties方法)则使用JdkDynamicAopProxy
代理进行简单的反射方法调用即可。JSON1
链的简要调用流程图下。
拓展
在JSON1
的例子中我们使用了CompositeInvocationHandlerImpl
根据方法声明类选择不同的invocationHandler
对方法进行处理,其目的是为了屏蔽getCompositeType
调用异常,
同样的在Jackson
中也存在方法调用异常,不过在这里利用的却是反射获取代理类的特性。那么是否可以将CompositeInvocationHandlerImpl
用于Jackson
中呢?
以下是对原始的Jackson
链进行改造后的代码。
1 | CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); |
运行修改后的Jackson
链,正常弹出计算器,同时在日志中同样可以查看目标方法被成功调用
在jdk8u71
以下版本的运行环境中该修改可以成功运行,但在之后的版本中该修改是不能成功运行的,原因在于使用了AnnotationInvocationHandler
来替换getStylesheetDOM
的响应结果,AnnotationInvocationHandler
在jdk8u71
后被增加了新的限制,其只能代理使用了注解的方法,如:@Override
注解等。在JSON1
链中我们代理的getCompositeType
方法便存在注解@Override
,所以不受jdk
版本限制。
总结
随着技术的发展以及JDK不断地更新,AnnotationInvocationHandler
的使用被加上了限制,在jdk8u71后已经不再能够被随意使用。虽然如此,利用动态代理思想解决问题的思路是一以贯之的。
在不同的实践中也可能存在着其他更具利用价值的InvocationHandler
值得我们去发掘。攻防对抗就是这样魔高一丈道高一尺,只有在攻与防的不断对抗中砥砺前行才能发现更多有效的思路与技巧。