Java中的代理模式

Oyst3r 于 2024-04-25 发布

前言

Java中的动态代理是一种非常有用的机制,通常用于在不修改原始代码的情况下增强对象的功能,这点和CC1中的transform函数还蛮像的。而在反序列化漏洞这里,作用体现在“自动执行”这四个字,这其实和readObject是很像的,readObject 方法在反序列化当中会被自动执行、而 invoke 方法在动态代理中会被自动执行。从静态代理的概念出发,逐步过渡到动态代理。

静态代理

静态代理是一种在编译时就确定代理类的方式。在静态代理中,我们会手动创建一个代理类,这个代理类会持有目标对象,并在方法调用时将调用委托给目标对象。简单来说,静态代理就是在编译期就预先编写好的代理类。给个例子(摘自Drunkbaby师傅),给CRUD操作增加日志记录的功能,要求不能改变原有的代码。

先看原本功能的实现,也就是 UserService 的接口和实现类–>

public interface UserService {
    public void add();
    public void delete();
    public void update();
    public void query();
}

它原本的实现类–>

public class UserServiceImpl implements UserService{

    @Override
    public void add() {
        System.out.println("增加了一个用户");
    }

    @Override
    public void delete() {
        System.out.println("删除了一个用户");
    }

    @Override
    public void update() {
        System.out.println("更新了一个用户");
    }

    @Override
    public void query() {
        System.out.println("查询了一个用户");
    }
}

去写一个静态代理类,增加功能–>

public class UserServiceProxy implements UserService{

    private UserServiceImpl userService;

    public void setUserService(UserServiceImpl userService) {
        this.userService = userService;
    }

    public void add() {
        log("add");
        userService.add();
    }

    public void delete() {
        log("delete");
        userService.delete();
    }

    public void update() {
        log("update");
        userService.update();
    }

    public void query() {
        log("query");
        userService.query();
    }

    public void log(String msg){
        System.out.println("[Debug]使用了" + msg +"方法");
    }
}

修改Main类如下–>

public class Main {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
        UserServiceProxy proxy = new UserServiceProxy();
        proxy.setUserService(userService);
        proxy.add();
        proxy.delete();
        proxy.update();
        proxy.query();
    }
}

此时的运行结果如下–>

[Debug]使用了add方法
增加了一个用户
[Debug]使用了delete方法
删除了一个用户
[Debug]使用了update方法
更新了一个用户
[Debug]使用了query方法
查询了一个用户

代码逻辑很清晰,增加了记录日志的功能。但静态代理的最大问题在于原始类变了,代理类也要跟着变,很繁琐。

动态代理

针对于上述代码,改进的思路:写一个类,它的功能是可以把UserServiceImpl其中的方法名当作参数传入,先调用增加的功能,再调用UserServiceImpl中的方法。Java中将一个字符串参数当作方法名去调用,也只能反射了,所以这个类肯定是要用到反射的技术。但这么写的话在Main方法中将会是对象(方法名)这样的形式,但其实需要的是对象.方法名的形式,这时Java中的java.lang.reflect.Proxy类就实现了这样的功能。接下来看一下改进后的代码–>

首先写一个调用处理器–>

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

public class UserServiceInvocationHandler implements InvocationHandler {
    private UserService target;

    public UserServiceInvocationHandler(UserService target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log(method.getName());
        Object result = method.invoke(target, args);
        return result;
    }

    public void log(String msg){
        System.out.println("[Debug]使用了" + msg +"方法");
    }
}

然后就是写一个动态代理–>

import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        UserServiceInvocationHandler handler = new UserServiceInvocationHandler(userService);
        UserService proxy = (UserService) Proxy.newProxyInstance(
                userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(),
                handler);
        proxy.add();
        proxy.delete();
        proxy.update();
        proxy.query();
    }
}

运行结果如下–>

[Debug]使用了add方法
增加了一个用户
[Debug]使用了delete方法
删除了一个用户
[Debug]使用了update方法
更新了一个用户
[Debug]使用了query方法
查询了一个用户

可以看到,动态代理的代码量大大减少,并且在Main方法中可以直接使用对象.方法名的形式,很方便。其中注意的一点是,UserServiceInvocationHandler类只是实现了一个调用处理器,而Proxy.newProxyInstance()方法才是动态代理的精髓所在。

漏洞利用

在下图位置下断点

在调试过程中便可发现它们下一步都会不约而同的走到调用处理器类的invoke方法中,在例子中也就是UserServiceInvocationHandler类的invoke方法中。如下图–>

回到开头–>作用还是体现在“自动执行”这四个字,和readObject是很像的,readObject 方法在反序列化当中会被自动执行、而 invoke 方法在动态代理中会被自动执行。利用这个特性去举个例子–>

1. 有一个sink是ExpClass.exp()
2. 有一个入口类EntryClass,它的readObject方法中有parm.fake()这样的代码,且parm接受一个对象且是可控的

那么如果这个EntryClass的readObject方法里面是parm.exp(),那就直接成了,和URLDNS链类似。但事实是parm.fake(),但这里还有种可能是存在一个动态代理类ProxyClass,它的调用处理器中的invoke方法中有service.exp(),且service是可控的,那么就可以将service写成ExpClass的实例,然后再把ProxyClass的实例传给parm,这样不管parm后面是什么方法都会执行invoke中已经构造好的恶意方法,最后序列化EntryClass实例,这样就成功生成了一个恶意Payload。

很巧妙的思路,在构造gadget时候又多了一种选择。