动态代理在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值得我们去发掘。攻防对抗就是这样魔高一丈道高一尺,只有在攻与防的不断对抗中砥砺前行才能发现更多有效的思路与技巧。