RASP基础知识

  1. 概念

    RASP(Runtime application self-protection,运行时程序自动保护技术)是一种新型应用安全保护技术,它将保护程序像疫苗一样注入到应用程序中,内置或链接到应用程序环境中与应用程序融为一体,能实时检测和阻断安全攻击,使应用程序具备自我保护能力。

  2. WAF和RASP的区别:

    image-20241108155143568

    • WAF是位于Web应用和客户端之间的安全网关,用于检测和防御来自网络层和应用层的攻击。且WAF大部分是基于规则去拦截,你的请求参数在规则中存在即拦截,误报率高。
    • RASP是一种嵌入在Web应用程序中的安全技术,在应用的运行时环境内工作,利用应用上下文信息来检测并防御攻击行为,工作在应用层。

    以防御SQL注入为例:

    • WAF:预定义规则库,匹配HTTP请求数据包参数字段中是否存在SQL注入的特征模式。可能被绕过措施绕过、误报率高
    • RASP:动态监控应用的实际运行行为。在应用程序将sql语句预编译完视图访问数据库资源时,RASP可以在其发送之前将其拦截进行检测,如果sql语句没有危险操作,则正常放行,不会影响程序本身的功能。
  3. 部署方法

    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),减少发送到副本的无意义流量,避免资源浪费
  4. 优势

    • RASP具有更高的准确性,因为它可以洞察应用程序逻辑和配置、事件和数据流、执行的指令和数据处理。而且在预生产中很容易部署,能成功阻止攻击。
    • RASP具有动态分析和上下文感知能力,可以更好地防御针对业务逻辑的攻击,包括高级攻击和零日漏洞等。
    • 通过嵌入应用实现保护,适合云原生和微服务架构,便于集成。
  5. 国内外产品浅谈RASP技术攻防之基础篇

  6. 资源

  7. 检测内存马

    • Attach 检测jar包到JVM进程
    • 获取JVM中已经加载的class列表
    • 根据以上可疑特征将可疑的class反编译为Java源码
    • 根据源码检测Webshell

Java内存马

基础知识

  1. 概念

    Java内存马(Java Memory Shell)是一种攻击手段,通常利用java的动态加载机制,在Java应用程序的内存中动态生成或加载恶意的Java对象或类,确保恶意代码在java应用内存中主流、执行,从而在不修改服务器文件系统的情况下保持对目标系统的持久访问,同时避免传统文件检测工具的发现。

  2. 例子

    攻击者通过反射或内存操作(如setAttribute())将恶意Servlet注入到当前Web应用的ServletContext中,并动态映射到指定的URL路径(如/memory),而无需在服务器的WEB-INF/classesWEB-INF/lib目录中添加任何文件。

    例如:

    • 动态添加组件:通过Java代码或脚本直接向Web服务器中添加一个Servlet、Filter或Listener。由于这些组件是在内存中注册的,不会涉及磁盘操作。

    • 类加载器挂钩:通过修改或替换ClassLoader,使其在加载指定类时附加恶意功能。此方法也不会创建新的文件,只是在内存中操作类对象。

    由于内存马不需要在磁盘上留下任何恶意文件,只要它被加载到内存中运行,就可以在不触发文件系统检测的情况下持久驻留在目标系统中。这种特性使得内存马具有高度的隐蔽性。

  3. 使用场景(优势)

    • 由于网络原因不能反弹 shell 的;
    • 内部主机通过反向代理暴露 Web 端口的;
    • 服务器上有防篡改、目录监控等防御措施,禁止文件写入的;
    • 服务器上有其他监控手段,写马后会告警监控,人工响应的;
    • 服务使用 Springboot 等框架,无法解析传统 Webshell 的;
  4. 缺点

    • 服务重启后会失效;
    • 对于传统内存马,存在的位置相对固定,已经有相关的查杀技术可以检出。

java内存马的分类

image-20241025163717595

  1. 传统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或请求路径触发恶意代码的执行。通过内存马的动态注册,这些恶意代码能够绕过文件系统的本地存储,不会在配置文件中留下任何可见的恶意设置,从而具有很强的隐蔽性和持久性。

  2. 框架型内存马

    框架性内存马是利用Web框架(如Spring、Struts、Shiro等)的特性进行持久化攻击的手段,其主要目的是利用框架的动态注册机制,将恶意代码注入到内存中,进行隐蔽的持久化控制。

    例如:Spring框架内存马通过动态注册Spring的核心组件(如Controller、Filter、Interceptor、WebFilter等),将恶意代码注入到应用的上下文中。

  3. 中间件型内存马

    中间件通常以流式和管道式方式处理请求,每个组件在处理完请求后会将其传递给下一个组件。这种设计模式为攻击者提供了多个插入点,攻击者可以在请求链的任意位置动态注入恶意组件。

    在中间件的很多功能实现上,因为采用了类似 Filter-FilterChain 的职责链模式,可以被用来做内存马,由于行业对 Tomcat 的研究较多,因此大多数的技术实现和探究是针对 Tomcat 的,但其他中间件也有相当多的探究空间。

  4. Agent 型内存马

    通过Java的Java Agent机制来注入恶意代码的一种内存马形式。它利用JVM的Instrumentation API 来实现对应用程序类的修改、增强或替换,从而达到持久化控制和隐蔽攻击的目的。这种内存马在JVM启动时加载,并在整个JVM生命周期内驻留,是一种极具隐蔽性和持久性的内存马攻击手段。

  5. 其他内存马

    还有一些其他非常规的利用思路,可以用在内存马的实现上,例如 WebSocket 协议等。

但实际上,内存马的深度和广度完全不局限于此,还有很多思路可以用来进行内存马的扩展

  • 对于 Agent 型内存马,可以 hook 非常多的位置,如各种 SPI 的实现,可以接管整个 JVM,获取数据;
  • 除了基于 Web 协议的内存马,可以使用各种协议作为内存马的通信途径,如 grpc、jmx、jpda 等,或封装多层协议;
  • 对于各种中间件/框架,利用其设计模式,可挖掘出多种内存马的利用方式。

除了按照内存马的实现方式分类,还可以按照内存马的利用方式分为:冰蝎马、哥斯拉马、蚁剑马、命令回显马、流量隧道马等等。

Java Agent型内存马

Java Agent

  1. 概念

    Java Agent是JVM提供的一种机制,用于在Java应用程序启动时或运行时动态地修改、监控和管理类的字节码。它主要通过Java的Instrumentation API来实现,能在不停止应用程序的情况下动态修改已加载或者未加载的类,一般用于实现性能监控、调试、AOP(面向切面编程)、字节码增强等功能。

  2. 使用方式

    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方法。

img

agentmain方法

例子:

  1. 编写 hello.jar

    文件结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    hello_project/
    ├── src
    │ └── com
    │ └── test
    │ ├── HelloWorld.java
    │ ├── hello.java
    │ ├── GetPid.java
    │ └── MANIFEST.MF

    HelloWorld.java:主程序,用于启动并输出进程ID,等待用户输入。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package 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
    7
    package 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
    12
    package 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
    3
    Manifest-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 .
  2. 编写 agent.jar

    代码创建一个Java Agent,动态注入到目标JVM中,修改特定类的方法和行为。

    文件结构:

    1
    2
    3
    4
    5
    6
    7
    8
    hello_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
    32
    package 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";

    @Override
    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 **:监控内存中的加载类,检测到目标类后调用 transform

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package 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
    5
    Manifest-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 包并复制到项目根目录下

    image-20241115160938181

    项目根目录下执行命令编译打包

    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模块。

      image-20241115172027516

    -cp 表示连接外部jar,因为这里使用到了外部的javassist.jar

    可以使用下面的命令查看jar包内容,确定是否成功打包:

    1
    jar tf agent.jar

    image-20241115185440522

  3. 编写 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
    30
    package 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
    3
    Manifest-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 .
  4. Agent 注入

    启动目标程序:在终端中运行 hello.jar

    1
    java -jar hello.jar

    可以看到对应输出的JVM的name和PID。

    image-20241109000130195

    注入 agent.jar:在另一个终端中运行以下命令,通过Attach API 将 Java Agent (agent.jar) 动态注入到 hello.jar 的 JVM 上。

    1
    2
    // 29976为目标JVM的[PID]
    java -jar attacher.jar 29976 "./agent.jar"

    image-20241109000159066

    输出"java memory shell",成功将 System.out.println("java memory shell"); 对应的字节码修改为 System.out.println("java memory shell");

    image-20241109000318944

  5. 注入内存马

    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接口

image-20241109003250776

常见的特征包括:

  • 继承可能实现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
2
3
4
5
6
7
8
9
10
11
12
// 获取默认 ClassPool
ClassPool cp = ClassPool.getDefault();
// 找到 CtClass,重写 com.nsfocus.Demo
CtClass cc = cp.get("com.nsfocus.Demo");
// 增强方法 test
CtMethod m = cc.getDeclaredMethod("test");
// 前面插入代码
m.insertBefore("{ System.out.println(\"javassist start\"); }");
// 后面插入代码
m.insertAfter("{ System.out.println(\"javassist end\"); }");
// Java agent 获取字节码数据
return cc.toBytecode();

Javassist用法详解

例子

创建 HelloWorld.java, hello.java, GetPid.java 三个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
// HelloWorld.java
package 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
pid.GetPid();
// 产生中断,等待注入
Scanner sc = new Scanner(System.in);
sc.nextInt();
hello h2 = new hello();
h2.hello();
System.out.println("ends...");
}
}

// hello.java
package com.test;

public class hello {
public void hello() {
System.out.println("hello world");
}
}

//GetPid.java
package 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);
}

}

参考资料