RASP从0到Demo实现

前言

在2012年的时候,Gartner引入了“Runtime application self-protection”一词,简称为RASP。它是一种新型应用安全保护技术,它将保护程序像疫苗一样注入到应用程序中,应用程序融为一体,能实时检测和阻断安全攻击,使应用程序具备自我保护能力,当应用程序遭受到实际攻击伤害,就可以自动对其进行防御,而不需要进行人工干预。

RASP技术可以快速的将安全防御功能整合到正在运行的应用程序中,它拦截从应用程序到系统的所有调用,确保它们是安全的,并直接在应用程序内验证数据请求。Web和非Web应用程序都可以通过RASP进行保护。该技术不会影响应用程序的设计,因为RASP的检测和保护功能是在应用程序运行的系统上运行的。

RASP和WAF

很多时候大家在攻击中遇到的都是基于流量规则的waf防御,waf往往误报率高,绕过率高,市面上也有很多针对不同waf的绕过方式。

image-20250427221654762

而RASP技术防御是根据请求上下文进行拦截的,RASP进程是直接嵌入到APP执行流程中去,这一点和WAF有本质的不同。正是由于这一点,RASP可以避免WAF规则被各种奇异的编码绕过的痛点,因为Agent进程最终获取的参数正是各个层面编码转换完成后真正执行的参数。并且RASP不像WAF那样需要拦截每个请求去check是否命中了攻击的规则,而是当HOOK住的危险函数被调用之后,才会触发检测逻辑。

image-20250427221749808

例如攻击者对url为 http://xxx.com/index.do?id=1 进行测试,一般情况下,扫描器或者人工测试sql注入都会进行一些sql语句的拼接,来验证是否有注入,会对该url进行大量的发包,发的包可能如下:

1
http://xxx.com/index.do?id=1' and 1=2--

但是应用程序本身已经在程序内做了完整的注入参数过滤以及编码或者其他去危险操作,实际上访问该链接以后在数据库中执行的sql语句为:

1
select id,name,age from home where id='1 \' and 1=2--'

可以看到这个sql语句中已经将单引号进行了转义,导致无法进行,但是waf大部分是基于规则去拦截的(也有小部分WAF是带参数净化功能的),如果你的请求参数在他的规则中存在,那么waf都会对其进行拦截,这样会导致误报率大幅提升。

RASP技术可以做到程序底层拼接的sql语句到数据库之前进行拦截。在应用程序将sql语句预编译的时候,RASP可以在其发送之前将其拦截下来进行检测,如果sql语句没有危险操作,则正常放行,不会影响程序本身的功能。如果存在恶意攻击,则直接将恶意攻击的请求进行拦截或净化参数。

WAF优势

  1. 攻击前流量预警:攻击者在实施真正的攻击前,会产生大量的异常流量,这些流量包括推测服务器环境信息、可注入点尝试等。这些流量通常不会直接造成危害,因此RASP可能无法获悉全量的攻击流量(只会处理可能有危害的流量),而WAF可以完整记录异常流量。
  2. 对于CC攻击、爬虫、恶意扫描和脚本小子这些大流量的攻击或者有明显攻击特征的流量,如果让其直接打到装有RASP插桩的应用上,会造成不必要的性能占用;另外由于RASP会占用应用程序的计算资源,因此也不适合进行过于复杂的计算。所以对于此类攻击,最好的办法就是使用WAF从流量侧对其分析和拦截。

image-20250427222021679

RASP优势

  1. 拦截混淆和加密的流量:RASP并不需要对流量进行解密,可以根据场景对恶意行为进行分析,有效拦截被精心设计的攻击流量。
  2. 针对业务场景进行优化:基于RASP函数Hook的特性,不仅可以对通用类、框架类的函数进行插桩,也可以对自研代码部分进行插桩。例如对于应用在交付前来不及修补的漏洞,可以通过函数级别的虚拟补丁提供防护,保证应用按时交付。
  3. 极低的维护成本:除了根据需要配置虚拟补丁外,由于RASP从底层函数进行保护,所以基本上不需要对RASP的规则做任何调整即可实现应用的安全内建。
  4. 兼顾东西向流量安全:RASP工作在应用程序内部,不仅可以分析南北向流量的风险,也可以分析企业内部,应用之间东西向流量的风险。例如微服务架构中涉及多个模块间的调用,它们之间通常会使用rpc等非http协议来进行数据交换,传统的 WAF 通常对其无能为力,而 RASP 则可以很好的解决这样的问题。
  5. 防御0day漏洞:RASP可以保护应用运行时环境中的所有代码,包括自研代码、第三方组件、Web应用容器(Tomcat、Django、Flask等)。例如最近几个波及范围较广的0day漏洞:Log4j2 RCE(CVE-2021-44228)、Spring RCE(CVE-2022-22965)、Fastjson反序列化漏洞等等,虽然攻击方式有变化,但是最终实施攻击总是需要调用一些底层的方法/函数。无论攻击入口如何变化、攻击手段如何隐蔽,都无法绕开最终关键函数的执行过程,因此RASP一定能对其进行有效拦截。

image-20250427222147839

RASP在Java进程的实现

概述

Java是通过Java Agent方式进行实现(Agent本质是java中的一个动态库,利用JVMTI暴露的一些接口实现的),具体使用ASM(或者其他字节码修改框架如javassist)技术实现RASP技术。

Java Agent有三种机制Agent_OnLoad、Agent_OnAttach、Agent_OnUnload,大部分时候使用的都是Agent_OnLoad技术和Agent_OnAttach技术,Agent_OnUnload技术很少使用。

在jdk1.5之后,java提供一个名为Instrumentation的API接口,Instrumentation 的最大作用,就是类定义动态改变和操作。开发者可以在一个普通Java程序(带有 main 函数的 Java 类)运行时,通过 – javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。

Java Instrumentation是从JavaSE 5开始提供的新特性,用于构建独立于java应用的agent程序,主要目的是对JVM上的应用进行监控,比如性能优化监控等等。

通过这个特性,我们可以实现在不修改JVM源码的基础上操控字节码,这也就可以实现一种虚拟机级别的AOP机制,这和Spring中的基于动态代理实现的AOP机制是有所不同的,前者更加轻量化,与项目的耦合性更低。

JavaSE 6中,Instrumentation的功能更加强大,甚至可以对原生代码(Native code)进行修改。

RCE远程代码执行场景RASP实现

编写Agent类实现premain方法

Java代理程序入口类需要有名为premain的静态方法,Premain方法只做一件事情,就是设置一个ClassFileTransform,用来获取和操作字节码。

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Agent {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
System.out.println("++++++++++++++++++start++++++++++++++++++\n");
// 添加ClassFileTransformer类
inst.addTransformer(new RceHook(inst));
}
}

PreMain方法参数:

  1. agentArgs:传递给代理的参数。

  2. inst:Instrumentation 对象,用于操控 JVM 的字节码加载和转换过程。

RceHook 是实现了 ClassFileTransformer 接口的类,用于在类加载时对字节码进行修改。

编写RceHook类实现transform方法

Java Agent 修改字节码的关键在于 transformer() 方法,因此重写该方法即可,可以将其修改成过滤的代码。RceHook类实现了Java的代理程序机制提供的ClassFileTransformer接口 ,能够在运行时(Runtime)对类的字节码进行替换与修改,RceHook也只有一个实现方法:transform。

transform方法参数:

  1. loader:加载当前类的类加载器。

  2. className:当前加载的类的名称,以斜杠分隔的形式。

  3. classBeingRedefined:如果这是类重定义或重新转换,则为正在重定义或重新转换的类;如果这是类的初始定义,则为 null。

  4. protectionDomain:当前类的保护域信息。

  5. classfileBuffer:当前类的字节码。

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
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class RceHook implements ClassFileTransformer {
private Instrumentation inst;
private ClassPool classPool;
public RceHook(Instrumentation inst){
this.inst = inst;
this.classPool = new ClassPool(true);
}

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
classPool.insertClassPath(new javassist.LoaderClassPath(loader));
if (className.equals("java/lang/ProcessBuilder")){
CtClass ctClass = null;
try {
// 找到ProcessBuilder对应的字节码
ctClass = this.classPool.get("java.lang.ProcessBuilder");
// 获取所有method
CtMethod[] methods = ctClass.getMethods();
// $0代表this,这里this = 用户创建的ProcessBuilder实例对象
String src = "if ($0.command.get(0).equals(\"cmd.exe\")) {" +
" System.out.println(\"阻止危险函数!\");" +
" return null;" +
"}";
for (CtMethod method : methods) {
// 找到start方法,并插入拦截代码
if (method.getName().equals("start")){
method.insertBefore(src);
break;
}
}
classfileBuffer = ctClass.toBytecode();
}
catch (Exception e) {
}
finally {
if (ctClass != null){
ctClass.detach();
}
}
}
return classfileBuffer;
}
}

构造函数 RceHook(Instrumentation inst) 初始化了 Instrumentation 对象 inst 和 ClassPool 对象 classPool。

1
2
3
4
public RceHook(Instrumentation inst){
this.inst = inst;
this.classPool = new ClassPool(true);
}
  1. Instrumentation 是 JVM 提供的一个接口,用于在运行时获取并修改字节码。

  2. ClassPool 是 Javassist 提供的一个类,用于操作类字节码。

这里的transform方法先检查当前加载的类名是否为 java/lang/ProcessBuilder。

1
if (className.equals("java/lang/ProcessBuilder"))

声明一个 CtClass 对象用于操作字节码,使用 classPool 获取 ProcessBuilder 类的字节码,通过getMethods方法获取 ProcessBuilder 类的所有方法。

1
CtMethod[] methods = ctClass.getMethods();

遍历 ProcessBuilder 类的所有方法,检查方法名是否为 start,是的情况下在 start 方法的开头插入拦截代码。

1
2
3
4
5
6
for (CtMethod method : methods) {
// 找到start方法,并插入拦截代码
if (method.getName().equals("start")){
method.insertBefore(src);
break;
}

这段插入的拦截代码会在 ProcessBuilder 的 start 方法被调用时执行,它检查第一个命令参数是否为 “cmd.exe”,如果是则强制退出程序,起到拦截作用。

1
2
3
4
5
if ($0.command.get(0).equals("cmd.exe")) {
System.out.println("阻止危险函数!");
System.exit(0);
}
//$0 是当前实例对象,类似于 this.command.get(0)

$0

  1. 含义: this,即当前实例对象。

  2. 作用: 在实例方法中,$0 代表调用该方法的对象。在静态方法中,$0 不存在,因为静态方法没有关联的实例对象。

将修改后的字节码转换为字节数组并返回,至此整个简易的拦截RCE的RASP就设计完成。

1
classfileBuffer = ctClass.toBytecode();

编写命令执行场景

java.lang.ProcessBuilder也是java.lang中的一个API,该类主要用于创建操作系统进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Main {
public static void main(String[] args) throws InterruptedException, IOException {
System.out.println("main start!");
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command("cmd.exe","/c", "whoami");
Process process = processBuilder.start();
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "gbk"));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
}
}

在java.lang.ProcessBuilder中主要关注command()方法以及start()方法,可通过该方法设置要执行的命令参数。

  1. command()方法

command()方法主要用于设置要执行的命令,command()方法传参有两种方式,一种是可变的字符串(普通字符串或者字符串数组),另一种是字符串列表。

img

1
2
ProcessBuilder p = new ProcessBuilder();
p.command("calc");
  1. start()方法

使用start()方法可以创建一个新的具有命令或环境或工作目录或输入来源或标准输出和标准错误输出的目标或redirectErrorStream属性的进程。

新进程中调用的命令和参数有command()方法设置,工作目录将由directory()方法设置,进程环境将由environment()设置。

在使用command()方法设置执行命令参数后,然后由start()方法创建一个新的进程进而在系统中执行了我们设置的命令

1
Process cmd = new ProcessBuilder(command).start();

拦截效果

单独运行命令执行恶意jar时候,会将命令直接输出在控制台,没有拦截。

img

配合RASP进行agent注入后再次执行,发现成功拦截命令执行函数。

img

SQL注入场景RASP实现

编写Agent类实现premain方法

Java代理程序入口类需要有名为premain的静态方法,这里和RCE远程代码执行的总体框架大致相同。

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Agent {
public static void premain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
System.out.println("++++++++++++++++++start++++++++++++++++++\n");
// 添加ClassFileTransformer类
inst.addTransformer(new SqlHook(inst));
}
}

SqlHook 是实现了 ClassFileTransformer 接口的类,用于在类加载时对字节码进行修改。

编写SqlHook类实现transform方法

在SqlHook类中进行在运行时(Runtime)对类的字节码进行替换与修改,也只有一个实现方法:transform,这里的逻辑根据Sql注入可能的场景进行设计。

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
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class SqlHook implements ClassFileTransformer {
private Instrumentation inst;
private ClassPool classPool;
public SqlHook(Instrumentation inst){
this.inst = inst;
this.classPool = new ClassPool(true);
}

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
classPool.insertClassPath(new javassist.LoaderClassPath(loader));
if (className.equals("com/mysql/cj/jdbc/StatementImpl")){
CtClass ctClass = null;
try {
// 找到StatementImpl对应的字节码
ctClass = this.classPool.get("com.mysql.cj.jdbc.StatementImpl");
// 获取所有method
CtMethod[] methods = ctClass.getMethods();
// $1 代表方法的第一个参数,这里为 SQL 查询字符串
String src = "if ($1.split(\"'\").length > 3) {" +
" System.out.println(\"阻止危险Sql注入操作!\");" +
" return null;" +
"}";
for (CtMethod method : methods) {
// 找到executeQuery方法,并插入拦截代码
if (method.getName().equals("executeQuery")){
method.insertBefore(src);
break;
}
}
classfileBuffer = ctClass.toBytecode();
}
catch (Exception e) {
}
finally {
if (ctClass != null){
ctClass.detach();
}
}
}
return classfileBuffer;
}
}

构造函数 SqlHook(Instrumentation inst) 初始化了 Instrumentation 对象 inst 和 ClassPool 对象 classPool。

1
2
3
4
public SqlHook(Instrumentation inst){
this.inst = inst;
this.classPool = new ClassPool(true);
}

这里的transform方法先检查当前加载的类名是否为 com/mysql/cj/jdbc/StatementImpl(MySQL 驱动的实现类)。

1
if (className.equals("com/mysql/cj/jdbc/StatementImpl"))

声明一个 CtClass 对象用于操作字节码,使用 classPool 获取 StatementImpl 类的字节码,通过getMethods方法获取 StatementImpl 类的所有方法。

1
CtMethod[] methods = ctClass.getMethods();

遍历 StatementImpl 类的所有方法,检查方法名是否为 executeQuery 是的情况下在 executeQuery 方法的开头插入拦截代码。

1
2
3
4
5
6
7
for (CtMethod method : methods) {
// 找到executeQuery方法,并插入拦截代码
if (method.getName().equals("executeQuery")){
method.insertBefore(src);
break;
}
}

这段插入的拦截代码会在 executeQuery 方法中执行,这里通过检查 SQL 查询中的单引号数量是否超过两个,如果单引号数量大于 2 则强制退出程序进行简单拦截效果(正常用户单值查询只需使用自身的两个单引号即可),更加合理的拦截方法可以自行实现。

1
2
3
4
5
if ($1.split(\"'\").length > 3) {
System.out.println(\"阻止危险Sql注入操作!\");
System.exit(0);
}
//$1.split("'"):将字符串按单引号分割成一个字符串数组,如果数组的长度大于 3,则说明字符串中包含了超过两个单引号。

$1、$2、$3…

  1. 含义: 方法的参数。

  2. 作用: $1 代表方法的第一个参数,$2 代表第二个参数,依此类推。

将修改后的字节码转换为字节数组并返回,至此整个简易的拦截Sql注入的RASP就设计完成。

1
classfileBuffer = ctClass.toBytecode();

编写Sql注入场景

这里以经典的JDBC的方式实现场景,Java数据库连接(Java DataBase Connectivity)是SUN公司定义的一套接口(规范),不同数据库厂商要实现和Java连接,需要数据库厂商通过驱动去实现。

image-20250427223910299

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
package org.example;

import java.sql.*;
import java.util.Scanner;

public class Main {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 加载 MySQL JDBC 驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// 数据库连接信息
String url = "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
Connection conn = DriverManager.getConnection(url, "root", "root");

// 获取用户输入的用户名
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名: ");
String userInput = scanner.nextLine();

String sqlQuery = "SELECT * FROM user WHERE user_name = '" + userInput + "'";
Statement sta = conn.createStatement();

// 执行查询
ResultSet rs = sta.executeQuery(sqlQuery);

// 处理查询结果
while (rs.next()) {
System.out.println("ID: " + rs.getInt("id"));
System.out.println("用户名: " + rs.getString("user_name"));
System.out.println("邮箱: " + rs.getString("email"));
}

// 关闭资源
rs.close();
sta.close();
conn.close();
}
}

通过 Class.forName 反射的方法加载 MySQL JDBC 驱动,这个操作注册了驱动,使得程序可以与 MySQL 数据库进行通信。

1
Class.forName("com.mysql.cj.jdbc.Driver");

设置数据库连接 URL 、账号、密码,然后开始建立连接。

1
2
String url = "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
Connection conn = DriverManager.getConnection(url,"root","root");

url 包含连接 MySQL 数据库的详细信息

  1. jdbc:mysql://127.0.0.1:3306/test: 指定了数据库类型、服务器地址、端口号和数据库名称。

  2. useSSL=false: 禁用 SSL 连接。

  3. useUnicode=true 和 characterEncoding=UTF-8: 确保使用 UTF-8 字符集。

  4. serverTimezone=Asia/Shanghai: 设置服务器的时区。

  5. allowPublicKeyRetrieval=true: 允许在公共密钥不可用时检索公共密钥。

创建 SQL 语句和执行查询,这里以标准输入的形式让用户输入进行模拟。

1
2
String userInput = scanner.nextLine();
String sqlQuery = "SELECT * FROM user WHERE user_name = '" + userInput + "'";

遍历输出执行的返回数据。

1
2
3
4
5
while (rs.next()) {
System.out.println("ID: " + rs.getInt("id"));
System.out.println("用户名: " + rs.getString("user_name"));
System.out.println("邮箱: " + rs.getString("email"));
}

拦截效果

运行Sql场景并通过 – javaagent参数指定用于拦截Sql注入的RASP jar 文件,进行正常查询操作,可以返回正常数据。

img

输入恶意Sql注入的语句,RASP会进行拦截操作。

img

对应拦截的ClassLoader为sun.misc.Launcher$AppClassLoader@18b4aac2。

img

其他场景RASP实现

对于其他场景下实现RASP原理大同小异,主要是对class类和需要hook的关键方法的选择,这些都体现在ClassTransformer类实现的transform方法中,均可以通过这个框架进行实现。

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
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
classPool.insertClassPath(new javassist.LoaderClassPath(loader));
if (className.equals("class类")){
CtClass ctClass = null;
try {
// 找到class对应的字节码
ctClass = this.classPool.get("class类");
// 获取所有method
CtMethod[] methods = ctClass.getMethods();
String src = "hook的实现逻辑代码";
for (CtMethod method : methods) {
// 找到需要hook的方法,并插入拦截代码
if (method.getName().equals("需要hook的方法")){
method.insertBefore(src);
break;
}
}
classfileBuffer = ctClass.toBytecode();
}
catch (Exception e) {
}
finally {
if (ctClass != null){
ctClass.detach();
}
}
}
return classfileBuffer;
}

在hook的实现逻辑里面,有些常用的Javassist用法可以帮助我们更好的修改需要的逻辑实现。

  1. $0

● 含义: this,即当前实例对象。

● 作用: 在实例方法中,$0 代表调用该方法的对象。在静态方法中,$0 不存在,因为静态方法没有关联的实例对象。

  1. $1、$2、$3…

● 含义: 方法的参数。

● 作用: $1 代表方法的第一个参数,$2 代表第二个参数,依此类推。

  1. $args

● 含义: 表示方法的所有参数,作为一个数组。

● 作用: 可以用于遍历或操作所有传入的参数。

  1. $_

● 含义: 表示方法的返回值。

● 作用: 在方法返回值的修改中使用。如果在方法体内对 $_ 赋值,则可以修改返回值。

  1. $$

● 含义: 表示方法的所有参数的占位符。

● 作用: 在调用另一个方法时,将当前方法的所有参数传递给另一个方法。

  1. $class

● 含义: 表示当前类的 Class 对象。

● 作用: 获取当前正在修改的类的 Class 对象。

这些变量和占位符是 Javassist 提供的强大工具,用于在修改字节码时操作方法的参数、对象、返回值等,简化了字节码操作的复杂性。

WEB场景下的SQL注入RASP实现

SpringBoot场景

环境搭建

创建一个SpringBoot工程,引入数据库依赖mysql-connector-java用于Java连接数据库进行操作。

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>

编写UserController类,用作用户和数据库交互。

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
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.sql.*;

@Controller
public class UserController {

public class JDBC {
public boolean check(String username, String password) throws ClassNotFoundException, SQLException {
// 加载 MySQL JDBC 驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 数据库连接信息
String url = "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
Connection conn = DriverManager.getConnection(url, "root", "root");
//用户登录逻辑SQL语句
String sqlQuery = "SELECT * FROM user WHERE user_name = '" + username + "' AND pass_word = '" + password + "'";
Statement sta = conn.createStatement();
// 执行查询
ResultSet rs = sta.executeQuery(sqlQuery);
// 判断是否有结果
boolean hasResult = rs.next();
// 关闭资源
rs.close();
sta.close();
conn.close();
//返回结果
return hasResult;
}
}
JDBC jdbc = new JDBC();
@RequestMapping(value="/login", method= RequestMethod.POST)
@ResponseBody
public String checkLogin(@RequestParam(name = "username") String username,@RequestParam(name = "password") String password) {
try {
boolean isValidUser = jdbc.check(username, password);
System.out.println(username);
if (!isValidUser) {
return "false";
} else {
return "success";
}
} catch (SQLException | ClassNotFoundException e) {
return "error";
}
}
}

这里的用户登录的SQL处理逻辑,将用户的输入直接拼接到SQL查询语句中,存在明显的SQL注入攻击的可能。

1
"SELECT * FROM user WHERE user_name = '" + username + "' AND pass_word = '" + password + "'"

编写前端登录页面,用于用户进行对数据库进行传参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Form</title>
</head>
<body>
<form action="/login" method="post">
<label for="username">Username:</label><br>
<input type="text" id="username" name="username"><br>
<label for="password">Password:</label><br>
<input type="password" id="password" name="password"><br><br>
<input type="submit" value="提交">
</form>
</body>
</html>
RASP对SpringBoot拦截

单独运行SpringBoot环境,进行SQL注入攻击测试。

1
java -jar Sql-web-0.0.1-SNAPSHOT.jar

image-20250428024834868

访问 http://localhost:8080/ 进行测试启动运行状态。

img

对目标进行SQL注入测试,username参数存在SQL注入。

1
sqlmap -r request.txt --purge --batch

image-20250428024915297

image-20250428024934470

将RASP注入到SpringBoot环境进行测试。

1
java -javaagent:.\Rasp-sql-1.0-SNAPSHOT-jar-with-dependencies.jar -jar .\Sql-web-0.0.1-SNAPSHOT.jar

img

手工进行SQL注入试探,成功触发RASP防御机制,阻止恶意SQL语句执行。

1
admin' or '1'=1--

img

再次对目标进行SQL注入测试,显示已经不存在SQL注入风险。

1
sqlmap -r request.txt --purge --batch

img

对应拦截ClassLoader为org.springframework.boot.loader.LaunchedURLClassLoader@5ef04b5。

img

Tomcat场景(Servlet)

环境搭建

引入 Servlet 依赖和数据库依赖 ,Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>

编写Servlet,用作用户和数据库交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

String username = request.getParameter("username");
String password = request.getParameter("password");

boolean isValid = validateUser(username, password);

response.setContentType("text/html");
PrintWriter out = response.getWriter();

if (isValid) {
out.println("<h3>Login successful! Welcome, " + username + ".</h3>");
} else {
out.println("<h3>Invalid username or password.</h3>");
}
}

private boolean validateUser(String username, String password) {
boolean isValid = false;
String dbURL = "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true";
String dbUser = "root";
String dbPassword = "root";
try {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection(dbURL, dbUser, dbPassword);
String sql = "SELECT * FROM user WHERE user_name = '" + username + "' AND pass_word = '" + password + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
if (resultSet.next()) {
isValid = true;
}
resultSet.close();
statement.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
return isValid;
}
}

这里的用户登录的SQL处理逻辑和SpringBoot一致,将用户的输入直接拼接到SQL查询语句中,存在明显的SQL注入攻击的可能。

1
SELECT * FROM user WHERE user_name = '" + username + "' AND pass_word = '" + password + "'

编写前端登录页面,用于用户进行对数据库进行传参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>Login</h2>
<form method="post" action="login">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required><br><br>

<label for="password">Password:</label>
<input type="password" id="password" name="password" required><br><br>

<input type="submit" value="Login">
</form>
</body>
</html>
RASP对Tomcat拦截

单独运行Tomcat环境,进行SQL注入攻击测试。

img

对目标进行SQL注入测试,username参数存在SQL注入。

1
sqlmap -r request.txt --purge --batch

img

将RASP注入到Tomcat环境进行测试,在 VM Options 里面加入用于启动 Java 程序时配置虚拟机(JVM)的各种选项和参数。

1
2
3
4
-Dfile.encoding=UTF-8
-noverify
-Xbootclasspath/p:路径\Rasp-sql-1.0-SNAPSHOT-jar-with-dependencies.jar
-javaagent:路径\Rasp-sql-1.0-SNAPSHOT-jar-with-dependencies.jar

image-20250428025029536

选项和参数分别对应:

  1. 设置字符编码为 UTF-8。

  2. 禁用字节码验证。

  3. 将自定义的 JAR 文件放到引导类路径的前面,以便优先加载。

  4. 启动时加载 Java 代理来修改应用程序的字节码

再次对目标进行SQL注入测试,显示已经不存在SQL注入风险。

1
sqlmap -r request.txt --purge --batch

img

对应拦截的ClassLoader为ParallelWebappClassLoader。

img

声明:本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。