『Java内存马』RASP检测Java内存马
RASP基础知识
-
概念
RASP(Runtime application self-protection,运行时程序自动保护技术)是一种新型应用安全保护技术,它将保护程序像疫苗一样注入到应用程序中,内置或链接到应用程序环境中与应用程序融为一体,能实时检测和阻断安全攻击,使应用程序具备自我保护能力。
-
WAF和RASP的区别:
- WAF是位于Web应用和客户端之间的安全网关,用于检测和防御来自网络层和应用层的攻击。且WAF大部分是基于规则去拦截,你的请求参数在规则中存在即拦截,误报率高。
- RASP是一种嵌入在Web应用程序中的安全技术,在应用的运行时环境内工作,利用应用上下文信息来检测并防御攻击行为,工作在应用层。
以防御SQL注入为例:
- WAF:预定义规则库,匹配HTTP请求数据包参数字段中是否存在SQL注入的特征模式。可能被绕过措施绕过、误报率高
- RASP:动态监控应用的实际运行行为。在应用程序将sql语句预编译完视图访问数据库资源时,RASP可以在其发送之前将其拦截进行检测,如果sql语句没有危险操作,则正常放行,不会影响程序本身的功能。
-
部署方法
RASP工作在应用层,因此每个产品都需要以某种方式与应用程序集成。监控应用程序使用(调用)或执行(运行时)的方法有多种,每种方法的部署略有不同,收集的应用程序运行方式略有不同。
-
Servlet 过滤器和插件:一些RASP平台作为Web服务器插件或Java Servlet实现,通常安装到Apache Tomcat或 Microsoft .NET中以处理入站HTTP请求。插件在到达应用程序代码之前过滤请求,将检测规则应用于收到的每个入站请求。
这种部署方式和WAF比较类似。WAF部署位置为应用程序之外,作为代理处理流量;RASP的这种部署方式位于应用入口,可以了解 Web 应用的基本结构(例如 URL 路由、参数类型等),但对更深层的运行逻辑仍然无法完全感知。
但是相比WAF的检测深度更高,WAF只能根据数据包模式或其他特征进行检测,但 这种rasp可以利用服务器端环境 来进一步理解请求的上下文,比如请求头、请求体、甚至可能的用户会话信息(即可以接触到明文信息)。
-
库/JVM替换:通过替换标准应用程序库、JAR文件甚至Java虚拟机来安装某些RASP产品。在此模型下,RASP工具可以全面了解应用程序代码路径和系统调用,甚至可以学习状态机或序列行为。可以更深入的分析程序上下文,允许更细粒度的检测规则。
- 检测规则可以基于更加丰富的上下文信息,如调用栈、数据流等。
-
虚拟化或复制:创建应用程序的副本,通常为虚拟化容器、流量镜像、云实例等。在副本中隔离运行并检测应用程序行为,并阻止恶意或格式错误的请求。
- 异步处理或并行分析,加快响应速度
- 主环境配置基本防护策略(如 WAF),减少发送到副本的无意义流量,避免资源浪费
-
-
优势
- RASP具有更高的准确性,因为它可以洞察应用程序逻辑和配置、事件和数据流、执行的指令和数据处理。而且在预生产中很容易部署,能成功阻止攻击。
- RASP具有动态分析和上下文感知能力,可以更好地防御针对业务逻辑的攻击,包括高级攻击和零日漏洞等。
- 通过嵌入应用实现保护,适合云原生和微服务架构,便于集成。
-
国内外产品:浅谈RASP技术攻防之基础篇
- 百度OpenRasp:rasp.baidu.com
- 蚂蚁RASP:应用防护RASP介绍_云安全中心(Security Center)-阿里云帮助中心
- 云鲨RASP:悬镜安全 - 代码疫苗内核驱动的新一代应用威胁自免疫平台
- Micro Focus:OpenText Cybersecurity Cloud solutions
-
资源
-
检测内存马
- Attach 检测jar包到JVM进程
- 获取JVM中已经加载的class列表
- 根据以上可疑特征将可疑的class反编译为Java源码
- 根据源码检测Webshell
Java内存马
基础知识
-
概念
Java内存马(Java Memory Shell)是一种攻击手段,通常利用java的动态加载机制,在Java应用程序的内存中动态生成或加载恶意的Java对象或类,确保恶意代码在java应用内存中主流、执行,从而在不修改服务器文件系统的情况下保持对目标系统的持久访问,同时避免传统文件检测工具的发现。
-
例子
攻击者通过反射或内存操作(如
setAttribute()
)将恶意Servlet注入到当前Web应用的ServletContext
中,并动态映射到指定的URL路径(如/memory
),而无需在服务器的WEB-INF/classes
或WEB-INF/lib
目录中添加任何文件。例如:
-
动态添加组件:通过Java代码或脚本直接向Web服务器中添加一个Servlet、Filter或Listener。由于这些组件是在内存中注册的,不会涉及磁盘操作。
-
类加载器挂钩:通过修改或替换ClassLoader,使其在加载指定类时附加恶意功能。此方法也不会创建新的文件,只是在内存中操作类对象。
由于内存马不需要在磁盘上留下任何恶意文件,只要它被加载到内存中运行,就可以在不触发文件系统检测的情况下持久驻留在目标系统中。这种特性使得内存马具有高度的隐蔽性。
-
-
使用场景(优势)
- 由于网络原因不能反弹 shell 的;
- 内部主机通过反向代理暴露 Web 端口的;
- 服务器上有防篡改、目录监控等防御措施,禁止文件写入的;
- 服务器上有其他监控手段,写马后会告警监控,人工响应的;
- 服务使用 Springboot 等框架,无法解析传统 Webshell 的;
-
缺点
- 服务重启后会失效;
- 对于传统内存马,存在的位置相对固定,已经有相关的查杀技术可以检出。
java内存马的分类
-
传统web应用型内存马
使用基本Servlet-API实现的动态注册内存马,此种类型的内存马最经典,已经被扩展至适应各个中间件。
-
Servlet型内存马:将恶意的
Servlet
对象动态注册到Web容器(如Tomcat、Jetty、JBoss等)的内存中。恶意Servlet一旦注册,攻击者可以通过特定URL直接访问它,从而执行进一步的攻击操作。 -
Filter型内存马:通过动态添加过滤器(Filter)的方式,将恶意的Filter注入到应用中。这种内存马会对所有HTTP请求和响应进行拦截和处理,可以用于窃取、篡改或注入恶意数据。
-
Listener型内存马:动态注册恶意的事件监听器(Listener),监听应用中各类事件(如Session创建、销毁事件等),从而在特定事件发生时执行恶意代码。
JavaWeb应用会将Servlet、Filter、Listener及其映射放在Context中,并在程序运行时进行查找和匹配。注入后,这些组件被挂载到Web应用的Context中,从而允许攻击者在不修改磁盘文件的情况下,通过特定的URL或请求路径触发恶意代码的执行。通过内存马的动态注册,这些恶意代码能够绕过文件系统的本地存储,不会在配置文件中留下任何可见的恶意设置,从而具有很强的隐蔽性和持久性。
-
-
框架型内存马
框架性内存马是利用Web框架(如Spring、Struts、Shiro等)的特性进行持久化攻击的手段,其主要目的是利用框架的动态注册机制,将恶意代码注入到内存中,进行隐蔽的持久化控制。
例如:Spring框架内存马通过动态注册Spring的核心组件(如Controller、Filter、Interceptor、WebFilter等),将恶意代码注入到应用的上下文中。
-
中间件型内存马
中间件通常以流式和管道式方式处理请求,每个组件在处理完请求后会将其传递给下一个组件。这种设计模式为攻击者提供了多个插入点,攻击者可以在请求链的任意位置动态注入恶意组件。
在中间件的很多功能实现上,因为采用了类似 Filter-FilterChain 的职责链模式,可以被用来做内存马,由于行业对 Tomcat 的研究较多,因此大多数的技术实现和探究是针对 Tomcat 的,但其他中间件也有相当多的探究空间。
-
Agent 型内存马
通过Java的
Java Agent
机制来注入恶意代码的一种内存马形式。它利用JVM的Instrumentation API
来实现对应用程序类的修改、增强或替换,从而达到持久化控制和隐蔽攻击的目的。这种内存马在JVM启动时加载,并在整个JVM生命周期内驻留,是一种极具隐蔽性和持久性的内存马攻击手段。 -
其他内存马
还有一些其他非常规的利用思路,可以用在内存马的实现上,例如 WebSocket 协议等。
但实际上,内存马的深度和广度完全不局限于此,还有很多思路可以用来进行内存马的扩展:
- 对于 Agent 型内存马,可以 hook 非常多的位置,如各种 SPI 的实现,可以接管整个 JVM,获取数据;
- 除了基于 Web 协议的内存马,可以使用各种协议作为内存马的通信途径,如 grpc、jmx、jpda 等,或封装多层协议;
- 对于各种中间件/框架,利用其设计模式,可挖掘出多种内存马的利用方式。
除了按照内存马的实现方式分类,还可以按照内存马的利用方式分为:冰蝎马、哥斯拉马、蚁剑马、命令回显马、流量隧道马等等。
Java Agent型内存马
Java Agent
-
概念
Java Agent是JVM提供的一种机制,用于在Java应用程序启动时或运行时动态地修改、监控和管理类的字节码。它主要通过Java的Instrumentation API来实现,能在不停止应用程序的情况下动态修改已加载或者未加载的类,一般用于实现性能监控、调试、AOP(面向切面编程)、字节码增强等功能。
-
使用方式
Java Agent的使用方式有两种:
-
premain,在JVM启动前加载
启动时配置
-javaagent
参数,会执行Agent中的premain方法。JVM启动时 会先执行premain
方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等来改写实现类。 -
agentmain,在JVM启动后加载
首先使用Attach API将java agent动态附加到运行中的目标JVM上(建立连接),然后使用com.sun.tools.attach.VirtualMachine包提供的loadAgent方法,将指定的 Java Agent JAR 文件加载到目标 JVM 中,从而启动该 JAR 文件中定义的
agentmain
方法,调用Instrumentation API
对目标 JVM 进行字节码修改等操作。
-
premain 方法
JVM启动时会先执行premain
方法,即:在 main 方法启动前通过premain方法拦截大部分类的加载活动(用户类的加载肯定会被拦截,但是很多系统类先于agent执行,并不会被拦截),后续就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等来改写实现类。
启动时配置 -javaagent
参数,会执行Agent中的premain方法。
agentmain方法
例子:
-
编写
hello.jar
文件结构:
1
2
3
4
5
6
7
8
9hello_project/
├── src
│ └── com
│ └── test
│ ├── HelloWorld.java
│ ├── hello.java
│ ├── GetPid.java
│ └── MANIFEST.MFHelloWorld.java
:主程序,用于启动并输出进程ID,等待用户输入。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package com.test;
import java.util.Scanner;
public class HelloWorld {
public static void main(String[] args) {
hello h1 = new hello();
GetPid pid = new GetPid();
h1.hello();
pid.GetPid(); // 打印当前的进程ID
Scanner sc = new Scanner(System.in);
sc.nextInt(); // 暂停等待输入 便于后续注入
hello h2 = new hello();
h2.hello();
System.out.println("ends");
}
}hello.java
:简单的类,用于输出“hello world”。1
2
3
4
5
6
7package com.test;
public class hello {
public void hello() {
System.out.println("hello world");
}
}GetPid.java
:获取当前JVM进程的名称和PID。1
2
3
4
5
6
7
8
9
10
11
12package com.test;
import java.lang.management.ManagementFactory;
public class GetPid {
public void GetPid() {
String name = ManagementFactory.getRuntimeMXBean().getName();
System.out.println("JVM:" + name);
String pid = name.split("@")[0];
System.out.println("PID:" + pid);
}
}打包
hello.jar
:在项目根目录下输入下面的命令,编译上面的三个java文件为
.class
并输出到out文件下1
javac -d out src/com/test/*.java
在test目录文件夹下创建
MANIFEST.MF
文件,用于指明premain
的入口:1
2
3Manifest-Version: 1.0
Main-Class: com.test.HelloWorld注:
MANIFEST.MF
最后一行是空行,不能省略,否则会报错根目录下将编译后的
.class
文件和MF文件打包成hello.jar
包1
jar cvfm hello.jar src/com/test/MANIFEST.MF -C out .
-
编写
agent.jar
代码创建一个Java Agent,动态注入到目标JVM中,修改特定类的方法和行为。
文件结构:
1
2
3
4
5
6
7
8hello_project/
├── src
│ └── com
│ └── agent
│ ├── AgentDemo.java
│ ├── TransformerDemo.java
│ └── MANIFEST.MF**
TransformerDemo.java
**:调用Instrumentation API 提供的ClassFileTransformer
接口,修改目标类中hello方法的字节码。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
32package com.agent;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class TransformerDemo implements ClassFileTransformer {
public static final String editClassName = "com.test.hello";
public static final String editMethodName = "hello";
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!className.equals(editClassName.replace('.', '/'))) return classfileBuffer;
// 调用Javassist库来转换类
try {
ClassPool cp = ClassPool.getDefault();
cp.appendClassPath(new LoaderClassPath(loader));
CtClass ctc = cp.get(editClassName);
// 获取目标类的hello方法
CtMethod method = ctc.getDeclaredMethod(editMethodName);
// 修改方法体,将方法体的内容改为仅输出 "java memory shell"
method.setBody("{System.out.println(\"java memory shell\");}");
// 生成新的字节码,并返回此字节码作为转换后的字节码。
byte[] byteCode = ctc.toBytecode();
ctc.detach();
return byteCode;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}**
AgentDemo.java
**:监控内存中的加载类,检测到目标类后调用 transform1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package com.agent;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
// 获取所有已加载的类
Class[] classes = inst.getAllLoadedClasses();
// 遍历所有已加载的类
for (Class aClass : classes) {
// 判断是否已加载指定的目标类TransformerDemo.editClassName
if (aClass.getName().equals(TransformerDemo.editClassName)) {
System.out.println("EditClassName:" + aClass.getName());
// 注入 Transformer
inst.addTransformer(new TransformerDemo(), true);
inst.retransformClasses(aClass);
}
}
}
}打包
agent.jar
同样地在agent文件夹下创建
MANIFEST.MF
:指定 Agent 类1
2
3
4
5Manifest-Version: 1.0
Agent-Class: com.agent.AgentDemo
Can-Retransform-Classes: true
Can-Redefine-Classes: true因为这里需要使用
javassist.jar
包进行字节码的改写,可以使用maven下载,将下面的依赖添加到pom.xml
文件中并更新:1
2
3
4
5
6
7<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version> <!-- 使用最新的版本 -->
</dependency>
</dependencies>找到repository下的
javassist.jar
包并复制到项目根目录下项目根目录下执行命令编译打包
1
2
3
4
5
6
7
8
9
10
11
12
13# 1.编译源文件
javac -d out -cp ./javassist-3.28.0-GA.jar src/com/agent/*.java
# 如果出现编码错误,加上-encoding UTF-8进行编码
javac -encoding UTF-8 -d out -cp ./javassist-3.28.0-GA.jar src/com/agent/*.java
# 2. 将 javassist 库的内容解压缩
mkdir temp-javassist
cd temp-javassist
jar xf ../javassist-3.28.0-GA.jar
# 将 javassist 和编译后的类文件一起打包,或者直接把javassist文件夹放到out目录下(z去掉后面的-C javassist .)
cd ..
jar cvfm agent.jar src/com/agent/MANIFEST.MF -C out . -C javassist .注意:
-
这里需要使用javassist.jar包进行字节码的改写,可以使用maven下载javassist.jar,也可以直接去网站下载。但是我通过网站下载到的jar包中的对应修改功能模块一直不能被代码识别,建议还是和我一样使用maven下载的javassist.jar包
-
同时,最好删除掉之前生成过的out文件,不然的话后续生成的jar包里面会包含之前生成的class文件
-
这里新建一个temp-javassist的目的是:后续添加到agent.jar包中的javassist相关class类文件都在javassist文件夹下,代码中的
import javassist.*;
会找不到javassist模块。
-cp 表示连接外部jar,因为这里使用到了外部的javassist.jar
可以使用下面的命令查看jar包内容,确定是否成功打包:
1
jar tf agent.jar
-
-
编写
attacher.jar
通过pid将java agent attach到目标JVM,然后通过LoadAgent加载指定的jar文件
AgentAttach.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
30package com.attacher;
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;
public class AgentAttach {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String id = args[0]; // 获取目标 JVM 的 PID
String jarName = args[1]; // 要注入的 Java Agent 的 jar 文件路径
System.out.println("id ==> " + id);
System.out.println("jarName ==> " + jarName);
// Attach 到指定的 JVM 进程
VirtualMachine virtualMachine = VirtualMachine.attach(id);
// 加载 Java Agent
virtualMachine.loadAgent(jarName);
// Detach 后释放与目标 JVM 的连接
virtualMachine.detach();
System.out.println("ends");
}
}MANIFEST.MF
:1
2
3Manifest-Version: 1.0
Main-Class: com.attacher.AgentAttach类似的,编译打包成
attacher.jar
1
2
3
4
5# 1. 编译
javac -d out src/com/attacher/*.java
# 2. 打包
jar cvfm attacher.jar src/com/attacher/MANIFEST.MF -C out . -
Agent 注入
启动目标程序:在终端中运行
hello.jar
1
java -jar hello.jar
可以看到对应输出的JVM的name和PID。
注入
agent.jar
:在另一个终端中运行以下命令,通过Attach API 将 Java Agent (agent.jar
) 动态注入到hello.jar
的 JVM 上。1
2// 29976为目标JVM的[PID]
java -jar attacher.jar 29976 "./agent.jar"输出"java memory shell",成功将
System.out.println("java memory shell");
对应的字节码修改为System.out.println("java memory shell");
-
注入内存马
将
System.out.println("java memory shell");
替换为想要执行的内存马代码即可。修改哪个类的哪个方法,是注入内存马的前提和关键。我们需要利用上述方法,将木马注入到某个一定会执行的方法内。后门的本质就是在目标上留下一个用户可控的参数,黑客通过控制这个参数,达到执行任意系统命令的目的。因此,想要注入内存马,就必然绕不开 request 和 response,因此大多的内存马将目标放在FilterChain上面,通过修改Filter来注入恶意代码
RASP检测
与注入内存马一样,我们同样可以利用Java的Instrument机制,动态注入我们的检测Agent,获取JVM中所有已加载的Class,匹配内存马特有的可疑特征,让隐藏的内存马现出原型。
检测步骤:
- 1)Attach检测Agent到JVM进程
- 2)获取JVM中已经加载的Class列表
- 3)根据指纹特征将可疑的Class反编译为Java源码
- 4)根据源码检测出Webshell
所以,我们需要分析常见的内存马存在的一些可疑的特征,比如:
Agent通过加载Transformer实现功能,Transformer继承ClassFileTransformer接口
常见的特征包括:
- 继承可能实现webshell功能的接口
- javax.servlet.http.HttpServlet
- org.springframework.web.servlet.handler.AbstractHandlerMapping
- javax.servlet.Filter
- javax.servlet.Servlet
- javax.servlet.ServletRequestListener
- …
- 名字
- shell
- memshell
- …
- 常见已知的Webshell包名:
- net.rebeyond.*
- com.metasploit.*
- …
相关技术
修改字节码
Javassist
修改字节码的技术有很多,比如 ASM、Javassist、BCEL、CGLib 等,这里仅简要介绍 Javassist。Javassist 可以直接用 Java 编码来实现增强,无需关注字节码结构,比 ASM 更简单。Javassist 中核心的类主要有四个:
- CtClass:类信息
- ClassPool:可以从中获取 CtClass,key 为类的全限定名
- CtMethod:方法信息
- CtField:字段信息
基于这四个类,可以方便地实现增强,比如在指定方法前后增加代码:
1 | // 获取默认 ClassPool |
例子
创建 HelloWorld.java
, hello.java
, GetPid.java
三个java文件
1 | // HelloWorld.java |