Introduction
This blog post discusses two vulnerabilities discovered by NCC Group consultants during research undertaken on privilege elevation via COM local services. The first of these vulnerabilities (CVE-2019-1405) is a logic error in a COM service and allows local unprivileged users to execute arbitrary commands as a LOCAL SERVICE
user. The second vulnerability (CVE-2019-1322) is a simple service misconfiguration that allows any user in the local SERVICE
group to reconfigure a service that executes as SYSTEM
(this vulnerability was independently also discovered by other researchers). When combined, these vulnerabilities allow an unprivileged local user to execute arbitrary commands as the SYSTEM
user on a default installation of Windows 10.
COM Background
We will begin with some background material that should help frame the first vulnerability that we will discuss. Feel free to skip this section if you are comfortable with the basic concepts of COM on Windows.
Component Object Model (COM) is a binary-interface standard for software components introduced by Microsoft in 1993. Broadly speaking, this means COM is an architecture that allows developers to use certain software components in a language agnostic manner. Well known usages of COM include ActiveX and OLE (for example, embedding an Excel spreadsheet in a Word document), however COM is used internally across the entire Windows ecosystem by both Microsoft and many third party applications. It really is everywhere!
COM objects are identified via a Globally Unique Identifier (GUID) known as a CLSID, stored in the Windows registry (many COM objects are also named in the registry, but the name just links to the corresponding CLSID). A CLSID entry contains the information used by the COM subsystem when an instance of the object is created.
COM hides the implementation details of objects by exposing one or more interfaces. Essentially, an interface defines a set of methods that an object supports, without dictating anything about the implementation. The interface marks a clear boundary between code that calls a method and the code that implements the method. Every COM object supports the IUnknown
interface, and this interface is used to provide reference counting and casting to other supported interfaces via the AddRef
, Release
and QueryInterface
methods. Interfaces are typically defined via the Microsoft Interface Definition Language (MIDL) and are identified via a GUID known as an IID. Once again, these are stored in the Windows registry. Note, however, that the specific interfaces supported by a given COM object are not listed in the registry.
Loosely speaking, COM objects fall into one of two categories – those that are created inside the calling process, and those that are created out of process. For the purposes of privilege elevation attacks, out of process COM objects executing as local services are an obvious attack surface and a default installation of Windows includes many such objects.
A number of tools are available to inspect the COM objects and interfaces registered on a given instance of Windows. Perhaps the best of these is OleViewDotNet
[1], written by James Forshaw of Google Project Zero. We will not cover many of the features of this tool in this blog post, however if you are interested in learning more about COM, playing with OleViewDotNet
and reading anything James has written on the subject would be a great place to start!
The UPnP Device Host Service (CVE-2019-1405)
The UPnP Device Host service is enabled by default on Windows client operating systems from Windows XP to Windows 10 and executes as the user NT AUTHORITYLOCAL SERVICE
(this service is installed by default on server versions of Windows, but is not enabled by default on some). A number of COM objects execute as local servers in this service, as shown by OleViewDotNet
in the screenshot below:
Like any securable object in Windows, it is possible to apply access controls to COM servers and OleViewDotNet
is able to display this information. The following screenshot shows the launch permissions applied to the UPnP Device Host service:
In this case, a DACL prevents network users from launching COM objects in this service, however all local users are explicitly allowed to do so and this is clearly on the attack surface for privilege escalation.
The UPnPContainerManager
and UPnPContainerManager64
COM objects hosted by this service both implement the IUPnPContainerManager
interface. This interface is not documented, however OleViewDotNet
is able to recover some information about its exposed methods. Furthermore, Microsoft make debug symbols available for core operating system components, leading to the following initial working definition for the interface (as a fragment of MIDL):
[
object,
uuid(6d8ff8d4-730d-11d4-bf42-00b0d0118b56),
pointer_default(unique)
]
interface IUPnPContainerManager : IUnknown {
HRESULT ReferenceContainer([in] wchar_t* string1);
HRESULT UnReferenceContainer([in] wchar_t* string1);
HRESULT CreateInstance(
[in] wchar_t* string1,
[in] GUID* guid1,
[in] GUID* guid2,
IUnknown** pObject);
HRESULT CreateInstanceWithProgID(
[in] wchar_t* string1,
[in] wchar_t* guid1,
[in] GUID* guid2,
[out] IUnknown** pObject);
HRESULT Shutdown();
}
At first glance, the CreateInstance
method exposed by IUPnPContainerManager
seems particularly interesting. For those of you with some COM programming experience, both the name and the API for this method should suggest a possible relationship to the standard CoCreateInstance
method [2] used to create any COM object (although the purpose of the first argument is not immediately clear). Unfortunately, calling CreateInstance
with a well-known CLSID and IID and a dummy string as the first parameter results in an undocumented error code.
At this point, we were left with little choice but to break out a disassembler and take a look at the code implementing the method. Fortunately, identifying this code is trivial (OleViewDotNet
identifies both the module that implements the class of interest and the offsets of interface methods) and we found that the bulk of the work is performed in the CContainerManagerBase::CreateInstance
method implemented in the upnphost.dll
module. The following screenshot shows a decompilation of this method:
The string parameter passed to CreateInstance
, labelled a2
in this code, is copied to v7
and then apparently used to initialise the Block
object via a call to HrAssign
. Block
is then passed to HrLookup
and if this call succeeds, the real work then appears to be performed by the virtual function call at line 33. Debugging this code quickly revealed that when a dummy string is passed as the first argument to CreateInstance
, we fail checking the result of HrLookup
. Clearly, therefore, the contents of this argument are important.
A natural next step was to pull apart the HrLookup
function, however this turned out to be non-trivial and we were forced to reconsider. Remembering that in practice we interact with this code via the IUPnPContainerManager
interface, we decided to take a look at the other exposed methods for possible inspiration. In particular, the following screenshot shows a decompilation of the method CContainerManagerBase::ReferenceContainer
that implements ReferenceContainer
:
The start of this method looks quite similar to the implementation of CreateInstance
, however in this case if the call to HrLookup
fails, we ultimately call the HrInsertTransfer
method, passing in the Block
object created from the string argument to ReferenceContainer
. A natural assumption at this point is that prior to calling CreateInstance
we may need to call ReferenceContainer
and a quick test showed that this does indeed work. A better working definition for the IUPnPContainerManager
interface based upon this knowledge is shown below:
[
object,
uuid(6d8ff8d4-730d-11d4-bf42-00b0d0118b56),
pointer_default(unique)
]
interface IUPnPContainerManager : IUnknown {
HRESULT ReferenceContainer([in] wchar_t* containerName);
HRESULT UnReferenceContainer([in] wchar_t* containerName);
HRESULT CreateInstance(
[in] wchar_t* containerName,
[in] GUID* clsid,
[in] GUID* iid,
IUnknown** pObject);
HRESULT CreateInstanceWithProgID(
[in] wchar_t* containerName,
[in] wchar_t* progID,
[in] GUID* iid,
[out] IUnknown** pObject);
HRESULT Shutdown();
}
A call to the ReferenceContainer
method with an arbitrary string followed by a call to the CreateInstance
method with the same string causes an instance of the COM object identified by the clsid
parameter to be created in the local server process and a reference to the interface identified by iid
from this object is returned to the caller in the pObject
parameter.
So why is this interesting? Essentially, this allows a low privilege local user to use any registered in-process COM object as if it were an out of process local server executing as the user NT AUTHORITYLOCAL SERVICE
. In particular, it is possible to create an instance of a Windows Script Host Shell object and obtain a reference to the IWshShell
interface of this object. Arbitrary commands may then be executed from the context of the UPnP Device Host process via calls to the Run
method from this interface.
On Windows 10, the UPnP Device Host service is configured to execute without impersonation privileges as the user NT AUTHORITYLOCAL SERVICE
with a ServiceSidType
set to SERVICE_SID_TYPE_UNRESTRICTED
. The following screenshot from Process Explorer shows the process properties for an instance of svchost hosting the UPnP Device Host service and confirms that SeImpersonatePrivilege
is not enabled:
Unfortunately, this prevents elevation to NT AUTHORITYSYSTEM
via well-known methods such as those documented at [3] and [4]. The service user has additional privileges over those of a standard low-privileged user, however, such as membership of the NT AUTHORITYSERVICE
group (which will be of particular interest below when we discuss our second vulnerability).
On Windows XP, such fine grained configuration of services is not possible and immediate elevation to NT AUTHORITYSYSTEM
is straightforward.
The Update Orchestrator Service (CVE-2019-1322)
Having discovered the vulnerability described above, we were naturally curious to see if the privileges obtained would allow us to exploit another vulnerability to gain full control of a host. A scan of the default access controls applied to objects that might be of use quickly identified the Update Orchestrator Service.
The Update Orchestrator Service runs as NT AUTHORITYSYSTEM
and is enabled by default on Windows 10 and Windows Server 2019. The following output from a call to the SysInternals tool accesschk.exe
shows the explicit access controls enabled on the Update Orchestrator Service (UsoSvc
) on Windows 10 versions 1803 to 1903 (prior to patching, of course!):
UsoSvc
Medium Mandatory Level (Default) [No-Write-Up]
R NT AUTHORITYAuthenticated Users
SERVICE_QUERY_STATUS
SERVICE_QUERY_CONFIG
SERVICE_INTERROGATE
SERVICE_ENUMERATE_DEPENDENTS
SERVICE_START
SERVICE_USER_DEFINED_CONTROL
R BUILTINAdministrators
SERVICE_QUERY_STATUS
SERVICE_QUERY_CONFIG
SERVICE_INTERROGATE
SERVICE_ENUMERATE_DEPENDENTS
SERVICE_START
SERVICE_STOP
SERVICE_USER_DEFINED_CONTROL
RW NT AUTHORITYSYSTEM
SERVICE_ALL_ACCESS
RW NT AUTHORITYSERVICE
SERVICE_ALL_ACCESS
The final two lines from this output show that users in the group NT AUTHORITYSERVICE
have full access to the service. In particular, users in this group are able to stop, reconfigure and start the service and this allows such a user to execute arbitrary commands as NT AUTHORITYSYSTEM
.
For example, the following commands (when executed as a user in the group NT AUTHORITYSERVICE
) will add an administrative user named _tmpAdmUser
(with password H.jqt41Kz!a!
) to a vulnerable host and restore the service to its default state:
sc stop UsoSvc
sc config UsoSvc binpath= "cmd.exe /c net user /add _tmpAdmUser H.jqt41Kz!a! "
sc start UsoSvc
sc stop UsoSvc
sc config UsoSvc binpath= "cmd.exe /c net localgroup administrators /add _tmpAdmUser "
sc start UsoSvc
sc stop UsoSvc
sc config UsoSvc binpath= "C:WINDOWSsystem32svchost.exe -k netsvcs -p"
sc start UsoSvc
A selection of Windows services were examined and the results indicate that all services running as either LOCAL SERVICE
or NETWORK SERVICE
are able to perform this attack. In particular, the UPnP Device Host service described above is able to perform this attack, allowing elevation of privilege from any local user to the SYSTEM
user on Windows 10 (versions 1803 to 1903) by chaining CVE-2019-1405 and CVE-2019-1322.
Patches
CVE-2019-1322 was addressed in October 2019 by removing full control to the Update Orchestrator Service from users in the SERVICE
group.
CVE-2019-1405 was addressed in November 2019 by implementing access checks in the methods ReferenceContainer
, CreateInstance
and CreateInstanceWithProgID
to ensure that the caller is a member of the Administrators group.
References
[1] https://github.com/tyranid/oleviewdotnet
[2] https://docs.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance
[3] https://foxglovesecurity.com/2016/09/26/rotten-potato-privilege-escalation-from-service-accounts-to-system/
[4] https://bugs.chromium.org/p/project-zero/issues/detail?id=325 redir=1
Published date: 12 November 2019
Written by: Phillip Langlois and Edward Torkington