在spring-aop中挖掘新反序列化gadget-chain
2025-1-12 05:35:0 Author: mp.weixin.qq.com(查看原文) 阅读量:0 收藏

目录

  • • 前言
  • • 挖掘过程
    • • AbstractAspectJAdvice
    • • ReflectiveMethodInvocation
    • • JdkDynamicAopProxy
  • • 调用链
  • • 代码示例

前言

前阵子在某个安全会议面基,和@jsjcw和@杨悦师傅交流的时候,他们透露最近新挖了一条仅依赖spring-aop的java原生反序列化gadget-chain,但没提到详情。

前天笔者正好在整理今年的一些笔记,有部分资料也和反序列化相关,就想起来了这个事情。于是就想挑战一下自己是否也能独立从spring-aop挖一条反序列化gadget-chain,最终运气不错,发现了一条gadget-chain,所需依赖为:spring-aop + aspectjweaver,能力是反射调用方法。

挖掘过程

AbstractAspectJAdvice

通过污点搜索和分析,注意到了org.springframework.aop.aspectj.AbstractAspectJAdvice这个类:即使在反序列化之后,也天然拥有反射调用方法的能力(因为Method本身并不能反序列化,所以这种情况并不多见)

  1. 1. invokeAdviceMethodWithGivenArgs方法有反射调用方法的能力
  2. 2. readObject之后通过反射重新实例化了aspectJAdviceMethod属性
// invokeAdviceMethodWithGivenArgs.java
privatefinal Class<?> declaringClass;
privatefinal String methodName;
privatefinal Class<?>[] parameterTypes;
protectedtransient Method aspectJAdviceMethod;

protected Object invokeAdviceMethodWithGivenArgs(Object[] args)throws Throwable {
    Object[] actualArgs = args;
    if (this.aspectJAdviceMethod.getParameterCount() == 0) {
        actualArgs = null;
    }
    try {
        ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
        returnthis.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
    }
    ...
}

privatevoidreadObject(ObjectInputStream inputStream)throws IOException, ClassNotFoundException {
    inputStream.defaultReadObject();
    try {
        this.aspectJAdviceMethod = this.declaringClass.getMethod(this.methodName, this.parameterTypes);
    }
    ...
}

反射调用方法的三要素:Method、Object、Args,虽然还不清楚invokeAdviceMethodWithGivenArgs中传入的args是否可控,但可以先简化场景,对于无参方法肯定是可行的,而公开的无参利用方法中就有不少,可以暂时认为Method和Args都解决了。

现在还要解决Object的问题,代码中通过this.aspectInstanceFactory.getAspectInstance()获取反射对象。此时目标是找到一个同时实现AspectInstanceFactorySerializable的子类,并且getAspectInstance方法可以返回指定的对象。

org.springframework.aop.aspectj.SingletonAspectInstanceFactory刚好满足。

到这里,org.springframework.aop.aspectj.AbstractAspectJAdvice#invokeAdviceMethodWithGivenArgs可以作为污点方法就基本定下来了。

ReflectiveMethodInvocation

接下来往上找调用链,多条调用链都会经过org.springframework.aop.framework.ReflectiveMethodInvocation#proceed走到org.springframework.aop.aspectj.AbstractAspectJAdvice#invokeAdviceMethodWithGivenArgs。例如:

org.springframework.aop.framework.ReflectiveMethodInvocation#proceed->
org.springframework.aop.aspectj.AspectJAroundAdvice#invoke->
org.springframework.aop.aspectj.AbstractAspectJAdvice#invokeAdviceMethod(org.aspectj.lang.JoinPoint, org.aspectj.weaver.tools.JoinPointMatch, java.lang.Object, java.lang.Throwable)->
org.springframework.aop.aspectj.AbstractAspectJAdvice#invokeAdviceMethodWithGivenArgs

ReflectiveMethodInvocation#proceed方法如下:

第一个点是interceptorOrInterceptionAdvice的获取,是从interceptorsAndDynamicMethodMatchers中拿到的,该属性本身定义就是一个List,可以序列化,而索引currentInterceptorIndex本身也只是int类型。因此可以认为interceptorOrInterceptionAdvice是可控的。

第二个点是interceptorOrInterceptionAdvice的类型,按照笔者上面的调用链,这个对象的类型是org.springframework.aop.aspectj.AspectJAroundAdviceAbstractAspectJAdvice的子类),那么proceed代码是走下面的分支,省去了一部分麻烦:)

第三个点是ReflectiveMethodInvocation本身并没有实现Serializable接口,想要在反序列化过程中使用,只能依赖于动态创建。直接往上找到创建ReflectiveMethodInvocation的地方,发现正是熟悉的老朋友org.springframework.aop.framework.JdkDynamicAopProxy#invoke。并且在创建后刚好就调用proceed方法,完美符合要求。

分析ReflectiveMethodInvocation的构造方法,需要控制传入的interceptorsAndDynamicMethodMatchers,也即对应了上面JdkDynamicAopProxy#invoke中的chain。

到这里为止,梳理一下目前的思路:

  1. 1. 通过反序列化触发JdkDynamicAopProxy#invoke方法,这个简单,本身就是动态代理。
  2. 2. 在JdkDynamicAopProxy#invoke方法中,控制chain的生成,需要让List放入目标对象AspectJAroundAdvice
  3. 3. 通过chain创建出ReflectiveMethodInvocation实例,并调用其proceed方法
...
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
    Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
    retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
    MethodInvocation invocation =
            new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
    retVal = invocation.proceed();
}
...
  1. 4. 通过ReflectiveMethodInvocation#proceed -> AspectJAroundAdvice#invoke->AbstractAspectJAdvice#invokeAdviceMethodWithGivenArgs,走到最后的污点函数,反射调用执行代码。

JdkDynamicAopProxy

接下来就是解决在JdkDynamicAopProxy#invoke方法中,控制chain变量的生成过程。

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

目标是让getInterceptorsAndDynamicInterceptionAdvice返回一个List,List里面有一个元素,是我们指定的任意对象。

分析org.springframework.aop.framework.AdvisedSupport#getInterceptorsAndDynamicInterceptionAdvice方法,实际上有两个获取方式:

  1. 1. 从缓存的methodCache中获取
  2. 2. 通过getInterceptorsAndDynamicInterceptionAdvice方法
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
    MethodCacheKey cacheKey = new MethodCacheKey(method);
    List<Object> cached = this.methodCache.get(cacheKey);
    if (cached == null) {
        cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
                this, method, targetClass);
        this.methodCache.put(cacheKey, cached);
    }
    return cached;
}

先看了一下methodCache属性,本身加了transient修饰符,并且在readObject方法中是直接新建的,没有任何元素,判断这条路是不可行的。

然后再分析getInterceptorsAndDynamicInterceptionAdvice是否可用,在这个方法中,三个入参都是可控的,Advised config实际上就是AdvisedSupport实例。

这个方法最终返回的就是interceptorList对象,核心是分析这个对象如何添加元素,然后往上找这个元素是怎么生成的。

public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
        Advised config, Method method, @Nullable Class<?> targetClass)
 {

    AdvisorAdapterRegistryregistry= GlobalAdvisorAdapterRegistry.getInstance();
    Advisor[] advisors = config.getAdvisors();
    List<Object> interceptorList = newArrayList<>(advisors.length);
    Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
    BooleanhasIntroductions=null;
    
    for (Advisor advisor : advisors) {
        if (advisor instanceof PointcutAdvisor) {
            if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
                MethodMatchermm= pointcutAdvisor.getPointcut().getMethodMatcher();
                boolean match;
                if (mm instanceof IntroductionAwareMethodMatcher) {
                    if (hasIntroductions == null) {
                        hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
                    }
                    match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
                }
                else {
                    match = mm.matches(method, actualClass);
                }
                if (match) {
                    MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
                    if (mm.isRuntime()) {
                        for (MethodInterceptor interceptor : interceptors) {
                            interceptorList.add(newInterceptorAndDynamicMethodMatcher(interceptor, mm));
                        }
                    }
                    else {
                        interceptorList.addAll(Arrays.asList(interceptors));
                    }
                }
            }
        }
        elseif (advisor instanceof IntroductionAdvisor) {
            IntroductionAdvisoria= (IntroductionAdvisor) advisor;
            if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
                Interceptor[] interceptors = registry.getInterceptors(advisor);
                interceptorList.addAll(Arrays.asList(interceptors));
            }
        }
        else {
            Interceptor[] interceptors = registry.getInterceptors(advisor);
            interceptorList.addAll(Arrays.asList(interceptors));
        }
    }

    return interceptorList;
}

经过分析,无论走哪个分支,这个元素最终都是通过registry.getInterceptors(advisor)获取的,而registry则是直接通过静态GlobalAdvisorAdapterRegistry.getInstance()方法获取的静态单例类

这个时候笔者还以为已经凉了,因为这种静态单例类一般无法通过反序列化过程控制的,要想修改这种实例的元素或属性,还需要其他执行分支甚至其他反序列化gadget chain来调用实例的方法。

"来都来了...",所以还是认真审了一下org.springframework.aop.framework.adapter.DefaultAdvisorAdapterRegistry#getInterceptors方法。

结果一下子就看到了希望,核心逻辑:advice变量是可控的,如果这个变量同时实现AdviceMethodInterceptor接口,则可以将其添加到interceptors,这个interceptors就是我们最终返回的目标chain。

public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
    List<MethodInterceptor> interceptors = newArrayList<>(3);
    // 可控,只要可序列化即可
    Adviceadvice= advisor.getAdvice();
    if (advice instanceof MethodInterceptor) {
        // 如果advice本身实现了MethodInterceptor接口,将advice直接添加到interceptors!!!
        interceptors.add((MethodInterceptor) advice);
    }
    for (AdvisorAdapter adapter : this.adapters) {
        if (adapter.supportsAdvice(advice)) {
            interceptors.add(adapter.getInterceptor(advisor));
        }
    }
    if (interceptors.isEmpty()) {
        thrownewUnknownAdviceTypeException(advisor.getAdvice());
    }
    return interceptors.toArray(newMethodInterceptor[0]);
}

笔者的需求是interceptors中元素是一个AspectJAroundAdvice实例,很显然,这个类满足了实现MethodInterceptor接口的需求,但并没有实现Advice....

看到这里,熟悉反序列化或者是看过笔者上一篇文章文章的小伙伴,应该会一下子就想到动态代理,而我们恰好又有spring-aop依赖,JdkDynamicAopProxy本来不就是用来做这个东西的吗?

通过JdkDynamicAopProxy来同时代理AdviceMethodInterceptor接口,并设置反射调用对象是AspectJAroundAdvice,如果后续仅被调用MethodInterceptor接口的方法,就可以直接混水摸鱼,如果还会调用Advice接口的方法,则可以再尝试使用CompositeInvocationHandlerImpl,详情可以参考上一篇文章《高版本Fastjson在Java原生反序列化中的利用》。

经过测试,这里只需要JdkDynamicAopProxy就可以了。到这里,整条gadget chain的主要障碍都基本被扫清了,剩下的就是一些边边角角的修改。

调用链

这条gadget chain的最终能力是反射调用方法,利用方式有不少的可能性,不过作为反序列化最好用的老朋友,这里演示依然使用templatesImpl#newTransformer作为最终一环的执行函数。

gadget chain如下所示:


代码示例

https://github.com/Ape1ron/SpringAopInDeserializationDemo1


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2MDY2ODc5MA==&mid=2247484198&idx=1&sn=6b6a82bb543e879295b7cd2d85f3a37f&chksm=ce23953ff9541c29418831b4e192b385e5c92d59562b40a7a28283e8468773caac9ad9736b1c&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh