Turker
Turker
Published on 2025-07-06 / 59 Visits
0

Java安全——反射&动态代理

Java安全——反射&动态代理

#Java

一、反射

1.1 class对象

在了解具体反射机制之前,首先需要了解Class类(全限定名称是java.lang.Class)和class对象。class对象就是由Class类创建的对象,只是这个创建过程不是由用户主动创建的,而是由JVM自动实现的。JVM会为项目中每一个类(不论是JDK自带的类、第三方jar包的类、还是自己编写的类)创建一个class对象,用来保存与这个类相关的信息。class对象只与类有关,与对象无关。

Student.class   //通过类名.class的方式获取class对象
stu1.getClass()   //通过类对象.getClass()的方式获取class对象
Class.forName("com.test.Student")  //通过全限定名的方式获取class对象

这三种获取class对象的方式都要掌握,遇到不同的应用场景能熟练的使用某一种方式获取到类的class对象。java反射的所有操作都是基于class对象来完成的。

传统生成一个类对象和通过反射来生成类对象,从本质上来看最终创建的对象是一样的,但是创建的方法是完全不同的。通过new关键字来生成对象,调用对象方法是传统的方法;通过class对象来生成对象,调用方法是属于反射机制。看起来似乎反射的调用比传统的方式更加复杂,但是反射的主要作用之一就是在动态上面,我们调用的类名称和方法名称都是通过变量传入的,在某些复杂逻辑下,动态的类名和函数名是极为重要的。

另外,反射机制还有一个极为重要的特征就是可以调用类的私有方法(包括protected和private)。如下图所示,这种看似违背继承机制的特性却是反射里面极为重要的概念,也是后续很多java反序列化利用链依赖反射机制的重要原因。

到此,我们就先简单总结一下反射的优势:

  1. 反射是对动态类和方法的调用
  2. 反射可以调用和修改类的私有属性和方法

1.2 Java反射

Java反射机制一般有下面三种用途:字段获取和修改、方法获取和访问、构造函数获取和使用。
我们先创建一个示例类Student:

class School{  
    private String name = "BurgerKing High School";  
}  
  
class Student extends School {  
    public String name;  
    private int age;  
    private static final String schoolName = new StringBuilder("BurgerKing High School").toString();  
    public Student(){  
        this.name = "Burger King";  
        this.age = 20;  
    }  
    private void sayHello(String message1 ,String message2){  
        System.out.println(message1 + name + " aged " + age + " at " + schoolName + message2);  
    }  
}

1.2.1 字段获取和修改

方法 功能
getField(String name) 根据字段名获取对应的字段,只能获取 public类型的字段,可以获取父类的字段。
getFields() 获取类所有的字段,只能获取 public类型的字段,可以获取父类的字段。
getDeclaredField(String name) 根据字段名获取对应的字段,可以获取 public、protected和 private类型的字段,不能获取父类的字段。
getDeclaredFields() 获取类所有的字段,包括 public、protected和 private。不能获取父类的字段。
field.get(Obj) 获取 Obj对象的 field字段
field.set(Obj,field_value) 设置 Obj对象的 field字段值为 field_value

Java的反射机制提供了获取属性的方式,同时支持对私有的属性进行获取。一种最典型的获取属性的方式如下:

public class Main {  
    public static void main(String[] args) throws Exception {  
        // Create an instance of Student  
        Student student = new Student();  
  
        Class clazz = student.getClass();  
  
        // Access and print the public field 'name'  
        System.out.println("Name: " + student.name);  
  
        // Access and print the private field 'age'  
        Field ageField = clazz.getDeclaredField("age");  
        ageField.setAccessible(true); // Bypass private access  
        System.out.println("Age: " + ageField.getInt(student));  
  
        // Access and print the private field 'name' from the superclass  
        Field superNameField = clazz.getSuperclass().getDeclaredField("name");  
        superNameField.setAccessible(true); // Bypass private access  
        System.out.println("Super Name: " + superNameField.get(student));  
    }  
}
Name: Burger King
Age: 20
Super Name: BurgerKing High School

在进行私有属性获取时,有两个注意事项。第一,必须使用 getDeclaredField(s)函数来获取。第二,在对字段进行操作之前,必须 field.setAccessible(true)这个表示设置允许对字段进行操作。同时可以通过 clazz.getSuperclass()拿到父类的class对象。进而访问其私有属性。

在获取类的属性字段时,有一个需要特殊注意的类型就是final修饰符。使用传统的方式对final修饰符的字段进行修改,代码运行不会报错,但是实际上却是进行的伪修改,真正的字段值并没有被修改。那有什么办法可以修改final属性的值吗?final字段能否修改,取决于字段是直接赋值还是间接赋值。

  1. 这样的赋值方式不可修改
private final String name= "zhangsan";
public final int age = 10;
public final int age = 10+1;
  1. 通过构造函数赋值的可以修改
    private final String name;
    public Student(){
        name = "zhangsan";
    }
  1. new生成的对象可以修改
private final StringBuilder name = new StringBuilder("zhangsan");
  1. 表达式产生的可以修改
private final String name = (null!=null?"zhangsan":"zhangsan");

那么如果一个变量又有 final又有 static怎么办?当然是直接把final修饰符干掉!(JDK8成功,JDK11WARNING,之后的都不行)

private static final String schoolName = new StringBuilder("BurgerKing High School").toString();
Field schoolField = clazz.getDeclaredField("schoolName");  
Field modifiersField = schoolField.getClass().getDeclaredField("modifiers");  
modifiersField.setAccessible(true);  
modifiersField.setInt(schoolField, schoolField.getModifiers() & ~Modifier.FINAL); // Remove final modifier  
schoolField.setAccessible(true); // Bypass private access  
schoolField.set(null, "New BurgerKing High School"); // Change static final field value  
System.out.println("Updated School Name: " + schoolField.get(null));
Updated School Name: New BurgerKing High School

1.2.2 方法获取和访问

方法 功能
getMethod(String name, Class... parameterTypes) 根据方法名和参数类型获取对应的方法,只能获取 public类型的方法,可以获取父类的方法。
getMethods() 获取类所有的方法,只能获取 public类型的字段,可以获取父类的方法。
getDeclaredMethod(String name, Class... parameterTypes) 根据方法名获取对应的方法,可以获取 public、protected和 private类型的方法,不能获取父类的方法。
getDeclaredMethods() 获取类所有的方法,包括 public、protected和 private。不能获取父类的方法。
invoke(Object obj, Object[] args) 执行通过反射获取的方法,obj代表要执行方法的对象,args代表方法的参数。

这里需要强调的是getMethod方法的第二个参数,代表的是想要获取方法的参数类型。第二个参数需要传入的是参数对应的class对象,多个参数以数组的形式传入。而要使用通过反射获取的方法,需要使用invoke函数。

// Access and invoke the private method 'sayHello'  
Method sayHelloMethod = clazz.getDeclaredMethod("sayHello", new Class<?>[] {String.class, String.class});  
sayHelloMethod.setAccessible(true); // Bypass private access  
sayHelloMethod.invoke(student,new Object[]{"Hello, ", ". Nice day!"});
Hello, Burger King aged 20 at New BurgerKing High School. Nice day!

获取父类的私有方法就不再赘述了,与获取父类的私有字段的方式一样

1.2.3 构造函数获取和使用

构造函数理论上也是一种方法,只是这是一种特殊的方法。我们很多情况下都需要通过反射获取构造函数,然后调用构造函数生成类的实例。可能某些小伙伴会问,生成类的实例不是直接new就可以了吗,为啥还要用反射这么复杂的调用方式来生成实例?答案是很多情况下,我们遇到的构造函数是private的或者是protected的,又或是我们本身想生成实例的类就是private的,这样我们就不能直接用new来生成实例了,只能通过反射来生成。在获取到构造函数之后,需要通过newInstance函数来生成类对象。

方法 功能
getConstructor(Class... parameterTypes) 根据参数类型获取对应的构造函数,只能获取 public类型的构造函数,不能获取父类的构造函数。
getConstructors() 获取类所有的构造函数,只能获取 public类型的字段,不能获取父类的构造函数。
getDeclaredConstructor (Class... parameterTypes) 根据参数类型获取对应的构造函数,可以获取 public、protected和 private类型的构造函数,不能获取父类的构造函数。
getDeclaredConstructors() 获取类所有的构造函数,包括 public、protected和 private。不能获取父类的构造函数。
newInstance(Object ... initargs) newInstance函数接受可变的参数个数,构造函数实际有几个传输,这里就传递几个参数值。newInstance返回的数据类型是 Object。
private Student(String name,int age) {  
    this.name = name;  
    this.age = age;  
}
Class clazz = Student.class;  
Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);  
constructor.setAccessible(true); // Bypass private constructor access  
Student student = (Student) constructor.newInstance("Burger King", 20);

由于 getDeclaredConstructor函数和 newInstance函数接受的是可变长度的参数,每一个参数对应的就是构造函数中的参数。这里需要和获取普通方法时传递数组参数进行区别。

1.3. Java反射的简单应用

Java反射作为Java安全学习过程中最重要的概念,后面很多关于Java漏洞编写poc或者exp都要用到Java反射。但是因为我们现在还没有说到反序列化,就暂时不说反射在反序列化利用链中的作用。这里只说反射的一个简单应用,通过反射来隐藏webshell关键字,绕过WAF检测。

最简单的一个java执行命令的方式如下

Runtime.getRuntime().exec("calc.exe");

很多WAF在检测webshell的时候会基于关键字Runtime、getRuntime、exec这三个关键字来进行规则匹配。下面我们就通过反射来隐藏着三个关键字,如下图所示。我们把着三个关键字都写成了字符串变量,稍微做一下字符串拼接也就没有这三个关键字了。(这里为了方便大家阅读就不替换了)

public class Shell {  
    public static void main(String[] args) throws Exception{  
        Class<?> clazz = Class.forName("java.lang.Runtime");  
        Method method1 = clazz.getDeclaredMethod("getRuntime");  
        Runtime runtimeObj = (Runtime) method1.invoke(null); // Invoke static method getRuntime  
        Method method2 = clazz.getDeclaredMethod("exec", String.class);  
        method2.setAccessible(true); // Bypass private access  
        String command = "calc.exe"; // Command to execute  
        Process process = (Process) method2.invoke(runtimeObj, command); // Execute the command  
    }  
}

二、代理

2.1 Java代理简介

Java 代理是一种强大的技术,它允许开发者在 Java 应用程序运行时(Runtime)动态地修改类的字节码,从而改变或增强应用程序的行为,而无需修改其源代码,它可以挂载到任何正在运行的 Java 程序上,实现以下功能:

  • 性能监控: 像 SkyWalking、New Relic 这类工具,就是通过 Java 代理来测量方法调用时长、追踪请求链路等。
  • 日志记录: 动态地为某些方法增加详细的日志输出。
  • 代码分析: 收集类加载信息、对象创建信息等。
  • 热部署: 在不重启服务的情况下,替换已加载的类。
    为方便理解,这里以购买汉堡为例。

2.2 静态代理

首先我们需要定义一个汉堡接口:

public interface Burgers {  
    void sellBurger();  
}

接下来定义一个真正的汉堡类:

public class RealBurger implements Burgers{  
    @Override  
    public void sellBurger() {  
        System.out.println("Selling a real burger!");  
    }  
}

再定义一个快餐店代理类:

public class FastFoodRestaurant implements Burgers{  
    RealBurger realBurger;  
  
    public FastFoodRestaurant(RealBurger realBurger) {  
        this.realBurger = realBurger;  
    }  
  
    @Override  
    public void sellBurger() {  
        preSell();  
        realBurger.sellBurger();  
        postSell();  
    }  
  
    public void preSell() {  
        System.out.println("Preparing to sell a burger...");  
    }  
    public void postSell() {  
        System.out.println("Burger sold successfully!");  
    }  
}

卖个汉堡试试:

public class Main{  
    public static void main(String[] args) {  
        FastFoodRestaurant restaurant = new FastFoodRestaurant(new RealBurger());  
        restaurant.sellBurger();  
    }  
}

得到:

Preparing to sell a burger...
Selling a real burger!
Burger sold successfully!

也就是说,我们并没有改变 RealBurger的能力,而是通过快餐店代理加上了对卖汉堡过程 preSell和 postSell的监控。
也就是说,静态代理可以在不修改被代理的对象的情况下拓展功能。但是它必须给每个接口实现代理类,且一旦接口增加一个方法,所有的目标对象和代理对象都要维护。

2.3 动态代理

动态代理也是代理,他和静态代理的功能和目的是没有区别的,唯一的区别就在于动态代理是动态生成的,省去为接口实现代理类的操作。JDK实现代理只需要使用 newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:

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

注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:

ClassLoader loader:指定当前目标对象使用类加载器,获取加载器的方法是固定的
Class<?>[] interfaces:目标对象实现的接口的类型,使用泛型方式确认类型
InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入

2.3.1 动态生成

如果卖一个牛肉汉堡:

public class BeefBurger implements Burgers {  
    @Override  
    public void sellBurger() {  
        System.out.println("Selling a beef burger!");  
    }  
}

餐馆:

import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
  
public class DynamicRestaurant implements InvocationHandler {  
    Object burgers;  
  
    public DynamicRestaurant(Object burgers) {  
        this.burgers = burgers;  
    }  
  
    public void setBurgers(Object burgers) {  
        this.burgers = burgers;  
    }  
  
    @Override  
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
        preSell();  
        Object result = method.invoke(burgers, args);  
        postSell();  
        return result;  
    }  
  
    public void preSell() {  
        System.out.println("Preparing to sell a burger...");  
    }  
  
    public void postSell() {  
        System.out.println("Burger sold successfully!");  
    }  
}

卖一个看看:

public class Main{  
    public static void main(String[] args) {  
        BeefBurger beefBurger = new BeefBurger();  
        DynamicRestaurant restaurant = new DynamicRestaurant(beefBurger);  
        Burgers burgerProxy = (Burgers) Proxy.newProxyInstance(  
                BeefBurger.class.getClassLoader(),  
                BeefBurger.class.getInterfaces(),  
                restaurant);  
        burgerProxy.sellBurger();  
    }  
}

输出:

Preparing to sell a burger...
Selling a beef burger!
Burger sold successfully!

动态代理的好处就在于,不需要重新实现一个代理类,而是通过实现 InvocationHandler 接口的 invoke方法来形成代理,通过 Proxy.newProxyInstance()创建了一个代理类来执行 sellBurger方法。如果我们要卖一个猪肉汉堡呢?

public class PorkBurger implements Burgers{  
    @Override  
    public void sellBurger() {  
        System.out.println("Selling a pork burger!");  
    }  
}

很显然我们不需要再实现别的代理类了,只需要这样:

public class Main{  
    public static void main(String[] args) {  
        BeefBurger beefBurger = new BeefBurger();  
        DynamicRestaurant beefrestaurant = new DynamicRestaurant(beefBurger);  
        Burgers burgerProxy = (Burgers) Proxy.newProxyInstance(  
                BeefBurger.class.getClassLoader(),  
                BeefBurger.class.getInterfaces(),  
                beefrestaurant);  
        burgerProxy.sellBurger();  
  
        PorkBurger porkBurger = new PorkBurger();  
        DynamicRestaurant porkRestaurant = new DynamicRestaurant(porkBurger);  
        burgerProxy = (Burgers) Proxy.newProxyInstance(  
                PorkBurger.class.getClassLoader(),  
                PorkBurger.class.getInterfaces(),  
                porkRestaurant);  
        burgerProxy.sellBurger();  
  
    }  
}

结果为:

Preparing to sell a burger...
Selling a beef burger!
Burger sold successfully!
Preparing to sell a burger...
Selling a pork burger!
Burger sold successfully!

可以看出来,我们不需要创建很多代理类就可以实现不同汉堡的卖出。

2.3.2 探究动态代理

使用静态代理的时候,我们通过 new FastFoodRestaurant(new RealBurger())创建了代理实例。那么以此类推,动态代理肯定也要创建一个实例,我们可以跟进 Proxy.newProxyInstance()来一探究竟。
java.lang.reflect.Proxy#newProxyInstance:

public static Object newProxyInstance(ClassLoader loader,  
                                      Class<?>[] interfaces,  
                                      InvocationHandler h)  
    throws IllegalArgumentException  
{  
    Objects.requireNonNull(h);  
  
    final Class<?>[] intfs = interfaces.clone();  
    final SecurityManager sm = System.getSecurityManager();  
    if (sm != null) {  
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);  
    }  
  
    /*  
     * Look up or generate the designated proxy class.     */    Class<?> cl = getProxyClass0(loader, intfs);  
  
    /*  
     * Invoke its constructor with the designated invocation handler.     */    try {  
        if (sm != null) {  
            checkNewProxyPermission(Reflection.getCallerClass(), cl);  
        }  
  
        final Constructor<?> cons = cl.getConstructor(constructorParams);  
        final InvocationHandler ih = h;  
        if (!Modifier.isPublic(cl.getModifiers())) {  
            AccessController.doPrivileged(new PrivilegedAction<Void>() {  
                public Void run() {  
                    cons.setAccessible(true);  
                    return null;  
                }  
            });  
        }  
        return cons.newInstance(new Object[]{h});  
    } catch (IllegalAccessException|InstantiationException e) {  
        throw new InternalError(e.toString(), e);  
    } catch (InvocationTargetException e) {  
        Throwable t = e.getCause();  
        if (t instanceof RuntimeException) {  
            throw (RuntimeException) t;  
        } else {  
            throw new InternalError(t.toString(), t);  
        }  
    } catch (NoSuchMethodException e) {  
        throw new InternalError(e.toString(), e);  
    }  
}

可以看到创建实例的逻辑:
java.lang.reflect.Proxy#getProxyClass0:

private static Class<?> getProxyClass0(ClassLoader loader,  
                                       Class<?>... interfaces) {  
    if (interfaces.length > 65535) {  
        throw new IllegalArgumentException("interface limit exceeded");  
    }  
  
    // If the proxy class defined by the given loader implementing  
    // the given interfaces exists, this will simply return the cached copy;    // otherwise, it will create the proxy class via the ProxyClassFactory   
    return proxyClassCache.get(loader, interfaces);  
}

会通过缓存获取,如果没有的话再通过 ProxyClassFactory 生成。

private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // prefix for all proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";

    // next number to use for generation of unique proxy class names
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        for (Class<?> intf : interfaces) {
            /*
                 * Verify that the class loader resolves the name of this
                 * interface to the same Class object.
                 */
            Class<?> interfaceClass = null;
            try {
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }
            /*
                 * Verify that the Class object actually represents an
                 * interface.
                 */
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            /*
                 * Verify that this interface is not a duplicate.
                 */
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        String proxyPkg = null;     // package to define proxy class in
        int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

        /*
             * Record the package of a non-public proxy interface so that the
             * proxy class will be defined in the same package.  Verify that
             * all non-public proxy interfaces are in the same package.
             */
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                accessFlags = Modifier.FINAL;
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // if no non-public proxy interfaces, use com.sun.proxy package
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        /*
             * Choose a name for the proxy class to generate.
             */
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        /*
             * Generate the specified proxy class.
             */
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
        try {
            return defineClass0(loader, proxyName,
                                proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
            throw new IllegalArgumentException(e.toString());
        }
    }
}

可知代理类名为 String proxyName = proxyPkg + proxyClassNamePrefix + num,即包名+$Proxy+id序号