Monday, July 17, 2017

"Bypassing" Microsoft's Patch for CVE-2017-0199

Background

If you have followed my research on the infamous CVE-2017-0199 zero-day attack, you may know we (w/ my colleague Bing) did a presentation titled “Moniker Magic: Running Scripts Directly in Microsoft Office” at the SYSCAN360 security conference on May 31th in Seattle, WA. The slides are available at https://sites.google.com/site/zerodayresearch/Moniker_Magic_final.pdf. During the presentation, we explained the background of the CVE-2017-0199 (there’re actually 2 bugs/variants under the same CVE-ID, including the “secret” one that was reported by myself in January), the root cause of the two vulnerabilities, as well as how Microsoft addressed them.

The Microsoft patch actually leverages a mechanism on the Windows level, which is called "COM Activation Filter". The patch filters out 2 dangerous CLSIDs (a CLSID is an identifier of a COM object) leveraged in the 2 variants, respectively. However, the whole "features" aren't changed at all. It means that if someone finds another (the 3rd) dangerous COM object, with some modifications of the original PoC, he/she is able to achieve remote code execution once again. We expressed our concerns at the conference as well as during personal conversations with Microsoft security folks.

The story didn't end there, in fact, there's indeed such a variant, it's reported to Microsoft just on the day before our presentation (May 30), I have kept the secret during the conference (sorry for the infosec friends:-)). As the new patch has been released on Patch Tuesday last week, let’s talk about it.

If you haven't applied the new patch yet, you're highly recommended to do so right now.

The Details

The new variant leverages the Composite Moniker. According to MSDN,

"A composite moniker is a moniker that is a composition of other monikers and can determine the relation between the parts. This lets you assemble the complete path to an object given two or more monikers that are the equivalent of partial paths."

The following picture shows the stream of the Composite Moniker used in our PoC.



As we explained in our presentation, the stream of the moniker is actually the same as the so-called "MonikerStream" in the "\x01Ole" stream in the RTF-style PoC, it means that if you fill the above stream in the "MonikerStream" position, a Composite Moniker will be called in place.

When we put the sample test.sct file as we used in our “PPSX Script Moniker” bug (see our slide 30) in the right place (here we put it on the “C:\temp\test.sct” on local machine, but it could be put on remote computer via various protocols such as SMB share), and ran the modified RTF file, we got a beautiful calc.exe popping up on the latest Windows 10 + Office 2016 environment (before the July Patch Tuesday, of course).


How it worked? Well, the Composite Moniker is really a complex place but I’m going to try to explain a little bit. As we know, a Composite Moniker means there’re 2 or more Monikers working together. In our PoC, the Composite Moniker contains 2 monikers, a File Moniker and a so-called "new" Moniker. We could easily find out the CLSIDs in the stream.

{00000309-0000-0000-C000-000000000046} -> Composite Moniker (position 0)
{00000303-0000-0000-C000-000000000046} -> File Moniker (position 0x14)
{ECABAFC6-7F19-11D2-978E-0000F8757E2A} -> “new” Moniker (position 0xA1)

When binding the Composite Moniker, the binding process starts from the right-side Moniker to the left-side Moniker, while the left-side Moniker is held in the “pmkToLeft” parameter of the “IMoniker::BindToObject()” method.

HRESULT BindToObject(
  [in]  IBindCtx *pbc,
  [in]  IMoniker *pmkToLeft,
  [in]  REFIID   riidResult,
  [out] void     **ppvResult
);

When binding the “new” Moniker, it obtains the left-side moniker via the “pmkToLeft” parameter. In this case, the left-side Moniker is the File Moniker. So, it uses the File Moniker to initialize an object determined by the extension name ".sct". If we look into our Windows Registry, we could easily figure out the CLSID of the object is "{06290BD2-48AA-11D2-8432-006008C3FBFC}", progid is "scriptletfile". So the "scriptletfile" object is created and initialized by the content of the test.sct. This is a typical File Moniker binding process.

The “new” Moniker queries the IClassFactory interface to communicate with the returned object. Since the “scriptletfile” object has the IClassFactory interface exposed, the method IClassFactory::CreateInstance() is called. The "scriptletfile" object's IClassFactory::CreateInstance() implementation starts the scripting environment and runs our scripts, so this is the problem.

So, in an easier-to-understand “script” language, the logic looks like the following:

new (object persisted in “\\127.0.0.1\C$\temp\test.sct”)

Here is the call stack when calc.exe is being popped up, note the highlighted key functions.

0013cad8 67d5d248 kernel32!CreateProcessW
0013cb60 67d5d54a wshom!CWshShell::CreateShortcut+0x161
0013cbc0 750bcc68 wshom!CWshShell::Exec+0x19a
0013cbe0 750bcae2 OLEAUT32!DispCallFunc+0x165
0013cc70 67d601c7 OLEAUT32!CTypeInfo2::Invoke+0x23f
0013cca0 67d5b055 wshom!CDispatch::Invoke+0x5c
0013cccc 67115424 wshom!CWshExec::Invoke+0x29
0013cd10 6711505b jscript!IDispatchInvoke2+0x8d
0013ce08 67117622 jscript!VAR::InvokeByName+0x389
0013ce54 671175d6 jscript!VAR::InvokeDispName+0x3e
0013ce80 671144a7 jscript!VAR::InvokeByDispID+0x310a
0013d278 671148ff jscript!CScriptRuntime::Run+0x12b9
0013d374 67114783 jscript!ScrFncObj::CallWithFrameOnStack+0x15f
0013d3cc 67114cc3 jscript!ScrFncObj::Call+0x7b
0013d470 67123797 jscript!CSession::Execute+0x23d
0013d4bc 67120899 jscript!COleScript::ExecutePendingScripts+0x16b
0013d4d8 6ec2831f jscript!COleScript::SetScriptState+0x51
0013d4e8 6ec28464 scrobj!ScriptEngine::Activate+0x1a
0013d500 6ec299d3 scrobj!ComScriptlet::Inner::StartEngines+0x6e
0013d550 6ec2986e scrobj!ComScriptlet::Inner::Init+0x156
0013d560 6ec2980b scrobj!ComScriptlet::New+0x3f
0013d580 6ec297d0 scrobj!ComScriptletConstructor::CreateScriptletFromNode+0x26
0013d5a0 6ec33b7e scrobj!ComScriptletConstructor::Create+0x4c
0013d5cc 6ec22946 scrobj!ComScriptletFactory::CreateInstanceWithContext+0x115
0013d5e8 6f2264be scrobj!ComScriptletFactory::CreateInstance+0x19
0013d63c 766bb5dd comsvcs!CNewMoniker::BindToObject+0x14f
0013d670 767240c9 ole32!CCompositeMoniker::BindToObject+0x105 [d:\w7rtm\com\ole32\com\moniker2\ccompmon.cxx @ 1104]
0013d6dc 5fc737a6 ole32!CDefLink::BindToSource+0x14e [d:\w7rtm\com\ole32\ole232\stdimpl\deflink.cpp @ 4611]
0013d720 5f7cdad1 wwlib!wdGetApplicationObject+0x68f70

Why it bypassed Microsoft's patch?

As we explained in our presentation, the Microsoft's April Patch banned two objects in Office, they are:

{3050F4D8-98B5-11CF-BB82-00AA00BDCE0B} -> the “htafile” object
{06290BD3-48AA-11D2-8432-006008C3FBFC} -> the “script” object

They didn't ban any of the CLSIDs used in our new PoC.:-) While the "{06290BD3-48AA-11D2-8432-006008C3FBFC}" is very close to the "{06290BD2-48AA-11D2-8432-006008C3FBFC}", they are still not the same one. The banned one is “script” object, which, when being bind, would find and run scripts for us nicely. However, the “scriptletfile” object could also do the same work with a little help from the “new” Moniker.

Some Thoughts

There's some of my personal thoughts around this, the first is absolutely this new variant strongly demonstrates this is an “open” attack surface, and Microsoft's work wasn’t done very well. While I prefer not to judge Microsoft's patching strategy why they didn’t touch the features as they may have their own considerations (compatibility, user experience, etc), it's a clear proof that the patching strategy is weak, or weaker than I expected. The patching strategy is very similar to the ActiveX "killbit" solution when in the old days ActiveX vulnerabilities were very popular. It's an "easy fix" - people find a vulnerable object, we kill the it, people find another, we kill another, easy but not proactive, I'd say.

Also, it's the reason why I personally prefer to say the "RTF URL Moniker" issue, the "PPSX Script Moniker" issue, and this one, are separated bugs and should be assigned with different CVE-IDs (though Microsoft has assigned a new CVE-ID, CVE-2017-8570, for this variant). Microsoft putting them under a same CVE has caused confusions - we didn't assign all the ActiveX vulnerabilities as one CVE-ID, right?

Stay secure.