Exploit-DB #45893
Description: Microsoft Windows - DfMarshal Unsafe Unmarshaling Privilege Escalation
Type: local
Platform: windows
Date: 2018-11-20
Author: Google Security Research

Exploit:
Windows: DfMarshal Unsafe Unmarshaling Elevation of Privilege (Master)
Platform: Windows 10 1803 (not tested earlier, although code looks similar on Win8+)
Class: Elevation of Privilege

Note, this is the master issue report for the DfMarshal unmarshaler. I’m reporting multiple, non-exhaustive, issues in this marshaler in case you decide that you want to try and “fix” it rather than blocking the marshaler outright.

Summary: The unmarshaler for Storage objects is complete unsafe and yet is marked as a system trusted marshaler. There are multiple ways of abusing this to unmarshaler to get privilege escalation.

Description:

Storage objects are used by different parts of the OS and Office as a structured container format for sub-streams of data. You can create a new instance using APIs such as StgCreateDocFile. Being a COM object it can be marshaled around between processes, including special support during COM activation through CoGetInstanceFromIStorage. While all the important interfaces have proxy support the object also supports custom marshaling to improve performance when marshaling either INPROC or a LOCAL context. 

The COM class DfMarshal CLSID:0000030b-0000-0000-c[PII_REDACTED]00046 (in coml2.dll on Windows 10, ole32.dll downlevel) implements the custom unmarshaling for storage objects. When marshaling the implementation generates the following output:

MSHFLAGS <4 bytes>
Object Type IID <16 bytes> - Either IID_IStream or IID_IStorage.
Standard Marshaled Interface <Variable> - Used if the custom marshal fails.
SDfMarshalPacket <0x70 bytes on 64 bit, 0x44 on 32 bit> - Data for the custom marshal.

The SDfMarshalPacket has the following structure, note this comes from the Windows 8.1 private symbols for OLE32.DLL which are available on the public symbol server. On Windows 10 when the code was moved to COML2.DLL the private symbols didn’t move with it, however the code only seems to have had minor changes between 8.1 and 10.

struct SDfMarshalPacket
{
  CBasedPubDocFilePtr pdf;
  CBasedPubStreamPtr pst;
  CBasedSeekPointerPtr psp;
  CBasedMarshalListPtr pml;
  CBasedDFBasisPtr pdfb;
  CBasedGlobalContextPtr pgc;
  CBasedGlobalFileStreamPtr fsBase;
  CBasedGlobalFileStreamPtr fsDirty;
  CBasedGlobalFileStreamPtr fsOriginal;
  unsigned int ulHeapName;
  unsigned int cntxid;
  GUID cntxkey;
  CPerContext *ppc;
  HANDLE hMem;
};

The Ptr structures are native pointer sized values which are used as relative offsets into a shared memory section. The cntxid is the PID of the marshaling process, the hMem a handle to a section object which contains the shared allocation pool for use between processes. When the custom unmarshaling process starts the receiving process will try and open the process containing the shared memory handle (using cntxid and hMem) and duplicate it into the current process. Then it will map the section into memory and rebuild a local storage object based on the various relative pointers stored in the marshaled structure. Note that there is provision for performance improvements for in-process marshaling where cntxkey is a random GUID value which is known only to the process (it’s not set for cross context marshal). In that case ppc is used as a valid pointer, but ppc is always set so this leaks memory layout information to the process the object is marshaled to (not reporting this one separately). 

This will only work if the process can open the marshalling process for PROCESS_DUP_HANDLE access. This restricts this to processes at the same or higher privilege, therefore an obvious target would be unmarshaling this data from a user into a system service. Fortunately there’s some protection against that, the unmarshal occurs in CSharedMemoryBlock::InitUnMarshal and looks something like the following:

int CSharedMemoryBlock::InitUnMarshal(void *hMem, 
                                      unsigned int dwProcessId, 
                                      unsigned int culCommitSize) {
  unsigned int dwCurrentSession;
  unsigned int dwSourceSession;

  ProcessIdToSessionId(dwProcessId, &dwSourceSession);
  ProcessIdToSessionId(GetCurrentProcessId(), &dwCurrentSession);
  if (dwSourceSession != dwCurrentSession)
    return E_ACCESSDENIED;
  HANDLE hProcess = OpenProcess(PROCESS_DUP_HANDLE, 0, dwProcessId);
  ...
}

The code contains a check that the process containing the shared section is in the same console session as the process doing the unmarshal. If they’re not in the same session then the unmarshal process will fail. It’s unclear if this is a security check or whether it’s a reliability check, and even if it’s a security check it’s not hard to find a way around this.

One thought would be to try and use this to escape a sandbox, such as AppContainer as the sandbox process and a likely COM target would all be in the same session. While there are checks for the current process being in an AppContainer (so an AC process will never use the custom unmarshaling) there are no checks for the caller being an in AC. In fact there would be as the default HYBRID custom marshaling policy should kick in and block the custom unmarshal. However as DfMarshal is marked as a system trusted marshaler, it will still execute. It turns out that it’s difficult to trivially use this from a sandbox as later in the initialization an event object is opened by name (in CDfMutex::Init) from the current session’s BaseNamedObjects directory which an AC can’t write to. However if some other process in the same session had already shared a storage object, creating the event _and_ the AC could read the randomly assigned name it could be hijacked. 

So we’re back to either abusing something like UAC elevated processes/runas on the same desktop (doable but not a security boundary) or try and bypass the check to unmarshal from a user process into a system process. The key is the knowledge that the unmarshaler will open any process we tell it to,