0x00 前言

时不我待,只争朝夕。

参考教程:

Java动态代理详细讲解-使用方式及应用场景_动态代理常用的地方-CSDN博客

JDK动态代理

0x01 概述

要学习Java的动态代理,就要先了解两个概念:静态/动态代理

在 Java 中,编译期间即可确定的功能被称为静态功能,例如静态代理;而在程序运行时才动态确定的功能被称为动态功能,例如动态代理。

给出一个有趣的例子:

通俗来说,就是我想点份外卖,但是手机没电了,于是我让同学用他手机帮我点外卖。在这个过程中,其实就是我同学(代理对象)帮我(被代理的对象)代理了点外卖(被代理的行为),在这个过程中,同学可以完全控制点外卖的店铺、使用的APP,甚至把外卖直接吃了都行(对行为的完全控制)

我们可以总结出代理的四个要素:

  • 代理对象
  • 被代理的对象
  • 被代理的行为
  • 对行为的完全控制

一言以蔽之:被代理的对象的被代理的行为会被代理对象执行,代理对象完全控制被代理的行为

我们要实现代理功能,实际上就是实现上述的四个要素。

先实现静态代理

0x02 静态代理

假设有一个需求:当执行一个方法excute时,需要记录该方法的执行时间,最简单的方法就是记录方法开始和结束时的时间戳,但是如果该方法逻辑比较复杂,就需要写很多个记录时间戳的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.pax.UniserializeProxy.DynamicProxies;


public class Executor {

public void excute(int x) {
System.out.println("start:" + System.nanoTime());
if (x == 1) {
System.out.println("end:" + System.nanoTime());
return;
}
if (x == 2) {
System.out.println("end:" + System.nanoTime());
return;
}
if (x == 7) {
System.out.println("end:" + System.nanoTime());
return;
}
System.out.println("end:" + System.nanoTime());
return;
}
}

能不能简化代码呢?有一个办法:跳出方法内,在执行者层面进行时间戳记录:

1
2
3
4
5
6
7
8
9
10
11
package com.pax.UniserializeProxy.DynamicProxies;

public class Invoker {
private Executor executor = new Executor();

public void invoke() {
System.out.println("start:" + System.nanoTime());
executor.excute(1);
System.out.println("end:" + System.nanoTime());
}
}

但是如果程序在很多个地方被调用——也就是说有很多个执行者,或者说某个执行者需要调用很多个类似excute这样需要记录时间戳的方法,那么我们还是要写很多个记录时间戳的代码。

基于解决第一个问题:如果程序在很多个地方被调用——也就是说有很多个执行者,可以采用静态代理来帮助我们统一记录时间戳:

1
2
3
4
5
6
7
8
9
10
11
package com.pax.UniserializeProxy.DynamicProxies;

public class Proxy {
private Executor executor = new Executor();

public void execute(int x) {
System.out.println("start:" + System.nanoTime());
executor.execute(x);
System.out.println("end:" + System.nanoTime());
}
}

这样每一个执行者只需要调用Proxy的execute方法即可:

1
2
3
4
5
6
7
8
9
package com.pax.UniserializeProxy.DynamicProxies;

public class Invoker {
private Proxy proxy = new Proxy();

public void invoke() {
proxy.execute(1);
}
}

但是我们往往还需要同时面对第二个问题:某个执行者需要调用很多个类似excute这样需要记录时间戳的方法。最复杂的情况下就是:不仅有很多个执行者,而且每个执行者都需要执行很多个类似execute的方法(这些方法不一定来自相同的类),这时就需要动态代理了。

0x03 动态代理

设想

不妨先想想,我们需要动态代理解决什么问题,怎么解决问题?

问题是:不仅有很多个执行者,而且每个执行者都需要执行很多个类似execute的方法(这些方法不一定来自相同的类)。

其实execute方法属于一个类,称之为Executor类。我们不妨把该类的所有方法都代理了,这样就算该类新增方法,我们也能通过一个代理便捷地使用这些方法。

但是这样仍然不够,如果另一个类ExcutorToo类也有对应的方法,难道我要再搞一个对应的代理?太麻烦了。能不能直接面向这些需要代理的方法呢?

没错,面向接口,代理接口!

操作

我们前面谈过代理四要素,动态代理也需要满足这四个要素。基于此,下面我们自己尝试产生一个动态代理

代理对象

我们需要知道动态代理属于哪个类,换言之,动态代理怎么产生的。动态代理属于如下类

java.lang.reflect.Proxy
产生方式如下:

1
2
3
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

我们可以从方法签名里看到这四个要素:

  1. newProxyInstance:代理对象
  2. ClassLoader loader:被代理的对象
  3. Class<?>[] interfaces:被代理的行为
  4. InvocationHandler h:行为的完全控制

被代理的行为

应该是一个接口

1
2
3
4
5
6
package com.pax.UniserializeProxy.Proxy_term;

public interface ExecutorInterface {
void execute(int x, int y);
}

被代理的对象

该对象应该是实现接口的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.pax.UniserializeProxy.Proxy_term;

public class Executor implements ExecutorInterface{

@Override
public void execute(int x, int y) {
if (x == 3){
return;
}
if (y == 5){
return;
}
return;
}
}

行为的完全控制

这里是创建动态代理的核心!

先了解一下这个类:InvocationHandler

1
2
3
4
5
6
public class TimeLogHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}

InvocationHandler类定义了代理的行为该如何实现。

既然是要实现代理行为的完全控制,那么就需要提供代理的其他三个要素,看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.pax.UniserializeProxy.Proxy_term;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TimeLogHandler implements InvocationHandler {
private Object target;
public TimeLogHandler(Object target){
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("start:" + System.nanoTime());
Object result = method.invoke(target, args);
System.out.println("end:" + System.nanoTime());
return result;
}
}

代理对象:Object proxy

被代理的对象和方法:Method method(不懂的话可以去了解一下Method类)

最后,行为的完全控制的代码实现在invoke方法的函数体。

产生动态代理

上面三个要素都实现完毕,可以看看怎么生成动态代理类了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.pax.UniserializeProxy.Proxy_term;

import java.lang.reflect.Proxy;

public class Invoker {
private ExecutorInterface executor;

public Invoker() {
executor = (ExecutorInterface) Proxy.newProxyInstance(
Executor.class.getClassLoader(),
new Class[]{ExecutorInterface.class},
new TimeLogHandler(new Executor())
);
}

public void invoke() {
executor.execute(1, 2);
}
}

通过Proxy.newProxyInstance方法生成代理对象,然后执行代理对象对应的方法。

输出:

image-20241121172441782

最简单的使用

在主程序里最简单的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.pax.UniserializeProxy.Proxy_term;

import java.lang.reflect.Proxy;

public class Client {
public static void main(String[] args) {
ExecutorInterface executor = (ExecutorInterface) Proxy.newProxyInstance(
Executor.class.getClassLoader(),
new Class[]{ExecutorInterface.class},
new TimeLogHandler(new Executor())
);

executor.hello();
}
}
  • 代理对象:ExecutorInterface类型的executor
  • 被代理的对象(实际上是个类,不要被称呼误导):Executor
  • 被代理的行为以及对该行为的完全控制:executor.hello(),该方法实现:(对接口定义的所有方法都适用)

image-20241121174013429

进一步思考

如果此时Executor类新增方法,我们还能不能使用动态代理呢?

可以的,但是接口必须要有该方法,换言之,如果接口没有该方法,就不能用动态代理来代理该方法。

进一步想想,其实动态代理是面向接口的。如果一个类继承了该接口,那么我们就可以动态代理这个类,不管我们需要用几个方法(接口有定义这些方法),代理对象都可以直接使用。

小结

现在读者可以再想想动态代理是怎么解决这两个问题的

面向接口,是一个很完美的方法。因为接口可以定义好几个方法——被代理的行为;接口又可以被好几个类实现——这些类就是被代理的对象。

如此,就解决了这两个问题:

不仅有很多个执行者,而且每个执行者都需要执行很多个类似execute的方法这些方法不一定来自相同的类)。

0x04 与Java反序列化

  • 概述

无论是PHP反序列化还是Java反序列化,都需要一个触发条件。这里还是讨论Java反序列化:

反序列化readObject方法会自动执行,与之类似,在动态代理invoke方法会被自动执行。

  • 例子

入口类A有一个方法:

A[O] -> O.abc

我们希望传入B,使得B.abc可以getshell

A[B] -> B.abc{Runtime.getRuntime().exec("whoami")}

但是这很理想,一般来说见不到,这是比较常见的情况:O类是一个动态代理类O类的invoke方法可以调用O2类的f方法(该方法是危险方法):

O[O2] -> O2.f

那么不妨如下:

O[B] -> B.f{Runtime.getRuntime().exec("whoami")}

简单来说,就是利用动态代理时invoke方法自动触发这一机制,进一步延申链长以寻找反序列化的突破口。

  • 深入

有没有思考,为什么动态代理会触发invoke方法,触发谁的invoke方法?其实上面早已回答:

image-20241122162814169

进行动态代理时会调用动态代理类invoke方法。

  • 再深入

通过JDK动态代理机制在Java反序列化里的利用,我们可以学到什么呢?

在实际情况下,我们难以找到简单的链子,我们要去寻找在某些条件下可以自动进行一些操作的机制,进一步延申链长,寻找新的可能。

0x05 结语

写了有一段时间了,原来的那个教程讲不清楚,还好我及时看别的教程了——学习要理解原理,这样才能学得透。