java安全-动态代理

一、静态代理

本质就是:代理类和真实类实现同一个接口,代理里“包一层”真实对象。

1.业务接口

1
2
3
public interface PayService {
void pay(int amount);
}

2.真实对象

1
2
3
4
5
6
public class PayServiceImpl implements PayService {
@Override
public void pay(int amount) {
System.out.println("pay " + amount);
}
}

3.代理对象(手写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PayServiceProxy implements PayService {
private final PayService target;

public PayServiceProxy(PayService target) {
this.target = target;
}

@Override
public void pay(int amount) {
long t0 = System.nanoTime();
System.out.println("[LOG] before pay");
target.pay(amount);
System.out.println("[LOG] after pay, cost=" + (System.nanoTime() - t0));
}
}

4.使用

1
2
3
4
5
6
public class Demo {
public static void main(String[] args) {
PayService proxy = new PayServiceProxy(new PayServiceImpl());
proxy.pay(100);
}
}

最终实现

image-20260104174244548

pay方法,前后都实现了功能增强

缺点:

每个接口都要手写一个 Proxy 类,太累

想要在同一个某一个接口中的所有方法进行处理同样的事情,静态代理类中的每一个方法都需要重写。

二、动态代理

静态代理的问题:每个接口都得写一个 XXXProxy 类,太麻烦
动态代理就是:只需要写一个 Proxy 类,JDK 运行时直接生成。

1.InvocationHandler类

所有方法都会进这里(相当于“通用代理逻辑”)

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TimingLogHandler implements InvocationHandler {

// 真实对象(被代理对象)
private final Object target;

public TimingLogHandler(Object target) {
this.target = target;
}

// proxy:生成出来的代理对象本身
// method:这次调用的是哪个方法(例如 pay)
// args:这次调用的参数(例如 100)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// 方法调用前增强
long t0 = System.nanoTime();
System.out.println("【动态代理】before " + method.getName());

// 关键:反射调用真实对象的方法
// 等价于 target.pay(100) 这种效果
Object result = method.invoke(target, args);

// 方法调用后增强
System.out.println("【动态代理】after " + method.getName()
+ ", cost=" + (System.nanoTime() - t0));

return result; // 如果方法有返回值,这里要返回;void 的话就是 null
}
}

2.创建代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.Proxy;

public class Demo2 {
public static void main(String[] args) {
PayService target = new PayServiceImpl();

// 让 JDK 生成一个“实现 PayService 接口”的代理类实例
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
new Class[]{PayService.class}, // 代理要“长得像”哪些接口
new TimingLogHandler(target) // 拦截器:方法统一进 invoke
);

// 调用代理对象的方法,最终会进 invoke(...)
proxy.pay(200);
}
}

3.创建代理类参数详解

创建代理对象的核心方法:

1
2
3
4
5
Object proxy = Proxy.newProxyInstance(
类加载器,
接口数组,
调用处理器
);

参数一:ClassLoader(类加载器)

1
target.getClass().getClassLoader()
  • 用来加载生成的代理类
  • 决定代理类“由谁加载到 JVM 中”

参数二:interfaces(代理要实现的接口)

1
target.getClass().getInterfaces()
  • 指定代理类要实现哪些接口
  • JDK 动态代理 只能代理接口

参数三:InvocationHandler(调用处理器 / 拦截器)

1
new TimingLogHandler(target)
  • 代理的核心
  • 所有接口方法调用,最终都会走到这里

内部执行入口

1
public Object invoke(Object proxy, Method method, Object[] args)

三、在反序列化中的利用

简单演示

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
public class ProxyDeserializeDemo {

/** 相当于B:有一个“敏感动作” f(这里用打印代替) */
public static class B implements java.io.Serializable {
private static final long serialVersionUID = 1L;

public void f(String from) {
System.out.println(">>> B.f() 被调用!触发来源 = " + from);
}
}

/** 相当于你文里的 InvocationHandler:O 的方法调用最终都会进 invoke */
public static class H implements InvocationHandler, Serializable {
private final B target;

public H(B target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();

// 关键:反序列化 HashMap 时,会触发 key.hashCode(),从而进入这里
if ("hashCode".equals(name)) {
target.f("hashCode() during HashMap.readObject");
// hashCode 必须返回 int
return 123;
}
// 其他方法默认处理
return method.invoke(this, args);
}
}

public static void main(String[] args) throws Exception {
// 1) 准备代理对象 O(proxy)
B b = new B();
O proxy = (O) Proxy.newProxyInstance(
O.class.getClassLoader(),
new Class[]{O.class},
new H(b)
);

// 2) 把 proxy 放进 HashMap 作为 key(这就是“入口对象图”的一部分)
Map<Object, String> map = new HashMap<>();
map.put(proxy, "value");

// 3) 序列化
byte[] data;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(map);
data = bos.toByteArray();
}

System.out.println("=== 开始反序列化 ===");

// 4) 反序列化:你会看到这里“没调用 abc()”,但会打印 B.f 被触发
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
Object obj = ois.readObject();
}

System.out.println("=== 反序列化结束 ===");
}
}

1
2
3
4
A[O] -> O.abc        //正常方法
O[O2] invoke -> O2.f // 此时将 B 去替换 O2
最后 ---->
O[B] invoke -> B.f // 达到漏洞利用效果

示例中,使用porxy的hashcode方法代替掉 abc 无害方法,可以调试尝试食用!!!

代理的价值在反序列化链里不是“它会被反序列化自动执行”,而是:

它能把一次“看似无害的方法调用”(hashCode/toString/compare/abc)劫持成你 invoke() 里想做的任何事。