利用未记录的IStandardActivator接口实现跨会话DCOM激活攻击

本文详细分析了如何通过未记录的IStandardActivator接口结合会话标识符和IStorage激活,实现跨会话DCOM对象激活,从而扩展NTLM中继攻击的攻击面,涉及COM内部机制和实际漏洞利用代码。

标准激活自我实现伟大

本周@decoder_it和@splinter_code披露了一种滥用DCOM/RPC NTLM中继攻击访问远程服务器的新方法。这基于一个事实:如果您以用户身份登录到会话0(例如通过PowerShell远程处理)并调用CoGetInstanceFromIStorage,DCOM激活器将在最低交互会话而非会话0上创建对象。一旦对象创建,IStorage对象的初始解组将在认证到该会话的用户上下文中进行。如果该用户恰好是特权用户(如域管理员),则NTLM认证可能被中继到远程服务器,从而引发问题。

此攻击的明显问题在于需要处于会话0。当然,非管理员用户可能被允许通过PowerShell远程处理认证到系统,但这比在终端服务器上认证并攻击其他用户要罕见得多。如果能以某种方式选择对象创建的会话,那就更好了。

当然,这已经存在,您可以使用会话moniker跨会话激活对象(除了特殊的会话0)。我曾多次滥用此功能进行跨会话攻击,例如。我多次告诉Microsoft需要修复此激活路径,因为非管理员能够执行此操作毫无意义。但我的警告未被重视。

如果您阅读会话moniker的描述,可能会注意到一个问题:它不能与IStorage激活结合使用。COM API只提供其中之一。然而,如果您仔细研究DCOM协议文档,会发现它们在技术上是独立的。会话激活通过设置SpecialPropertiesData激活属性中的dwSessionId字段指定。而封送的IStorage对象可以在InstanceInfoData激活属性的ifdStg字段中传递。您将这些激活属性打包并发送到IRemoteSCMActivator的RemoteGetClassObject或RemoteCreateInstance方法。当然,这可能无法真正工作,但至少它们是独立的属性,可以混合使用。

测试此问题的问题在于实现DCOM激活很麻烦。激活属性首先需要在blob中进行NDR封送。然后需要正确打包才能发送到激活器。此外,文档仅适用于远程激活,这不是我们想要的,而且本地激活有一些奇怪的特性,我不打算深入探讨。是否有任何文档化的方式可以访问激活器而无需完成所有这些工作?

不,抱歉。但如果您感兴趣,有一种未记录的方式?当然?好的,我们继续。这类挑战的关键在于查看系统如何 already 执行此操作。具体来说,我们可以查看会话moniker如何激活对象,也许从中我们可以幸运地 reuse 它用于我们自己的目的。

从哪里开始?如果您阅读此MSDN文章,您可以看到需要调用MkParseDisplayNameEx将字符串解析为moniker。但这实际上是MkParseDisplayName的包装器,以提供我们不关心的URL moniker功能。我们将从OLE32中的MkParseDisplayName开始。

1
2
3
4
5
6
7
HRESULT MkParseDisplayName(LPBC pbc, LPCOLESTR szUserName, ULONG *pchEaten, LPMONIKER *ppmk) {
  HRESULT hr = FindLUAMoniker(pbc, szUserName, &pcchEaten, &ppmk);
  if (hr == MK_E_UNAVAILABLE) {
    hr = FindSessionMoniker(pbc, szUserName, &pcchEaten, &ppmk);
  }
  // 解析moniker的其余部分。
}

几乎 immediately 我们看到对FindSessionMoniker的调用,似乎有希望。深入研究该函数,我们找到了我们需要的内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
HRESULT FindSessionMoniker(LPBC pbc, LPCWSTR pszDisplayName, ULONG *pchEaten, LPMONIKER *ppmk) {
  DWORD dwSessionId = 0;
  BOOL bConsole = FALSE;

  if (wcsnicmp(pszDisplayName, L"Session:", 8))
    return MK_E_UNAVAILABLE;

  if (!wcsnicmp(pszDisplayName + 8, L"Console", 7)) {
    dwConsole = TRUE;
    *pcbEaten = 15;
  } else {
    LPWSTR EndPtr;
    dwSessionId = wcstoul(pszDisplayName + 8, &End, 0);
    *pcbEaten = EndPtr - pszDisplayName;
  }
  *ppmk = new CSessionMoniker(dwSessionId, bConsole);
  return S_OK;
}

此代码解析会话moniker数据,然后创建CSessionMoniker类的新实例。当然,这还没有进行任何激活。您不单独使用会话moniker,而是应该构建一个包含new或class moniker的复合moniker。MkParseDisplayName API将继续解析字符串(这就是为什么pchEaten被更新)并组合它找到的每个moniker。因此,如果您有moniker显示名称:

1
Session:3!clsid:0002DF02-0000-0000-C000-000000000046

API将返回一个复合moniker,由会话3的会话moniker和CLSID 0002DF02-0000-0000-C000-000000000046(Browser Broker)的类moniker组成。示例代码然后在复合moniker上调用BindToObject,该函数首先调用最右侧的moniker,即类moniker。

1
2
3
4
5
6
7
8
HRESULT CClassMoniker::BindToObject(LPBC pbc, LPMONIKER pmkToLeft, REFIID riid, void **ppv) {
  if (pmkToLeft) {
    IClassActivator pClassActivator;
    pmkToLeft->BindToObject(pcb, nullptr, IID_IClassActivator, &pClassActivator);
    return pClassActivator->GetClassObject(m_clsid, CLSCTX_SERVER, 0, riid, ppv);
  }
  // ...
}

pmkToLeft参数由复合moniker设置为左侧moniker,即会话moniker。我们可以看到类moniker调用会话moniker的BindToObject方法,请求IClassActivator接口。然后它调用GetClassObject方法,传递要激活的CLSID。我们快到了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HRESULT CSessionMoniker::GetClassObject(REFCLSID pClassID, CLSCTX dwClsContext, LCID locale, REFIID riid, void **ppv) {
  IStandardActivator* pActivator;
  CoCreateInstance(&CLSID_ComActivator, NULL, CLSCTX_INPROC_SERVER, IID_IStandardActivator, &pActivator);

  ISpecialSystemProperties pSpecialProperties;
  pActivator->QueryInterface(IID_ISpecialSystemProperties, &pSpecialProperties);
  pSpecialProperties->SetSessionId(m_sessionid, m_console, TRUE);

  return pActivator->StandardGetClassObject(pClassId, dwClsContext, NULL, riid, ppv);
}

最后,会话moniker创建一个具有IStandardActivator接口的新COM激活器对象。然后它查询ISpecialSystemProperties接口并设置moniker的会话ID和控制台状态。然后它在IStandardActivator上调用StandardGetClassObject方法,您现在应该拥有一个跨会话的COM服务器。当然,这些接口或类都没有官方文档记录(AFAIK)。

关键问题是,您是否也可以通过IStandardActivator接口进行IStorage激活?在COMBASE中深入研究该接口的实现,您会发现其功能之一是:

1
HRESULT StandardGetInstanceFromIStorage(COSERVERINFO* pServerInfo, REFCLSID pclsidOverride, IUnknown* punkOuter, CLSCTX dwClsCtx, IStorage* pstg, int dwCount, MULTI_QI pResults[]);

似乎答案是肯定的。当然,可能仍然无法将两者混合。这就是为什么我用C#编写了一个快速而粗糙的示例,可在此处获取。似乎工作正常。当然,我还没有用实际漏洞测试它是否在该场景中工作。这是其他人要做的事情。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计