Turker
发布于 2025-07-06 / 13 阅读
0
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) 根据字段名获取对应的字段,可以获取 publicprotectedprivate类型的字段,不能获取父类的字段。
getDeclaredFields() 获取类所有的字段,包括 publicprotectedprivate。不能获取父类的字段。
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) 根据方法名获取对应的方法,可以获取 publicprotectedprivate类型的方法,不能获取父类的方法。
getDeclaredMethods() 获取类所有的方法,包括 publicprotectedprivate。不能获取父类的方法。
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) 根据参数类型获取对应的构造函数,可以获取 publicprotectedprivate类型的构造函数,不能获取父类的构造函数。
getDeclaredConstructors() 获取类所有的构造函数,包括 publicprotectedprivate。不能获取父类的构造函数。
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的能力,而是通过快餐店代理加上了对卖汉堡过程 preSellpostSell的监控。
也就是说,静态代理可以在不修改被代理的对象的情况下拓展功能。但是它必须给每个接口实现代理类,且一旦接口增加一个方法,所有的目标对象和代理对象都要维护。

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序号


评论