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 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字段能否修改,取决于字段是直接赋值还是间接赋值。
- 这样的赋值方式不可修改
private final String name= "zhangsan";
public final int age = 10;
public final int age = 10+1;
- 通过构造函数赋值的可以修改
private final String name;
public Student(){
name = "zhangsan";
}
- new生成的对象可以修改
private final StringBuilder name = new StringBuilder("zhangsan");
- 表达式产生的可以修改
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序号