Hunting queries — UAC bypass via auto-elevated COM (T1548.002)

CLSIDs referenced throughout:

CLSID Object Interface Provider DLL
{3E5FC7F9-9A51-4367-9063-A120244FBEC7} CMSTPLUA ICMLuaUtil cmlua.dll
{D2E7041B-2927-42FB-8E9F-7CE93B6DC937} ColorDataProxy ICMLuaUtil colorui.dll

Expected process tree when the technique fires:

svchost.exe (-k DcomLaunch)
└── dllhost.exe /Processid:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}   <- High integrity, no consent.exe
    └── <payload>.exe / cmd.exe / powershell.exe                     <- inherits High integrity

The absence of consent.exe immediately before the high-integrity child is the discriminator between this and a legitimate user-approved elevation.


Microsoft Defender for Endpoint — Advanced Hunting (KQL)

1. Surrogate launch for an auto-elevating CLSID

let elevCLSIDs = dynamic([
    "3e5fc7f9-9a51-4367-9063-a120244fbec7",
    "d2e7041b-2927-42fb-8e9f-7ce93b6dc937"
]);
DeviceProcessEvents
| where FileName =~ "dllhost.exe"
| extend clsid = tolower(extract(@"/Processid:\{?([0-9a-fA-F\-]{36})\}?", 1, ProcessCommandLine))
| where clsid in (elevCLSIDs)
| project Timestamp, DeviceName, AccountName, clsid, ProcessCommandLine,
          ProcessIntegrityLevel, InitiatingProcessFileName, ProcessId
| order by Timestamp desc

2. Payload — elevated child of the surrogate

DeviceProcessEvents
| where InitiatingProcessFileName =~ "dllhost.exe"
| where InitiatingProcessCommandLine has_any (
      "3E5FC7F9-9A51-4367-9063-A120244FBEC7",
      "D2E7041B-2927-42FB-8E9F-7CE93B6DC937")
| project Timestamp, DeviceName, AccountName,
          FileName, ProcessCommandLine, ProcessIntegrityLevel,
          InitiatingProcessCommandLine, SHA256
| order by Timestamp desc

3. Silent elevation — high integrity with no preceding consent.exe

Strong behavioural hunt that generalises past any single CLSID.

let window = 30s;
let elevated =
    DeviceProcessEvents
    | where ProcessIntegrityLevel in ("High", "System")
    | where InitiatingProcessIntegrityLevel == "Medium"
    | project Timestamp, DeviceId, DeviceName, AccountName, FileName,
              ProcessCommandLine, InitiatingProcessFileName,
              InitiatingProcessCommandLine, ProcessId;
let consents =
    DeviceProcessEvents
    | where FileName =~ "consent.exe"
    | project DeviceId, ConsentTime = Timestamp;
elevated
| join kind=leftouter consents on DeviceId
// Count the consent prompts that fall in the window for THIS process, then keep
// the processes that had none. Filtering row-by-row instead would let a process
// through on the strength of some other, unrelated consent event on the device.
| summarize ConsentInWindow = countif(ConsentTime between ((Timestamp - window) .. Timestamp))
          by DeviceId, ProcessId, Timestamp, DeviceName, AccountName, FileName,
             ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| where ConsentInWindow == 0
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
          InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc

4. Moniker string in script content

DeviceEvents
| where ActionType == "PowerShellCommand"
| where AdditionalFields has "Elevation:Administrator!new:"
| project Timestamp, DeviceName, AccountName, InitiatingProcessCommandLine, AdditionalFields

Splunk (Sysmon)

Surrogate launch + payload, correlated by parent GUID

index=windows sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
    EventCode=1
    (
        (Image="*\\dllhost.exe" CommandLine="*3E5FC7F9-9A51-4367-9063-A120244FBEC7*")
        OR
        (ParentImage="*\\dllhost.exe" ParentCommandLine="*3E5FC7F9-9A51-4367-9063-A120244FBEC7*")
    )
| eval stage=if(match(Image, "dllhost\.exe$"), "surrogate_launch", "elevated_payload")
| table _time host User stage Image CommandLine ParentImage ParentCommandLine IntegrityLevel ProcessGuid ParentProcessGuid
| sort - _time

Elevated process with no consent.exe in the preceding 30 seconds

index=windows sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational EventCode=1
    (IntegrityLevel="High" OR IntegrityLevel="System")
| eval is_consent=if(match(Image, "consent\.exe$"), 1, 0)
| sort 0 host _time
| streamstats time_window=30s max(is_consent) as consent_seen by host
| where is_consent=0 AND consent_seen=0
| search NOT Image IN ("*\\services.exe","*\\svchost.exe","*\\wininit.exe","*\\lsass.exe","*\\MsMpEng.exe")
| table _time host User Image CommandLine ParentImage IntegrityLevel

Elastic — EQL sequence

Ties the surrogate launch to its elevated child in one signal.

sequence by host.id with maxspan=1m
  [ process where event.type == "start"
      and process.name == "dllhost.exe"
      and process.command_line : (
            "*3E5FC7F9-9A51-4367-9063-A120244FBEC7*",
            "*D2E7041B-2927-42FB-8E9F-7CE93B6DC937*") ] by process.entity_id
  [ process where event.type == "start" ] by process.parent.entity_id

Windows Event Log / 4688 fallback

If Sysmon is not deployed, enable Audit Process Creation plus Include command line in process creation events, then:

Event ID 4688
  New Process Name  ends with  \dllhost.exe
  Process Command Line  contains  3E5FC7F9-9A51-4367-9063-A120244FBEC7

and the follow-on event where Creator Process Name is that dllhost.exe. Note that 4688 does not carry integrity level — pair with 4624 logon elevation data or accept the reduced fidelity.


Sysmon configuration — required telemetry

Minimal additions to capture everything the rules above need.

<Sysmon schemaversion="4.90">
  <EventFiltering>

    <!-- EID 1: keep every COM surrogate launch and everything it spawns -->
    <RuleGroup name="ProcCreate-COMSurrogate" groupRelation="or">
      <ProcessCreate onmatch="include">
        <CommandLine condition="contains">3E5FC7F9-9A51-4367-9063-A120244FBEC7</CommandLine>
        <CommandLine condition="contains">D2E7041B-2927-42FB-8E9F-7CE93B6DC937</CommandLine>
        <ParentImage condition="image">dllhost.exe</ParentImage>
        <Image condition="image">consent.exe</Image>
      </ProcessCreate>
    </RuleGroup>

    <!-- EID 7: provider DLL mapping. Exclude the normal surrogate to cut volume. -->
    <RuleGroup name="ImageLoad-ICMLuaUtil" groupRelation="or">
      <ImageLoad onmatch="include">
        <ImageLoaded condition="end with">\cmlua.dll</ImageLoaded>
        <ImageLoaded condition="end with">\colorui.dll</ImageLoaded>
      </ImageLoad>
      <ImageLoad onmatch="exclude">
        <Image condition="image">cmstp.exe</Image>
      </ImageLoad>
    </RuleGroup>

    <!-- EID 10: token theft / handle abuse often accompanies elevation attempts -->
    <RuleGroup name="ProcessAccess-Elevation" groupRelation="or">
      <ProcessAccess onmatch="include">
        <TargetImage condition="image">dllhost.exe</TargetImage>
        <GrantedAccess condition="is">0x1F0FFF</GrantedAccess>
      </ProcessAccess>
    </RuleGroup>

  </EventFiltering>
</Sysmon>

Also enable, via GPO or registry:


Tuning and validation notes

Baseline before deploying. Query 30 days of dllhost.exe command lines and build an allow-list of CLSIDs your environment legitimately surrogates. On most fleets the two CLSIDs above never appear, which makes rules 1 and 2 near zero-noise. Environments that genuinely deploy Connection Manager profiles via cmstp.exe are the main exception.

Validate with vetted tooling, not custom code. Atomic Red Team’s T1548.002 atomics are sandboxed, documented, and safe to run in a lab. Use those to confirm the rules fire rather than authoring a bypass yourself.

Expect CLSID rotation. The auto-elevate CLSID list is long and attackers swap objects freely. Rule 3 in the Sigma file and KQL query 3 are the durable detections — they key on the structure of the bypass (medium-integrity parent, high-integrity child, no consent prompt) rather than a specific GUID. Weight your alerting toward those and treat the CLSID lists as fast-but-brittle signatures.

Blind spot to be aware of. If the payload is loaded in-process rather than executed via ShellExec — for example by writing a registry value through ICMLuaUtil::SetRegistryStringValue and waiting for another process to consume it — there is no elevated child process to catch. Cover that path with registry-write monitoring on the keys the technique targets, and with the EID 7 provider-load rule.

References